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
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ jobs:
enable-cache: true

- name: Install locked sidecar dependencies
run: uv sync --locked --extra build
run: python scripts/sync_frozen_dependencies.py

- name: Build platform-native sidecar
run: uv run python scripts/build_frozen_engine.py
Expand Down Expand Up @@ -222,7 +222,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check uv==0.11.29
uv sync --locked --extra build
python scripts/sync_frozen_dependencies.py
uv run python scripts/build_frozen_engine.py
uv run python scripts/verify_build_artifact.py sidecar
mkdir -p src-tauri/binaries
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ jobs:
--require-hashes --disable-pip --progress-spinner off
--requirement "${{ runner.temp }}/openadapt-desktop-requirements.txt"

python-macos-intel:
name: Python locked dependencies (macOS Intel)
runs-on: macos-15-intel
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
- name: Set up Python 3.12
run: uv python install 3.12
- name: Export the exact Intel macOS dependency set
run: >-
uv export --quiet --locked --all-extras --no-emit-project
--output-file "${{ runner.temp }}/openadapt-desktop-requirements.txt"
- name: Audit Intel macOS Python dependencies
run: >-
uvx --from pip-audit==2.10.1 pip-audit
--require-hashes --disable-pip --progress-spinner off
--requirement "${{ runner.temp }}/openadapt-desktop-requirements.txt"

