Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions agent-notes/repair/task6-installer-srcname-baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Repair legacy Inno Setup source and directory helpers

## Goal

Fix two confirmed baseline compatibility defects in the existing legacy module:
use the string values in importlib.machinery.SOURCE_SUFFIXES directly, and use
the distribution directory saved by InnoScript's constructor. Three production
lines change; one focused regression file loads the real production module.

## State

Isolated branch repair/task6-installer-srcname-baseline at
task-6/worktrees/installer-srcname-baseline starts at exact merged default
e59336b7b5acb88d76d829dc9b61ac6585ba189d. Origin is
https://github.com/accessibleapps/installer_builder. Canonical master 099ceb308
and its untracked user work, all archived/original tips, and other worktrees
remain untouched. The separate mypy worker's branches and source are not edited.
No remote actions or model calls are part of this local preparation.

## Decisions and why

Read global instructions, canonical untracked CLAUDE, exact baseline source,
manifest, README, hosted workflow, relevant existing fixtures and living notes.
No repository AGENTS or .agents directory was found. Existing CI authority is
the unchanged complete Windows Python 3.8/3.14 pytest suite and package build.

srcname accepts a module or dotted module name; srcnames forwards directly to it.
Real json/json.decoder modules and the real standard-library SOURCE_SUFFIXES
reach the string.suffix AttributeError. The module's changelog names these public
helpers, but no current internal builder caller was found: this is not a claim
that the normal installer command fails. Preserve source-sibling fallback and
the existing missing-source ValueError.

InnoScript.__init__ stores self.dist_dir, never self.builder. The legacy
innosetup.run creates this object and calls create, whose Setup handler reads
self.builder.dist_dir; chop has the same stale access. Both now read self.dist_dir.
The main InstallerBuilder uses NewInnoSetupCommand; do not describe this legacy
path defect as a normal installer outage. No compiler or signer behavior changes.

The fixture executes the real innosetup.py through importlib, with a private
temporary package name and empty native/freezer/signing module stubs. Registry
constants are inert objects. monkeypatch restores every replaced sys.modules
entry and distutils.command.__all__, including the legacy registration alias.
No copied helper implementation or AST extraction is used. Constructor tests
only scan pytest's temporary directory and render the Setup section in memory.

## Verification and surprises

Focused command (only the actively authored regression file):
task-6/worktrees/installer-baseline/.venv/Scripts/python.exe -m pytest
tests/test_innosetup_srcname.py -q --tb=short

Existing interpreter: CPython 3.8.20, pytest 8.3.5. Before any production fix,
all seven source-helper cases failed at line 306 with
AttributeError: 'str' object has no attribute 'suffix' (0.99 s).
After the one-line source fix, those same seven passed (0.64 s).
The two added directory cases then failed at lines 525/691 with
AttributeError: 'InnoScript' object has no attribute 'builder', while all seven
controls stayed green (0.68 s). After the two directory fixes, the unchanged
nine cases passed (0.44 s). Two existing invalid-escape deprecation warnings
were reported; none were suppressed. No full suite, build, mypy, compiler,
freezer, signing, registry API, network or native-resource action ran.

The production file retains its existing CRLF line endings; whitespace checking
uses git's cr-at-eol setting. The patch changes no support/dependency metadata,
workflow, existing assertions or configuration. No new skip or policy is added.

## Next step

Return the committed complete local diff/head and focused red/green evidence
for coordinator review before publication. Root owns any new draft, actual
hosted full matrix/build, Claude review, gate and merge. Those results are not
claimed by this focused local check. Keep all unrelated branches untouched.
6 changes: 3 additions & 3 deletions installer_builder/innosetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ def srcname(dottedname):
ext = ext.lower()

# Get source suffixes using importlib
_py_src_suffixes = [s.suffix for s in importlib.machinery.SOURCE_SUFFIXES]
_py_src_suffixes = importlib.machinery.SOURCE_SUFFIXES

