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
74 changes: 58 additions & 16 deletions .github/workflows/build_linux_arm64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,32 @@ jobs:
python -m pip install --upgrade pip setuptools wheel
python -m pip install -e .

- name: Cache native Blender ARM64 dependencies
uses: actions/cache@v4
- name: Resolve ARM64 dependency bundle version
id: deps_version
run: |
python - <<'PY'
import os
from pathlib import Path
from buildbpy.arm64_deps import BundleSpec

version = os.environ["TAG"].removeprefix("v")
spec = BundleSpec.for_blender_version(version)
with Path(os.environ["GITHUB_ENV"]).open("a") as environment:
print(f"BLENDER_VERSION={version}", file=environment)
print(f"DEPS_VERSION={spec.blender_version}", file=environment)
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as outputs:
print(
f"is_base_version={str(version == spec.blender_version).lower()}",
file=outputs,
)
PY

- name: Restore native Blender ARM64 dependency cache
id: deps_cache
uses: actions/cache/restore@v4
with:
path: |
~/.buildbpy/build_linux/deps_arm64
~/.buildbpy/blender/lib/linux_arm64
key: blender-${{ env.TAG }}-${{ env.PYTHON_VERSION }}-linux-arm64-deps-v1
path: ~/.buildbpy/build_linux/deps_arm64
key: blender-${{ env.DEPS_VERSION }}-${{ env.PYTHON_VERSION }}-linux-arm64-deps-v1