npm:
name: npm locked dependencies
runs-on: ubuntu-22.04
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/native-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check uv==0.11.29
uv sync --locked --extra build
python scripts/sync_frozen_dependencies.py
uv run python scripts/build_frozen_engine.py
uv run python scripts/verify_build_artifact.py sidecar
uv run python scripts/smoke_test_frozen_flow.py
Expand Down Expand Up @@ -286,7 +286,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check uv==0.11.29
uv sync --locked --extra build
python scripts/sync_frozen_dependencies.py
uv run python scripts/build_frozen_engine.py
uv run python scripts/verify_build_artifact.py sidecar
uv run python scripts/smoke_test_frozen_flow.py
Expand Down Expand Up @@ -426,7 +426,7 @@ jobs:
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check uv==0.11.29
uv sync --locked --extra build
python scripts/sync_frozen_dependencies.py
uv run python scripts/build_frozen_engine.py
uv run python scripts/verify_build_artifact.py sidecar
uv run python scripts/smoke_test_frozen_flow.py
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ dependencies = [
# PEP 440 comparison is required before Desktop enables a Flow capability.
# Hand-parsing would accept pre-release builds such as 1.26.0rc1 as 1.26.0.
"packaging>=24",
# Qualification evidence is signed locally. Cryptography 49 no longer
# publishes a macOS Intel wheel, so keep that platform on the latest
# universal2 release while using the current runtime elsewhere.
"cryptography==48.0.0; sys_platform == 'darwin' and platform_machine == 'x86_64'",
"cryptography>=48,<50; sys_platform != 'darwin' or platform_machine != 'x86_64'",
# Qualification evidence is signed locally. Version 50 fixes
# PYSEC-2026-3552 and the other known advisories on the former Intel macOS
# pin. PyPI has no Intel wheel, so frozen builds compile it from the locked
# source archive against the static Homebrew OpenSSL libraries.
"cryptography>=50,<51",
"loguru",
"psutil",
# Pure-Python, dependency-free, BSD-licensed QR encoder. The mobile
Expand Down
6 changes: 3 additions & 3 deletions scripts/build_frozen_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@

RAPIDOCR_NOTICE_DIR = ROOT / "third_party" / "rapidocr"
LINUX_RUNNER_RUNTIME_EXCLUDE = r"libgcc_s\.so(\..*)?"
MACOS_X86_CRYPTOGRAPHY_VERSION = "48.0.0"
MACOS_X86_CRYPTOGRAPHY_VERSION = "50.0.0"
MACOS_OPENSSL_DYLIB = re.compile(r"(?:^|/)(?:libssl|libcrypto)(?:\.[0-9]+)*\.dylib$")


Expand Down Expand Up @@ -123,8 +123,8 @@ def verify_macos_intel_cryptography_boundary(
installed = distribution_lookup("cryptography")
if installed.version != MACOS_X86_CRYPTOGRAPHY_VERSION:
raise RuntimeError(
"macOS Intel frozen builds require the universal2 cryptography "
f"{MACOS_X86_CRYPTOGRAPHY_VERSION} wheel, found {installed.version}"
"macOS Intel frozen builds require the source-built cryptography "
f"{MACOS_X86_CRYPTOGRAPHY_VERSION} runtime, found {installed.version}"
)
extension = Path(installed.locate_file("cryptography/hazmat/bindings/_rust.abi3.so"))
if not extension.is_file():
Expand Down
1 change: 1 addition & 0 deletions scripts/classify_build_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"scripts/package_ffmpeg_runtime.py",
"scripts/smoke_test_frozen_flow.py",
"scripts/smoke_test_native_installer.py",
"scripts/sync_frozen_dependencies.py",
"scripts/verify_build_artifact.py",
}

Expand Down
72 changes: 72 additions & 0 deletions scripts/sync_frozen_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Install the locked frozen-runtime dependencies for the current platform."""

from __future__ import annotations

import os
import platform
import subprocess
from collections.abc import Mapping
from pathlib import Path


def frozen_dependency_environment(
*,
system: str | None = None,
machine: str | None = None,
source: Mapping[str, str] | None = None,
run=subprocess.run,
) -> dict[str, str]:
"""Return the build environment, including the safe Intel macOS boundary."""

environment = dict(os.environ if source is None else source)
system = system or platform.system()
machine = machine or platform.machine()
if system != "Darwin" or machine != "x86_64":
return environment

openssl_dir = environment.get("OPENSSL_DIR", "").strip()
if not openssl_dir:
result = run(
["brew", "--prefix", "openssl@3"],
capture_output=True,
text=True,
timeout=30,
check=False,
)
if result.returncode != 0 or not result.stdout.strip():
raise RuntimeError(
"Intel macOS requires Homebrew openssl@3 to build the locked "
f"cryptography source archive: {result.stderr[-1000:]}"
)
openssl_dir = result.stdout.strip()

prefix = Path(openssl_dir)
missing = [
str(prefix / "lib" / name)
for name in ("libssl.a", "libcrypto.a")
if not (prefix / "lib" / name).is_file()
]
if missing:
raise RuntimeError(
"Intel macOS cryptography requires static OpenSSL libraries: " + ", ".join(missing)
)

environment["OPENSSL_DIR"] = str(prefix)
environment["OPENSSL_STATIC"] = "1"
return environment


def main() -> int:
environment = frozen_dependency_environment()
command = ["uv", "sync", "--locked", "--extra", "build"]
if platform.system() == "Darwin" and platform.machine() == "x86_64":
command.extend(
("--reinstall-package", "cryptography", "--no-binary-package", "cryptography")
)
subprocess.run(command, check=True, env=environment)
return 0


if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions tests/test_build_change_classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def test_protected_main_uses_cheap_jobs_for_application_only_changes(paths: list
"src-tauri/src/main.rs",
"src-tauri/Cargo.lock",
"scripts/build_frozen_engine.py",
"scripts/sync_frozen_dependencies.py",
"scripts/verify_build_artifact.py",
".github/workflows/build.yml",
"engine/__main__.py",
Expand Down
3 changes: 3 additions & 0 deletions tests/test_native_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def test_native_workflows_are_pinned_and_preserve_beta_boundary() -> None:
for bundles in ("dmg", "msi,nsis", "deb,appimage"):
assert f"bundles: {bundles}" in build
assert "smoke_test_native_installer.py" in build
assert build.count("scripts/sync_frozen_dependencies.py") == 2
assert release.count("scripts/sync_frozen_dependencies.py") == 3
assert "native_release.py checksums" in build
assert 'tags:\n - "desktop-v*"' in release
assert "environment: native-release" in release
Expand Down Expand Up @@ -153,6 +155,7 @@ def test_security_workflows_cover_all_languages_and_pin_every_dependency() -> No
for language in ("python", "javascript-typescript", "rust"):
assert f'"{language}"' in (workflow_dir / "codeql.yml").read_text()
dependency_audit = (workflow_dir / "dependency-review.yml").read_text()
assert "macos-15-intel" in dependency_audit
assert "uv export --quiet --locked --all-extras --no-emit-project" in dependency_audit
assert "pip-audit==2.10.1" in dependency_audit
assert "npm audit --audit-level=high --package-lock-only" in dependency_audit
Expand Down
82 changes: 82 additions & 0 deletions tests/test_sync_frozen_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

import tomllib
from pathlib import Path
from types import SimpleNamespace

import pytest
from packaging.requirements import Requirement

from scripts import sync_frozen_dependencies as sync
from scripts.sync_frozen_dependencies import frozen_dependency_environment

ROOT = Path(__file__).resolve().parents[1]


def test_every_platform_requires_fixed_cryptography_50() -> None:
project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8"))["project"]
requirements = [
Requirement(value) for value in project["dependencies"] if value.startswith("cryptography")
]

assert len(requirements) == 1
assert str(requirements[0].specifier) == "<51,>=50"
assert requirements[0].marker is None


def test_intel_macos_build_uses_static_homebrew_openssl(tmp_path: Path) -> None:
lib = tmp_path / "lib"
lib.mkdir()
(lib / "libssl.a").touch()
(lib / "libcrypto.a").touch()

environment = frozen_dependency_environment(
system="Darwin",
machine="x86_64",
source={"OPENSSL_DIR": str(tmp_path)},
)

assert environment["OPENSSL_DIR"] == str(tmp_path)
assert environment["OPENSSL_STATIC"] == "1"


def test_intel_macos_build_refuses_missing_static_openssl(tmp_path: Path) -> None:
with pytest.raises(RuntimeError, match="requires static OpenSSL libraries"):
frozen_dependency_environment(
system="Darwin",
machine="x86_64",
source={"OPENSSL_DIR": str(tmp_path)},
)


def test_intel_macos_sync_forces_a_fresh_source_build(tmp_path: Path, monkeypatch) -> None:
lib = tmp_path / "lib"
lib.mkdir()
(lib / "libssl.a").touch()
(lib / "libcrypto.a").touch()
monkeypatch.setenv("OPENSSL_DIR", str(tmp_path))
monkeypatch.setattr(sync.platform, "system", lambda: "Darwin")
monkeypatch.setattr(sync.platform, "machine", lambda: "x86_64")
calls = []

def run(command, **kwargs):
calls.append((command, kwargs))
return SimpleNamespace(returncode=0, stdout="", stderr="")

monkeypatch.setattr(sync.subprocess, "run", run)

assert sync.main() == 0
command, kwargs = calls[-1]
assert command == [
"uv",
"sync",
"--locked",
"--extra",
"build",
"--reinstall-package",
"cryptography",
"--no-binary-package",
"cryptography",
]
assert kwargs["check"] is True
assert kwargs["env"]["OPENSSL_STATIC"] == "1"
Loading
Loading