diff --git a/apps/maple-agent/scripts/build-release.sh b/apps/maple-agent/scripts/build-release.sh index 495340a8f..1931b15e8 100755 --- a/apps/maple-agent/scripts/build-release.sh +++ b/apps/maple-agent/scripts/build-release.sh @@ -41,6 +41,16 @@ if [[ ${#target_args[@]} != 0 ]]; then fi python3 "$component/scripts/generate-third-party-notices.py" binary="$component/target/release/maple-agent" -case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) binary="${binary}.exe" ;; esac -"$binary" --build-info | - python3 -c 'import json,sys; info=json.load(sys.stdin); expected=sys.argv[1]; assert info["profile"] == expected, "incorrect release profile"' "$1" +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + # The unsigned prebuilt Windows bootstrap is not an installed app. + # Validate its immutable PE metadata without entering runtime trust or + # installation checks; signing and installed-app smoke happen later. + python3 "$component/scripts/windows-build-info.py" "${binary}.exe" \ + --profile "$1" --source-sha "$(git rev-parse HEAD)" >/dev/null + ;; + *) + "$binary" --build-info | + python3 -c 'import json,sys; info=json.load(sys.stdin); expected=sys.argv[1]; assert info["profile"] == expected, "incorrect release profile"' "$1" + ;; +esac diff --git a/apps/maple-agent/scripts/test_release_packaging.py b/apps/maple-agent/scripts/test_release_packaging.py index 881c406f8..5650827c1 100644 --- a/apps/maple-agent/scripts/test_release_packaging.py +++ b/apps/maple-agent/scripts/test_release_packaging.py @@ -19,6 +19,7 @@ import unittest from test_macos_build_info import macho from test_third_party_notices import notice_fixture +from test_windows_build_info import pe, public_info as windows_info SCRIPTS = Path(__file__).resolve().parent @@ -170,6 +171,127 @@ def test_downloaded_binary_must_be_a_regular_file(self): self.assertEqual(target.stat().st_mode & 0o777, 0o644) +@unittest.skipUnless(os.name == "posix", "hermetic shell fixture requires a POSIX host") +class BuildReleaseMetadataTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.component = Path(temporary.name) / "component with spaces" + scripts = self.component / "scripts" + scripts.mkdir(parents=True) + for name in ("build-release.sh", "windows-build-info.py", "release-info.py"): + shutil.copy2(SCRIPTS / name, scripts / name) + shutil.copy2(SCRIPTS.parent / "release-profiles.json", self.component) + (scripts / "generate-third-party-notices.py").write_text( + "from pathlib import Path\nPath('notices-generated').write_text('called')\n" + ) + self.env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1") + for arguments in (("init", "--quiet"), ("add", "."), + ("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-c", "commit.gpgsign=false", "-c", "core.hooksPath=/dev/null", + "commit", "--quiet", "-m", "fixture source")): + subprocess.run(["git", "-C", str(self.component), *arguments], env=self.env, + check=True, capture_output=True, text=True) + self.source = subprocess.check_output( + ["git", "-C", str(self.component), "rev-parse", "HEAD"], env=self.env, text=True, + ).strip() + self.input = self.component / "fixture-binary" + self.metadata = self.component / "fixture-info.json" + self.executed = self.component / "fixture-executed" + self.commands = self.component / "cargo-commands.jsonl" + tools = self.component / "tools" + tools.mkdir() + for name, source in { + "uname": "import os\nprint(os.environ['FIXTURE_OS'])\n", + "cargo": """import json, os, shutil, sys +from pathlib import Path +arguments = sys.argv[1:] +with Path(os.environ['FIXTURE_COMMANDS']).open('a') as log: + log.write(json.dumps(arguments) + '\\n') +if arguments == ['fetch', '--locked']: + sys.exit(0) +windows = os.environ['FIXTURE_OS'].startswith(('MINGW', 'MSYS', 'CYGWIN')) +expected = ['build', '--release', '-p', 'maple-agent-app', '--locked', '--timings'] +if windows: + expected += ['--target', 'x86_64-pc-windows-msvc'] +assert arguments == expected, arguments +target = Path('target/x86_64-pc-windows-msvc/release/maple-agent.exe' if windows else 'target/release/maple-agent') +target.parent.mkdir(parents=True, exist_ok=True) +shutil.copyfile(os.environ['FIXTURE_INPUT'], target) +target.chmod(0o644 if windows else 0o755) +""", + }.items(): + tool = tools / name + tool.write_text(f"#!{sys.executable}\n" + source) + tool.chmod(0o755) + self.env.update(PATH=str(tools) + os.pathsep + self.env["PATH"], + FIXTURE_INPUT=str(self.input), FIXTURE_COMMANDS=str(self.commands), + FIXTURE_METADATA=str(self.metadata), FIXTURE_EXECUTED=str(self.executed)) + + def build(self, operating_system, profile="dev"): + self.env["FIXTURE_OS"] = operating_system + result = subprocess.run(["bash", str(self.component / "scripts/build-release.sh"), profile], + env=self.env, capture_output=True, text=True) + self.assertEqual((self.component / "notices-generated").read_text(), "called") + return result + + def test_windows_build_checks_the_staged_pe_without_executing_it(self): + for operating_system in ("MINGW64_NT-10.0", "MSYS_NT-10.0", "CYGWIN_NT-10.0"): + for profile in ("dev", "prod"): + with self.subTest(operating_system=operating_system, profile=profile): + expected = pe(windows_info(profile, self.source)) + self.input.write_bytes(expected) + self.input.chmod(0o644) + # This genuine metadata-only PE cannot execute on the + # portable fixture host. Success must come from parsing. + with self.assertRaises(OSError): + subprocess.run([str(self.input), "--build-info"], check=True) + result = self.build(operating_system, profile) + self.assertEqual(result.returncode, 0, result.stderr) + staged = self.component / "target/release/maple-agent.exe" + self.assertEqual(staged.read_bytes(), expected) + self.assertEqual(staged.stat().st_mode & 0o777, 0o644) + commands = [json.loads(line) for line in self.commands.read_text().splitlines()] + self.assertEqual(len(commands), 12) + self.assertEqual(commands[::2], [["fetch", "--locked"]] * 6) + + def test_windows_build_rejects_wrong_profile_source_and_dirty_revision(self): + for profile in ("dev", "prod"): + wrong_profile = "prod" if profile == "dev" else "dev" + for identity, error in ( + (windows_info(wrong_profile, self.source), "release profile"), + (windows_info(profile, "b" * 40), "source commit"), + ({**windows_info(profile, self.source), "git_revision": self.source[:8] + "-dirty"}, "revision"), + ): + with self.subTest(profile=profile, error=error): + self.input.write_bytes(pe(identity)) + result = self.build("MINGW64_NT-10.0", profile) + self.assertNotEqual(result.returncode, 0) + self.assertIn(error, result.stderr) + self.assertIn("static Windows build-info validation failed", result.stderr) + + def test_macos_and_linux_keep_executing_the_existing_build_info_cli(self): + self.input.write_text(f"#!{sys.executable}\n" + """import json, os, sys +from pathlib import Path +assert sys.argv[1:] == ['--build-info'] +Path(os.environ['FIXTURE_EXECUTED']).write_text(json.dumps(sys.argv[1:])) +print(Path(os.environ['FIXTURE_METADATA']).read_text()) +""") + for operating_system in ("Darwin", "Linux"): + for profile in ("dev", "prod"): + for reported in ("dev", "prod"): + with self.subTest(operating_system=operating_system, requested=profile, reported=reported): + self.executed.unlink(missing_ok=True) + self.metadata.write_text(json.dumps({"profile": reported})) + result = self.build(operating_system, profile) + self.assertEqual(json.loads(self.executed.read_text()), ["--build-info"]) + if reported == profile: + self.assertEqual(result.returncode, 0, result.stderr) + else: + self.assertNotEqual(result.returncode, 0) + self.assertIn("incorrect release profile", result.stderr) + + class LinuxVerifierExtractionTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory()