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
123 changes: 57 additions & 66 deletions scripts/release/package_desktop_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import hashlib
import os
import platform
import zipfile
import shutil
from pathlib import Path


Expand Down Expand Up @@ -61,94 +61,85 @@ def resolved_artifact_target() -> tuple[str, str]:
return normalized_platform(), normalized_architecture()


def artifact_identity() -> dict[str, str]:
def artifact_identity(filename: str) -> dict[str, str]:
"""Build the archive and manifest names for the current artifact target."""
git_sha = os.environ.get("GITHUB_SHA", "local")[:12]
target_platform, target_arch = resolved_artifact_target()
suffix = f"bandscope-{target_platform}-{target_arch}-{git_sha}"
ext = Path(filename).suffix
return {
"platform": target_platform,
"arch": target_arch,
"archive_name": f"{suffix}.zip",
"manifest_name": f"{suffix}.manifest.txt",
"archive_name": f"{suffix}{ext}",
"manifest_name": f"{suffix}{ext}.manifest.txt",
}


def expected_binary_path(repo_root: Path) -> Path:
"""Return the expected desktop binary path for the selected target triple."""
def find_installer_packages(repo_root: Path) -> list[Path]:
"""Find built Tauri installers (DMG, EXE, MSI)."""
target_triple = os.environ.get("BANDSCOPE_TARGET_TRIPLE")
if target_triple and "windows" in target_triple:
system = "windows"
elif target_triple and "apple-darwin" in target_triple:
system = "macos"
else:
system = normalized_platform()
binary_name = "bandscope-desktop.exe" if system == "windows" else "bandscope-desktop"
target_root = repo_root / "apps" / "desktop" / "src-tauri" / "target"
if target_triple:
target_root = target_root / target_triple
return target_root / "release" / binary_name

bundle_dir = target_root / "release" / "bundle"
installers = []

if bundle_dir.exists():
# macOS DMG
installers.extend(bundle_dir.glob("dmg/*.dmg"))
# Windows EXE/MSI
installers.extend(bundle_dir.glob("nsis/*.exe"))
installers.extend(bundle_dir.glob("msi/*.msi"))

return installers


def main() -> int:
"""Package the desktop binary, frontend assets, and metadata into a zip archive."""
"""Find the built installer packages, rename them, and calculate checksums."""
repo_root = Path(__file__).resolve().parents[2]
binary_path = expected_binary_path(repo_root)
frontend_dist = repo_root / "apps" / "desktop" / "dist"
output_dir = repo_root / "artifacts"
output_dir.mkdir(parents=True, exist_ok=True)

if not binary_path.exists():
raise FileNotFoundError(f"Missing built binary: {binary_path}")
if not frontend_dist.exists():
raise FileNotFoundError(f"Missing frontend dist directory: {frontend_dist}")

metadata_paths = [
repo_root / "services" / "analysis-engine" / "uv.lock",
repo_root / "package-lock.json",
repo_root / "apps" / "desktop" / "src-tauri" / "Cargo.lock",
repo_root / "supply-chain" / "supplemental-component-inventory.json",
]
missing_metadata = [str(path) for path in metadata_paths if not path.exists()]
if missing_metadata:
missing_list = ", ".join(missing_metadata)
raise FileNotFoundError(f"Missing release metadata files: {missing_list}")

identity = artifact_identity()
archive_name = identity["archive_name"]
archive_path = output_dir / archive_name

with zipfile.ZipFile(archive_path, "w", compression=zipfile.ZIP_DEFLATED) as archive:
archive.write(binary_path, arcname=f"bin/{binary_path.name}")
for path in frontend_dist.rglob("*"):
if path.is_file():
archive.write(
path,
arcname=str(Path("frontend") / path.relative_to(frontend_dist)),
)
for extra_path in metadata_paths:
archive.write(extra_path, arcname=str(Path("metadata") / extra_path.name))

checksum_path = output_dir / f"{archive_name}.sha256"
checksum_path.write_text(f"{sha256_file(archive_path)} {archive_name}\n", encoding="utf-8")

