feat: 添加 ARM64 manylinux wheel 构建选项

复用现有 Docker 和 Nuitka 流程,隔离架构镜像并支持 aarch64 标签。已通过 47 项测试、代码及文档门禁,并完成 Python 3.10 ARM64 wheel 构建、隔离导入与内容审计。
This commit is contained in:
ykcheungab
2026-10-14 16:08:54 +08:00
parent 3953747f17
commit bb15b750fb
11 changed files with 192 additions and 19 deletions
+121
View File
@@ -0,0 +1,121 @@
"""Exercise the Windows launcher without downloading or compiling Docker images."""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
POWERSHELL = shutil.which("powershell")
REPO_ROOT = Path(__file__).resolve().parents[1]
pytestmark = pytest.mark.skipif(not POWERSHELL, reason="Windows PowerShell is required")
def run_launcher(tmp_path, arguments, image_architecture):
docker_dir = tmp_path / "build" / "wheel" / "docker"
docker_dir.mkdir(parents=True)
shutil.copy2(REPO_ROOT / "build/wheel/docker/run.ps1", docker_dir)
(docker_dir / "entrypoint.sh").write_text("#!/bin/bash\n", encoding="utf-8")
parameters = {}
iterator = iter(arguments)
for argument in iterator:
parameters[argument.lstrip("-")] = (
True if argument == "-SkipImageBuild" else next(iterator)
)
(tmp_path / "arguments.json").write_text(json.dumps(parameters), encoding="utf-8")
harness = tmp_path / "harness.ps1"
harness.write_text(
"""
$ErrorActionPreference = 'Stop'
$global:Calls = [System.Collections.Generic.List[object]]::new()
function docker {
$global:Calls.Add(@($args))
$global:LASTEXITCODE = 0
if ($args[0] -eq 'version') { return 'linux' }
if ($args[0] -eq 'image') { return '__ARCH__' }
}
$decoded = Get-Content (Join-Path $PSScriptRoot 'arguments.json') -Raw | ConvertFrom-Json
$parameters = @{}
foreach ($property in $decoded.PSObject.Properties) { $parameters[$property.Name] = $property.Value }
try {
& (Join-Path $PSScriptRoot 'build/wheel/docker/run.ps1') @parameters
} finally {
ConvertTo-Json -InputObject @($global:Calls.ToArray()) -Depth 5 |
Set-Content (Join-Path $PSScriptRoot 'calls.json') -Encoding UTF8
}
""".replace("__ARCH__", image_architecture),
encoding="utf-8",
)
result = subprocess.run(
[POWERSHELL, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", str(harness)],
capture_output=True,
text=True,
check=False,
)
calls = json.loads((tmp_path / "calls.json").read_text(encoding="utf-8-sig"))
return result, calls
@pytest.mark.parametrize(
"arguments,image_architecture,platform,architecture,image_name",
[
([], "amd64", "linux/amd64", "x86_64", "orisys-manylinux"),
(
["-Architecture", "aarch64"],
"arm64",
"linux/arm64",
"aarch64",
"orisys-manylinux-aarch64",
),
(
["-Architecture", "AARCH64"],
"arm64",
"linux/arm64",
"aarch64",
"orisys-manylinux-aarch64",
),
],
)
def test_architecture_controls_build_and_run(
tmp_path, arguments, image_architecture, platform, architecture, image_name
):
result, calls = run_launcher(tmp_path, arguments, image_architecture)
assert result.returncode == 0, result.stderr
build = next(call for call in calls if call[0] == "build")
run = next(call for call in calls if call[0] == "run")
assert build[build.index("--platform") + 1] == platform
assert build[build.index("--build-arg") + 1] == f"MANYLINUX_ARCH={architecture}"
assert build[build.index("-t") + 1] == image_name
assert run[run.index("--platform") + 1] == platform
assert run[-1] == image_name
@pytest.mark.parametrize(
"image_architecture,success", [("arm64", True), ("amd64", False)]
)
def test_reused_image_must_match_requested_architecture(
tmp_path, image_architecture, success
):
result, calls = run_launcher(
tmp_path,
[
"-Architecture",
"aarch64",
"-SkipImageBuild",
"-ImageName",
"custom-arm",
"-PythonVersions",
"3.10",
],
image_architecture,
)
assert not any(call[0] == "build" for call in calls)
assert (result.returncode == 0) == success
runs = [call for call in calls if call[0] == "run"]
if success:
assert runs[0][-1] == "custom-arm"
assert "ORISYS_PYTHON_TAGS=cp310-cp310" in runs[0]
else:
assert not runs
assert "architecture" in result.stderr.lower()
+7
View File
@@ -29,6 +29,13 @@ def test_compliant_manylinux_tags(wheel_package):
)
@pytest.mark.parametrize("tag", ["manylinux2014_aarch64", "manylinux_2_28_aarch64"])
def test_compliant_arm_manylinux_tags(wheel_package, tag):
assert wheel_package.is_compliant_manylinux_wheel(
Path(f"orisys-1.2.3-cp310-cp310-{tag}.whl")
)
def test_rejects_illegal_manylinux_tags(wheel_package):
assert not wheel_package.is_compliant_manylinux_wheel(
Path("orisys-1.2.3-cp310-cp310-manylinux_x86_64.whl")
+3 -2
View File
@@ -350,10 +350,11 @@ def test_bad_manylinux_tag_fails(tmp_path):
assert "platform-tag" in _failed_names(inspect_wheel(wheel, repo_root=repo))
def test_good_manylinux_tag_passes(tmp_path):
@pytest.mark.parametrize("architecture", ["x86_64", "aarch64"])
def test_good_manylinux_tag_passes(tmp_path, architecture):
wheel, repo = _make_wheel(
tmp_path,
filename=f"orisys-{VERSION}-cp312-cp312-manylinux_2_28_x86_64.whl",
filename=f"orisys-{VERSION}-cp312-cp312-manylinux_2_28_{architecture}.whl",
)
assert inspect_wheel(wheel, repo_root=repo).ok