- name: Build and install bpy
env:
Expand Down Expand Up @@ -130,9 +149,8 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${TAG#v}"
RELEASE_TAG="deps-blender-${VERSION}-linux-arm64-v1"
ARCHIVE="blender-${VERSION}-linux-arm64-ubuntu24.04-gcc13-v1.tar.zst"
RELEASE_TAG="deps-blender-${DEPS_VERSION}-linux-arm64-v1"
ARCHIVE="blender-${DEPS_VERSION}-linux-arm64-ubuntu24.04-gcc13-v1.tar.zst"
if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
ASSETS="$(gh release view "${RELEASE_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
Expand All @@ -151,7 +169,7 @@ jobs:
fi

- name: Package verified ARM64 dependency bundle
if: steps.deps_release.outputs.exists != 'true'
if: steps.deps_release.outputs.exists != 'true' && steps.deps_version.outputs.is_base_version == 'true'
run: |
python - <<'PY'
import os
Expand All @@ -162,14 +180,21 @@ jobs:

root = Path.home() / ".buildbpy"
version = os.environ["TAG"].removeprefix("v")
deps_version = os.environ["DEPS_VERSION"]
commit = subprocess.check_output(
["git", "-C", str(root / "blender"), "rev-parse", "HEAD"],
[
"git",
"-C",
str(root / "blender"),
"rev-parse",
f"v{deps_version}^{{commit}}",
],
text=True,
).strip()
artifacts = create_bundle(
root / "blender/lib/linux_arm64",
root / "deps-dist",
BundleSpec(version),
BundleSpec.for_blender_version(version),
blender_commit=commit,
python_version=os.environ["PYTHON_VERSION"],
)
Expand All @@ -179,16 +204,15 @@ jobs:
PY

- name: Publish verified ARM64 dependency bundle
if: steps.deps_release.outputs.exists != 'true'
if: steps.deps_release.outputs.exists != 'true' && steps.deps_version.outputs.is_base_version == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${TAG#v}"
RELEASE_TAG="deps-blender-${VERSION}-linux-arm64-v1"
RELEASE_TAG="deps-blender-${DEPS_VERSION}-linux-arm64-v1"
gh release create "${RELEASE_TAG}" \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--title "Blender ${VERSION} Linux ARM64 dependencies v1" \
--title "Blender ${DEPS_VERSION} Linux ARM64 dependencies v1" \
--notes "Native Linux ARM64 dependency bundle validated by the bpy wheel build and import smoke test." \
--draft
gh release upload "${RELEASE_TAG}" \
Expand All @@ -198,6 +222,24 @@ jobs:
--repo "${GITHUB_REPOSITORY}" \
--draft=false

- name: Check native dependency cache contents
id: deps_cache_path
if: steps.deps_version.outputs.is_base_version == 'true'
run: |
if [[ -d ~/.buildbpy/build_linux/deps_arm64 ]] && \
find ~/.buildbpy/build_linux/deps_arm64 -mindepth 1 -print -quit | grep -q .; then
echo "exists=true" >> "${GITHUB_OUTPUT}"
else
echo "exists=false" >> "${GITHUB_OUTPUT}"
fi

- name: Save native Blender ARM64 dependency cache
if: steps.deps_version.outputs.is_base_version == 'true' && steps.deps_cache_path.outputs.exists == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: ~/.buildbpy/build_linux/deps_arm64
key: blender-${{ env.DEPS_VERSION }}-${{ env.PYTHON_VERSION }}-linux-arm64-deps-v1

- name: Upload ARM64 wheel artifact
uses: actions/upload-artifact@v4
with:
Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,13 @@ Linux ARM64 that it publishes for Linux x86-64. On ARM64, `buildbpy` therefore:

Bundles use immutable release tags such as
`deps-blender-5.2.0-linux-arm64-v1`; published assets are never clobbered in
place. A successful ARM64 CI build packages and publishes the exact dependency
tree only after the generated wheel installs and its exact requested Blender
version passes `import bpy`.
place. Every Blender 5.2 patch release (`5.2.x`) reuses that `5.2.0` dependency
bundle, cache key, and release identity. The manifest remains pinned to the
Blender `v5.2.0` tag commit while the generated wheel is validated against the
exact requested Blender patch version. A successful ARM64 CI build packages and
publishes a dependency tree only when the selected immutable bundle does not
already exist and only after the generated wheel installs and passes `import
bpy`.
Comment on lines +79 to +82

The bundle, manifest, and checksum are trusted through GitHub HTTPS and the
repository's release-write access. SHA-256 detects corruption and mixed assets;
Expand Down
14 changes: 14 additions & 0 deletions src/buildbpy/arm64_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
from pathlib import Path, PurePosixPath
import platform
import re
import subprocess
import tarfile
import tempfile
Expand All @@ -26,6 +27,19 @@ class BundleSpec:
compiler: str = "gcc13"
architecture: str = "arm64"

@classmethod
def for_blender_version(cls, blender_version: str) -> "BundleSpec":
"""Select the immutable dependency bundle for a Blender release."""
if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", blender_version) is None:
raise ValueError(
f"Blender dependency bundle requires a semantic version: {blender_version!r}"
)
parts = blender_version.split(".")
dependency_version = (
"5.2.0" if len(parts) == 3 and parts[:2] == ["5", "2"] else blender_version
)
return cls(dependency_version)

@property
def release_tag(self) -> str:
return (
Expand Down
22 changes: 20 additions & 2 deletions src/buildbpy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,13 +690,25 @@ def _install_arm64_bundle(
if extract_root.exists():
shutil.rmtree(extract_root)
logger.info("Verifying Linux ARM64 dependency bundle %s", artifacts.archive)
if spec.blender_version == self.version_strategy.minor_version:
expected_blender_commit = self.version_strategy.commit_hash
else:
expected_blender_commit = subprocess.check_output(
[
"git",
"rev-parse",
f"v{spec.blender_version}^{{commit}}",
],
cwd=self.blender_repo_dir,
text=True,
).strip()
verify_and_extract_bundle(
artifacts.archive,
artifacts.manifest,
artifacts.checksum,
extract_root,
spec,
expected_blender_commit=self.version_strategy.commit_hash,
expected_blender_commit=expected_blender_commit,
expected_python_series=f"{sys.version_info.major}.{sys.version_info.minor}",
)
target = self.lib_dir / "linux_arm64"
Expand All @@ -718,7 +730,13 @@ def setup_build_environment(self):
"./build_files/utils/make_update.py --no-libraries",
self.blender_repo_dir,
)
spec = BundleSpec(self.version_strategy.minor_version)
spec = BundleSpec.for_blender_version(self.version_strategy.minor_version)
if spec.blender_version != self.version_strategy.minor_version:
logger.info(
"Blender %s uses Linux ARM64 dependency bundle %s",
self.version_strategy.minor_version,
spec.blender_version,
)
archive_value = os.environ.get("BUILDBPY_ARM64_DEPS_ARCHIVE")
if archive_value:
archive = Path(archive_value).expanduser().resolve()
Expand Down
113 changes: 112 additions & 1 deletion tests/test_arm64_deps_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,33 @@ def test_bundle_spec_has_stable_release_names():
assert spec.release_tag == "deps-blender-5.2.0-linux-arm64-v1"
assert spec.archive_name == "blender-5.2.0-linux-arm64-ubuntu24.04-gcc13-v1.tar.zst"
assert spec.manifest_name == f"{spec.archive_name}.manifest.json"


@pytest.mark.parametrize("blender_version", ["5.2.0", "5.2.1", "5.2.99"])
def test_all_blender_5_2_patch_releases_select_5_2_0_dependency_bundle(
blender_version: str,
):
spec = BundleSpec.for_blender_version(blender_version)

assert spec.blender_version == "5.2.0"
assert spec.release_tag == "deps-blender-5.2.0-linux-arm64-v1"
assert spec.checksum_name == f"{spec.archive_name}.sha256"


def test_other_blender_series_keep_their_exact_dependency_version():
spec = BundleSpec.for_blender_version("5.3.0")

assert spec.blender_version == "5.3.0"


@pytest.mark.parametrize("blender_version", ["5.2", "v5.2.1", "5.2.1\nBAD=value"])
def test_dependency_bundle_selector_rejects_non_semantic_versions(
blender_version: str,
):
with pytest.raises(ValueError, match="semantic version"):
BundleSpec.for_blender_version(blender_version)


def test_open_zstd_tar_reports_missing_decoder_and_closes_stderr(tmp_path: Path):
stderr_file = MagicMock()
with (
Expand Down Expand Up @@ -506,6 +530,72 @@ def fake_download(client, repository, token, bundle_spec, output_dir):
assert (blender_repo / "lib/linux_arm64/lib/libexample.a").read_bytes() == b"release"


@requires_bundle_tools
def test_linux_arm64_5_2_patch_reuses_5_2_0_release_bundle(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
blender_repo = tmp_path / "blender"
blender_repo.mkdir()
subprocess.run(["git", "init"], cwd=blender_repo, check=True, capture_output=True)
subprocess.run(
["git", "config", "user.name", "Buildbpy Test"], cwd=blender_repo, check=True
)
subprocess.run(
["git", "config", "user.email", "buildbpy-test@example.invalid"],
cwd=blender_repo,
check=True,
)
(blender_repo / "version.txt").write_text("5.2.0")
subprocess.run(["git", "add", "version.txt"], cwd=blender_repo, check=True)
subprocess.run(["git", "commit", "-m", "5.2.0"], cwd=blender_repo, check=True, capture_output=True)
subprocess.run(["git", "tag", "v5.2.0"], cwd=blender_repo, check=True)
bundle_commit = subprocess.check_output(
["git", "rev-parse", "v5.2.0^{commit}"], cwd=blender_repo, text=True
).strip()
(blender_repo / "version.txt").write_text("5.2.1")
subprocess.run(["git", "add", "version.txt"], cwd=blender_repo, check=True)
subprocess.run(["git", "commit", "-m", "5.2.1"], cwd=blender_repo, check=True, capture_output=True)
checkout_commit = subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=blender_repo, text=True
).strip()

source = tmp_path / "source" / "linux_arm64"
(source / "lib").mkdir(parents=True)
(source / "lib" / "libexample.a").write_bytes(b"5.2-series-release")
bundle_spec = BundleSpec(blender_version="5.2.0", revision=1)
artifacts = create_bundle(
source,
tmp_path / "release",
bundle_spec,
blender_commit=bundle_commit,
python_version=f"{sys.version_info.major}.{sys.version_info.minor}.0",
)
version = ReleaseVersionCycleStrategy("5.2", "5.2.1", "release", checkout_commit)
with patch("platform.machine", return_value="aarch64"):
strategy = LinuxOSStrategy(version, tmp_path, blender_repo, httpx.Client())
commands = []
requested_specs = []
monkeypatch.delenv("BUILDBPY_ARM64_DEPS_ARCHIVE", raising=False)
monkeypatch.delenv("BUILDBPY_ARM64_DEPS_USE_RELEASE", raising=False)
monkeypatch.setenv("GITHUB_REPOSITORY", "michaelgold/buildbpy")
monkeypatch.setenv("GITHUB_TOKEN", "token")
monkeypatch.setattr(strategy, "run_command", lambda command, cwd: commands.append(command))

def fake_download(client, repository, token, spec, output_dir):
requested_specs.append(spec)
return artifacts

monkeypatch.setattr("buildbpy.main.download_release_bundle", fake_download)

strategy.setup_build_environment()

assert requested_specs == [bundle_spec]
assert commands == ["./build_files/utils/make_update.py --no-libraries"]
assert (
blender_repo / "lib/linux_arm64/lib/libexample.a"
).read_bytes() == b"5.2-series-release"


@requires_bundle_tools
def test_linux_arm64_falls_back_when_release_bundle_is_invalid(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
Expand Down Expand Up @@ -613,7 +703,28 @@ def test_arm64_workflow_validates_exact_tag_and_publishes_immutable_release():
assert "id: deps_release" in workflow
assert workflow.count("steps.deps_release.outputs.exists != 'true'") == 2
assert "already exists and is immutable" not in workflow
assert "key: blender-${{ env.TAG }}-${{ env.PYTHON_VERSION }}-linux-arm64-deps-v1" in workflow
assert "DEPS_VERSION={spec.blender_version}" in workflow
assert "BLENDER_VERSION={version}" in workflow
assert "is_base_version={str(version == spec.blender_version).lower()}" in workflow
assert (
"key: blender-${{ env.DEPS_VERSION }}-${{ env.PYTHON_VERSION }}-linux-arm64-deps-v1"
in workflow
)
assert "uses: actions/cache/restore@v4" in workflow
assert "uses: actions/cache/save@v4" in workflow
assert "uses: actions/cache@v4" not in workflow
assert "~/.buildbpy/blender/lib/linux_arm64" not in workflow
assert "steps.deps_cache_path.outputs.exists == 'true'" in workflow
assert "steps.deps_cache.outputs.cache-hit != 'true'" in workflow
save_cache_offset = workflow.index("- name: Save native Blender ARM64 dependency cache")
save_cache_section = workflow[save_cache_offset:]
assert "steps.deps_version.outputs.is_base_version == 'true'" in save_cache_section
assert 'RELEASE_TAG="deps-blender-${DEPS_VERSION}-linux-arm64-v1"' in workflow
assert 'ARCHIVE="blender-${DEPS_VERSION}-linux-arm64-ubuntu24.04-gcc13-v1.tar.zst"' in workflow
assert "BundleSpec.for_blender_version(version)" in workflow
assert workflow.count("steps.deps_version.outputs.is_base_version == 'true'") == 4
assert 'f"v{deps_version}^{{commit}}"' in workflow
assert '["git", "-C", str(root / "blender"), "rev-parse", "HEAD"]' not in workflow


def test_build_all_includes_linux_arm64_in_aggregate_success_barrier():
Expand Down
Loading