diff --git a/agent-notes/repair/task6-installer-srcname-baseline.md b/agent-notes/repair/task6-installer-srcname-baseline.md new file mode 100644 index 0000000..aac52c8 --- /dev/null +++ b/agent-notes/repair/task6-installer-srcname-baseline.md @@ -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. diff --git a/installer_builder/innosetup.py b/installer_builder/innosetup.py index 027968b..89142bd 100644 --- a/installer_builder/innosetup.py +++ b/installer_builder/innosetup.py @@ -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 @@ -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): @@ -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 ( diff --git a/tests/test_innosetup_srcname.py b/tests/test_innosetup_srcname.py new file mode 100644 index 0000000..7fba582 --- /dev/null +++ b/tests/test_innosetup_srcname.py @@ -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()