feat(builder): declare multi-arch builds with ARCHES

Project architecture lists belong in makefile.builder, not .env.builder.
deb/push-deb fan out when ARCH is omitted; Docker platforms derive from
ARCHES. Legacy makefiles without ARCHES keep the old single-arch behavior.
This commit is contained in:
2026-09-08 16:31:53 +08:00
parent c248c3f0e3
commit aa7a265e76
9 changed files with 433 additions and 36 deletions
+173
View File
@@ -102,6 +102,13 @@ class BuilderCheckTests(unittest.TestCase):
self.assertIn("makefile.builder", contract)
self.assertIn(".env.builder", contract)
self.assertIn("不要读取或改写用户 `.env`", contract)
self.assertIn("ARCHES", contract)
readme = (REPO_ROOT / "skills" / "builder" / "README.md").read_text(
encoding="utf-8"
)
self.assertIn("ARCHES", readme)
self.assertIn("amd64 arm64", readme)
self.assertIn("多架构", readme)
def test_no_makefile_without_ready_is_usage_error(self) -> None:
with tempfile.TemporaryDirectory() as temp:
@@ -233,6 +240,172 @@ class BuilderCheckTests(unittest.TestCase):
self.assertNotIn("hijacked", text)
self.assertNotIn(":latest", text)
def test_default_template_rejects_arch_outside_arches(self) -> None:
with tempfile.TemporaryDirectory() as temp:
project = Path(temp)
init_repo(project)
write_contract_makefile(project)
result = subprocess.run(
["make", "-C", str(project), "-f", "makefile.builder", "-n", "build", "ARCH=arm64"],
capture_output=True,
text=True,
check=False,
)
self.assertNotEqual(result.returncode, 0, result.stderr)
self.assertIn("amd64", result.stderr)
def test_multi_arches_deb_and_push_fan_out(self) -> None:
with tempfile.TemporaryDirectory() as temp:
project = Path(temp)
init_repo(project)
write_contract_makefile(project)
makefile = project / "makefile.builder"
makefile.write_text(
makefile.read_text(encoding="utf-8").replace(
"ARCHES ?= amd64", "ARCHES ?= amd64 arm64"
),
encoding="utf-8",
)
code, text = run_check(
project, "--ready", which={"dpkg-deb": "/usr/bin/dpkg-deb"}
)
self.assertEqual(code, 0, text)
self.assertIn("ARCHES: 'amd64 arm64'", text)
deb = subprocess.run(
["make", "-C", str(project), "-f", "makefile.builder", "-n", "deb"],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(deb.returncode, 0, deb.stderr)
combined = deb.stdout + deb.stderr
self.assertIn("ARCH=amd64", combined)
self.assertIn("ARCH=arm64", combined)
pinned = subprocess.run(
[
"make",
"-C",
str(project),
"-f",
"makefile.builder",
"-n",
"deb",
"ARCH=arm64",
],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(pinned.returncode, 0, pinned.stderr)
self.assertIn("_arm64.deb", pinned.stdout)
self.assertNotIn("_amd64.deb", pinned.stdout)
def test_legacy_makefile_without_arches_still_passes(self) -> None:
with tempfile.TemporaryDirectory() as temp:
project = Path(temp)
init_repo(project)
(project / "makefile.builder").write_text(
"\n".join(
[
"ARCH ?= amd64",
"ifneq ($(filter $(ARCH),amd64 arm64),$(ARCH))",
"$(error ARCH must be amd64 or arm64)",
"endif",
"PROJECT_NAME ?= $(notdir $(CURDIR))",
"DIST_DIR ?= dist",
f"include {SCRIPTS / 'version.mk'}",
f"BUILDER_SCRIPTS := {SCRIPTS}",
".PHONY: help version clean build deb push",
"help:",
"\t@echo $(PROJECT_NAME) $(VERSION)",
"version:",
"\t@echo $(VERSION)",
"clean:",
"\trm -rf $(DIST_DIR)",
"build:",
"\tmkdir -p $(DIST_DIR)",
"\t@echo build linux/$(ARCH)",
"deb: build",
"\tmkdir -p $(DIST_DIR)",
"\tdpkg-deb --build packaging $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb",
"push:",
"\t$(BUILDER_SCRIPTS)/upload_deb.sh $(DIST_DIR)/$(PROJECT_NAME)_$(VERSION)_$(ARCH).deb",
"",
]
),
encoding="utf-8",
)
code, text = run_check(
project, "--ready", which={"dpkg-deb": "/usr/bin/dpkg-deb"}
)
self.assertEqual(code, 0, text)
self.assertIn("arches target: absent", text)
result = subprocess.run(
["make", "-C", str(project), "-f", "makefile.builder", "-n", "build", "ARCH=arm64"],
capture_output=True,
text=True,
check=False,
)
self.assertEqual(result.returncode, 0, result.stderr)
def test_publish_docker_derives_platforms_from_arches(self) -> None:
script = SCRIPTS / "publish_docker.sh"
with tempfile.TemporaryDirectory() as temp:
project = Path(temp)
init_repo(project)
write_contract_makefile(project)
makefile = project / "makefile.builder"
makefile.write_text(
makefile.read_text(encoding="utf-8").replace(
"ARCHES ?= amd64", "ARCHES ?= amd64 arm64"
),
encoding="utf-8",
)
(project / "Dockerfile").write_text("FROM scratch\n", encoding="utf-8")
env = os.environ.copy()
env["BUILDER_SKILL_DIR"] = str(SCRIPTS.parent)
env.pop("PLATFORMS", None)
derived = subprocess.run(
[
str(script),
"--registry",
"example.com",
"--repository",
"ns/foo",
"--tag",
"dry-run-tag",
"--dry-run",
],
cwd=project,
capture_output=True,
text=True,
check=False,
env=env,
)
self.assertEqual(derived.returncode, 0, derived.stdout + derived.stderr)
self.assertIn("Platform: linux/amd64,linux/arm64", derived.stdout)
env["PLATFORMS"] = "linux/amd64"
overridden = subprocess.run(
[
str(script),
"--registry",
"example.com",
"--repository",
"ns/foo",
"--tag",
"dry-run-tag",
"--dry-run",
],
cwd=project,
capture_output=True,
text=True,
check=False,
env=env,
)
self.assertEqual(overridden.returncode, 0, overridden.stdout + overridden.stderr)
self.assertIn("Platform: linux/amd64", overridden.stdout)
self.assertNotIn("linux/arm64", overridden.stdout)
if __name__ == "__main__":
unittest.main()