manifest_path = output_dir / identity["manifest_name"]
manifest_path.write_text(
"\n".join(
[
f"platform={identity['platform']}",
f"arch={identity['arch']}",
f"target_triple={os.environ.get('BANDSCOPE_TARGET_TRIPLE', 'native')}",
f"binary={binary_path.name}",
f"archive={archive_name}",
f"checksum={checksum_path.name}",
]
installers = find_installer_packages(repo_root)
if not installers:
raise FileNotFoundError("Could not find any built installers (DMG/EXE) in target/release/bundle/")

# For safety, ensure we only pick one installer per run, or handle multiple
# Typically we might have both EXE and MSI for Windows.
for installer_path in installers:
identity = artifact_identity(installer_path.name)
archive_name = identity["archive_name"]

# If there are multiple (e.g. EXE and MSI), add original extension to avoid overwrite
if len(installers) > 1:
ext = installer_path.suffix
archive_name = archive_name.replace(ext, f"{ext}")

archive_path = output_dir / archive_name
shutil.copy2(installer_path, archive_path)

checksum_path = output_dir / f"{archive_name}.sha256"
checksum_path.write_text(f"{sha256_file(archive_path)} {archive_name}\n", encoding="utf-8")

manifest_path = output_dir / identity["manifest_name"]
manifest_path.write_text(
"\n".join(
[
f"platform={identity['platform']}",
f"arch={identity['arch']}",
f"target_triple={os.environ.get('BANDSCOPE_TARGET_TRIPLE', 'native')}",
f"original_file={installer_path.name}",
f"archive={archive_name}",
f"checksum={checksum_path.name}",
]
)
+ "\n",
encoding="utf-8",
)
+ "\n",
encoding="utf-8",
)

print(str(archive_path.relative_to(repo_root)))
print(f"Packaged {installer_path.name} to artifacts/{archive_name}")

return 0


Expand Down
87 changes: 50 additions & 37 deletions services/analysis-engine/tests/test_release_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ def test_release_packaging_includes_architecture_in_artifact_identity(
monkeypatch.setenv("BANDSCOPE_ARTIFACT_OS", "windows")
monkeypatch.setenv("BANDSCOPE_ARTIFACT_ARCH", "arm64")

artifact = packaging.artifact_identity()
artifact = packaging.artifact_identity("installer.dmg")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

assert artifact == {
"platform": "windows",
"arch": "arm64",
"archive_name": "bandscope-windows-arm64-abcdef123456.zip",
"manifest_name": "bandscope-windows-arm64-abcdef123456.manifest.txt",
"archive_name": "bandscope-windows-arm64-abcdef123456.dmg",
"manifest_name": "bandscope-windows-arm64-abcdef123456.dmg.manifest.txt",
}


Expand All @@ -46,44 +46,46 @@ def test_release_packaging_derives_artifact_identity_from_target_triple(
monkeypatch.setattr(packaging.platform, "system", lambda: "Darwin")
monkeypatch.setattr(packaging.platform, "machine", lambda: "arm64")

artifact = packaging.artifact_identity()
artifact = packaging.artifact_identity("installer.exe")

assert artifact == {
"platform": "windows",
"arch": "amd64",
"archive_name": "bandscope-windows-amd64-fedcba987654.zip",
"manifest_name": "bandscope-windows-amd64-fedcba987654.manifest.txt",
"archive_name": "bandscope-windows-amd64-fedcba987654.exe",
"manifest_name": "bandscope-windows-amd64-fedcba987654.exe.manifest.txt",
}


def test_expected_binary_path_uses_target_triple_when_provided(
def test_find_installer_packages_returns_dmg(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Ensure target triples redirect packaging to the expected Tauri output path."""
"""Ensure find_installer_packages finds dmg files."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_target"
)

monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "aarch64-apple-darwin")

binary_path = packaging.expected_binary_path(tmp_path)

assert binary_path == (
dmg_path = (
tmp_path
/ "apps"
/ "desktop"
/ "src-tauri"
/ "target"
/ "aarch64-apple-darwin"
/ "release"
/ "bandscope-desktop"
/ "bundle"
/ "dmg"
/ "Test.dmg"
)
dmg_path.parent.mkdir(parents=True)
dmg_path.write_bytes(b"dmg")

installers = packaging.find_installer_packages(tmp_path)
assert installers == [dmg_path]

Comment thread
coderabbitai[bot] marked this conversation as resolved.
def test_expected_binary_path_derives_windows_extension_from_target_triple(
monkeypatch, tmp_path: Path
) -> None:
"""Ensure Windows target triples select the .exe packaging path on non-Windows hosts."""

def test_find_installer_packages_returns_exe_and_msi(monkeypatch, tmp_path: Path) -> None:
"""Ensure find_installer_packages finds exe and msi files."""
packaging = load_module(
"scripts/release/package_desktop_artifact.py", "package_desktop_artifact_windows_target"
)
Expand All @@ -92,18 +94,37 @@ def test_expected_binary_path_derives_windows_extension_from_target_triple(
monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "x86_64-pc-windows-msvc")
monkeypatch.setattr(packaging.platform, "system", lambda: "Darwin")

binary_path = packaging.expected_binary_path(tmp_path)

assert binary_path == (
exe_path = (
tmp_path
/ "apps"
/ "desktop"
/ "src-tauri"
/ "target"
/ "x86_64-pc-windows-msvc"
/ "release"
/ "bandscope-desktop.exe"
/ "bundle"
/ "nsis"
/ "Test.exe"
)
msi_path = (
tmp_path
/ "apps"
/ "desktop"
/ "src-tauri"
/ "target"
/ "x86_64-pc-windows-msvc"
/ "release"
/ "bundle"
/ "msi"
/ "Test.msi"
)
exe_path.parent.mkdir(parents=True)
exe_path.write_bytes(b"exe")
msi_path.parent.mkdir(parents=True)
msi_path.write_bytes(b"msi")

installers = packaging.find_installer_packages(tmp_path)
assert set(installers) == {exe_path, msi_path}


def test_release_packaging_maps_darwin_to_macos(monkeypatch: pytest.MonkeyPatch) -> None:
Expand All @@ -129,29 +150,21 @@ def test_release_packaging_main_writes_arch_specific_manifest(
script_path = repo_root / "scripts" / "release" / "package_desktop_artifact.py"
script_path.parent.mkdir(parents=True)
script_path.write_text("# placeholder", encoding="utf-8")
binary_path = (

dmg_path = (
repo_root
/ "apps"
/ "desktop"
/ "src-tauri"
/ "target"
/ "aarch64-apple-darwin"
/ "release"
/ "bandscope-desktop"
/ "bundle"
/ "dmg"
/ "App.dmg"
)
binary_path.parent.mkdir(parents=True)
binary_path.write_bytes(b"binary")
frontend_file = repo_root / "apps" / "desktop" / "dist" / "index.html"
frontend_file.parent.mkdir(parents=True)
frontend_file.write_text("<html></html>", encoding="utf-8")
for metadata_path in [
repo_root / "services" / "analysis-engine" / "uv.lock",
repo_root / "package-lock.json",
repo_root / "apps" / "desktop" / "src-tauri" / "Cargo.lock",
repo_root / "supply-chain" / "supplemental-component-inventory.json",
]:
metadata_path.parent.mkdir(parents=True, exist_ok=True)
metadata_path.write_text("metadata", encoding="utf-8")
dmg_path.parent.mkdir(parents=True)
dmg_path.write_bytes(b"dmg")

monkeypatch.setattr(packaging, "__file__", str(script_path))
monkeypatch.setenv("GITHUB_SHA", "1234567890abcdef")
Expand All @@ -160,7 +173,7 @@ def test_release_packaging_main_writes_arch_specific_manifest(
monkeypatch.setenv("BANDSCOPE_TARGET_TRIPLE", "aarch64-apple-darwin")

assert packaging.main() == 0
manifest_path = repo_root / "artifacts" / "bandscope-macos-arm64-1234567890ab.manifest.txt"
manifest_path = repo_root / "artifacts" / "bandscope-macos-arm64-1234567890ab.dmg.manifest.txt"

assert manifest_path.exists()
assert "platform=macos" in manifest_path.read_text(encoding="utf-8")
Expand Down
Loading