if ext in _py_src_suffixes:
return filename
Expand Down Expand Up @@ -522,7 +522,7 @@ def parse_iss(self, s):
def chop(self, filename, dirname=""):
"""get relative path"""
if not dirname:
dirname = self.builder.dist_dir
dirname = self.dist_dir
if dirname[-1] not in "\\/":
dirname += "\\"
if filename.startswith(dirname):
Expand Down Expand Up @@ -688,7 +688,7 @@ def handle_iss_setup(self, lines, fp):
metadata = self.metadata
iss_metadata = dict((k, v % metadata)
for k, v in self.metadata_map.items())
iss_metadata["OutputDir"] = self.builder.dist_dir
iss_metadata["OutputDir"] = self.dist_dir
iss_metadata["AppId"] = self.appid
# add InfoBeforeFile
for filename in (
Expand Down
88 changes: 88 additions & 0 deletions tests/test_innosetup_srcname.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import distutils.command
import importlib.util
import io
import json
import json.decoder
import sys
from pathlib import Path
from types import ModuleType, SimpleNamespace

import pytest


@pytest.fixture
def innosetup_module(monkeypatch):
# Load the real legacy module without importing native packaging/signing code.
package_name = "_installer_srcname_test"
package = ModuleType(package_name)
package.__path__ = []
monkeypatch.setitem(sys.modules, package_name, package)
for name in (package_name + ".signtool", "win32api", "py2exe"):
monkeypatch.setitem(sys.modules, name, ModuleType(name))
registry = ModuleType("winreg")
for name in (
"HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER", "HKEY_CLASSES_ROOT",
"HKEY_USERS", "HKEY_CURRENT_CONFIG", "HKEY_PERFORMANCE_DATA",
):
setattr(registry, name, object())
monkeypatch.setitem(sys.modules, "winreg", registry)
monkeypatch.setattr(distutils.command, "__all__", list(distutils.command.__all__))
monkeypatch.setitem(
sys.modules, "distutils.command.innosetup", ModuleType("innosetup")
)
source = Path(__file__).resolve().parents[1] / "installer_builder" / "innosetup.py"
spec = importlib.util.spec_from_file_location(package_name + ".innosetup", source)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
monkeypatch.setitem(sys.modules, spec.name, module)
spec.loader.exec_module(module)
return module


@pytest.mark.parametrize("module", [json, json.decoder])
@pytest.mark.parametrize("use_name", [False, True])
def test_srcname_finds_real_standard_library_source(innosetup_module, module, use_name):
argument = module.__name__ if use_name else module

assert innosetup_module.srcname(argument) == module.__file__
assert Path(module.__file__).is_file()


def test_srcnames_accepts_modules_and_dotted_names(innosetup_module):
assert innosetup_module.srcnames(json, "json.decoder") == [
json.__file__, json.decoder.__file__
]


def test_srcname_falls_back_to_existing_source_sibling(innosetup_module, monkeypatch):
source = json.__file__
monkeypatch.setattr(json, "__file__", str(Path(source).with_suffix(".pyc")))

assert innosetup_module.srcname(json) == source


def test_srcname_preserves_missing_source_error(innosetup_module, tmp_path):
module = SimpleNamespace(__file__=str(tmp_path / "missing.pyc"))

with pytest.raises(ValueError, match="^Source file not found for "):
innosetup_module.srcname(module)


def test_chop_uses_constructor_distribution_directory(innosetup_module, tmp_path):
script = innosetup_module.InnoScript(str(tmp_path), SimpleNamespace(), "")

assert script.chop(str(tmp_path / "app.py")) == "app.py"


def test_setup_handler_uses_constructor_output_directory(innosetup_module, tmp_path):
metadata = SimpleNamespace(
name="Test App", version="1.0", author_email="", url="",
author="", copyright="", description="",
)
script = innosetup_module.InnoScript(str(tmp_path), metadata, "")
output = io.StringIO()

script.handle_iss_setup([], output)

assert script.iss_metadata["OutputDir"] == str(tmp_path)
assert "OutputDir=%s\n" % tmp_path in output.getvalue()