From 31f3cdbdb0378935d1d04ac8940ba9deb73057f9 Mon Sep 17 00:00:00 2001 From: Talha Date: Mon, 3 Aug 2026 17:24:52 +0500 Subject: [PATCH 1/2] ci(testpypi): ignore local attestation sidecars --- tests/test_published_distribution.py | 30 +++++++++++++++++++++++++- utils/verify_published_distribution.py | 20 +++++++++++------ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/tests/test_published_distribution.py b/tests/test_published_distribution.py index bc29738..48dffd4 100644 --- a/tests/test_published_distribution.py +++ b/tests/test_published_distribution.py @@ -1,9 +1,37 @@ +import tempfile import unittest +from pathlib import Path -from utils.verify_published_distribution import distribution_files +from utils.verify_published_distribution import ( + distribution_files, + local_distribution_files, +) class PublishedDistributionTests(unittest.TestCase): + def test_publisher_generated_local_attestations_are_ignored(self): + with tempfile.TemporaryDirectory() as temporary: + dist = Path(temporary) + wheel = dist / "vidxp-0.4.0.dev20-py3-none-any.whl" + sdist = dist / "vidxp-0.4.0.dev20.tar.gz" + wheel.write_bytes(b"wheel") + sdist.write_bytes(b"sdist") + (dist / f"{wheel.name}.publish.attestation").write_bytes(b"attestation") + (dist / f"{sdist.name}.publish.attestation").write_bytes(b"attestation") + + self.assertEqual( + set(local_distribution_files(dist)), + {wheel.name, sdist.name}, + ) + + def test_unknown_local_files_are_still_compared(self): + with tempfile.TemporaryDirectory() as temporary: + dist = Path(temporary) + unexpected = dist / "unexpected.zip" + unexpected.write_bytes(b"unexpected") + + self.assertEqual(set(local_distribution_files(dist)), {unexpected.name}) + def test_registry_attestations_are_not_distribution_archives(self): payload = { "urls": [ diff --git a/utils/verify_published_distribution.py b/utils/verify_published_distribution.py index 125e2e8..83662d9 100644 --- a/utils/verify_published_distribution.py +++ b/utils/verify_published_distribution.py @@ -24,11 +24,23 @@ def sha256(path: Path) -> str: return digest.hexdigest() +def is_distribution_file(filename: str) -> bool: + return not filename.endswith(_PUBLISH_ATTESTATION_SUFFIX) + + +def local_distribution_files(directory: Path) -> dict[str, str]: + return { + path.name: sha256(path) + for path in directory.iterdir() + if path.is_file() and is_distribution_file(path.name) + } + + def distribution_files(payload: dict[str, object]) -> dict[str, str]: return { file["filename"]: file["digests"]["sha256"] for file in payload.get("urls", []) - if not file["filename"].endswith(_PUBLISH_ATTESTATION_SUFFIX) + if is_distribution_file(file["filename"]) } @@ -40,11 +52,7 @@ def main() -> int: parser.add_argument("--dist", type=Path, default=Path("dist")) args = parser.parse_args() - local = { - path.name: sha256(path) - for path in args.dist.iterdir() - if path.is_file() - } + local = local_distribution_files(args.dist) if not local: raise SystemExit(f"No distribution files found in {args.dist}") From a0cac3983ecfb718c87212b65ecfe0c3568e5e50 Mon Sep 17 00:00:00 2001 From: Talha Date: Mon, 3 Aug 2026 17:53:03 +0500 Subject: [PATCH 2/2] build(deps): make uv lock authoritative --- .github/dependabot.yml | 2 +- .github/workflows/desktop.yml | 16 - desktop/runtime-constraints.txt | 501 -------------------------------- desktop/runtime-manifest.json | 1 - desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/build.rs | 65 ++++- desktop/src-tauri/src/lib.rs | 6 +- docs/desktop.md | 9 +- 8 files changed, 74 insertions(+), 527 deletions(-) delete mode 100644 desktop/runtime-constraints.txt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a414a0b..3d78ad2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,6 @@ version: 2 updates: - - package-ecosystem: pip + - package-ecosystem: uv directory: "/" schedule: interval: cron diff --git a/.github/workflows/desktop.yml b/.github/workflows/desktop.yml index 554c343..8b48c9b 100644 --- a/.github/workflows/desktop.yml +++ b/.github/workflows/desktop.yml @@ -101,22 +101,6 @@ jobs: npm run notices:check working-directory: desktop - - name: Verify the desktop runtime constraints - shell: bash - run: | - generated="$RUNNER_TEMP/runtime-constraints.txt" - uv export \ - --frozen \ - --extra local-worker \ - --extra frontend \ - --no-dev \ - --no-emit-project \ - --no-hashes \ - --format requirements-txt \ - --output-file "$generated" - git diff --no-index --ignore-matching-lines='^#' -- \ - desktop/runtime-constraints.txt "$generated" - - name: Restore the pinned uv sidecar id: uv-sidecar-cache uses: actions/cache@v6 diff --git a/desktop/runtime-constraints.txt b/desktop/runtime-constraints.txt deleted file mode 100644 index cebb21f..0000000 --- a/desktop/runtime-constraints.txt +++ /dev/null @@ -1,501 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv export --frozen --extra local-worker --extra frontend --no-dev --no-emit-project --no-hashes --format requirements-txt --output-file desktop/runtime-constraints.txt -aiohappyeyeballs==2.7.1 - # via aiohttp -aiohttp==3.14.3 - # via kubernetes -aiosignal==1.4.0 - # via aiohttp -altair==6.2.2 - # via streamlit -annotated-doc==0.0.5 - # via typer -annotated-types==0.8.0 - # via pydantic -anyio==4.14.2 - # via - # httpx - # httpx2 - # openai - # starlette - # streamlit - # watchfiles -attrs==26.1.0 - # via - # aiohttp - # jsonschema - # referencing -av==18.0.0 - # via faster-whisper -bcrypt==5.0.0 - # via chromadb -blinker==1.9.0 - # via streamlit -build==1.5.0 - # via chromadb -certifi==2026.7.22 - # via - # httpcore - # httpx - # kubernetes - # requests -charset-normalizer==3.4.9 - # via requests -chromadb==1.5.9 - # via vidxp -click==8.4.2 - # via - # huggingface-hub - # streamlit - # uvicorn -colorama==0.4.6 ; os_name == 'nt' or sys_platform == 'win32' - # via - # build - # click - # tqdm - # typer -ctranslate2==4.8.1 - # via faster-whisper -dbos==2.28.0 - # via vidxp -distro==1.9.0 - # via openai -durationpy==0.10 - # via kubernetes -faster-whisper==1.2.1 - # via vidxp -filelock==3.32.0 - # via - # huggingface-hub - # torch - # vidxp -flatbuffers==25.12.19 - # via onnxruntime -frozenlist==1.8.0 - # via - # aiohttp - # aiosignal -fsspec==2026.7.0 - # via - # huggingface-hub - # torch -genai-prices==0.0.72 - # via pydantic-ai-slim -gitdb==4.0.12 - # via gitpython -gitpython==3.1.57 - # via streamlit -googleapis-common-protos==1.75.0 - # via opentelemetry-exporter-otlp-proto-grpc -greenlet==3.5.4 - # via sqlalchemy -griffelib==2.1.0 - # via pydantic-ai-slim -grpcio==1.83.0 - # via - # chromadb - # opentelemetry-exporter-otlp-proto-grpc -h11==0.16.0 - # via - # httpcore - # httpcore2 - # uvicorn -hf-xet==1.5.2 ; platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64' - # via huggingface-hub -httpcore==1.0.9 - # via httpx -httpcore2==2.9.1 - # via httpx2 -httptools==0.8.0 - # via - # streamlit - # uvicorn -httpx==0.28.1 - # via - # chromadb - # huggingface-hub - # openai - # pydantic-ai-slim - # pydantic-graph -httpx2==2.9.1 - # via genai-prices -huggingface-hub==1.25.1 - # via - # faster-whisper - # sentence-transformers - # tokenizers - # transformers - # vidxp -idna==3.18 - # via - # anyio - # httpx - # httpx2 - # requests - # yarl -importlib-resources==7.1.0 - # via chromadb -itsdangerous==2.2.0 - # via streamlit -jinja2==3.1.6 - # via - # altair - # pydeck - # torch -jiter==0.16.0 - # via openai -joblib==1.5.3 - # via scikit-learn -jsonschema==4.26.0 - # via - # altair - # chromadb -jsonschema-specifications==2025.9.1 - # via jsonschema -kubernetes==36.0.3 - # via chromadb -logfire-api==4.39.0 - # via pydantic-graph -markdown-it-py==4.2.0 - # via rich -markupsafe==3.0.3 - # via jinja2 -mdurl==0.1.2 - # via markdown-it-py -mmh3==5.2.1 - # via chromadb -mpmath==1.3.0 - # via sympy -multidict==6.7.1 - # via - # aiohttp - # yarl -narwhals==2.24.0 - # via - # altair - # scikit-learn -networkx==3.6.1 - # via torch -numpy==2.4.6 ; python_full_version < '3.12' - # via - # chromadb - # ctranslate2 - # onnxruntime - # opencv-python-headless - # pandas - # pydeck - # scikit-learn - # scipy - # sentence-transformers - # streamlit - # transformers - # vidxp -numpy==2.5.1 ; python_full_version >= '3.12' - # via - # chromadb - # ctranslate2 - # onnxruntime - # opencv-python-headless - # pandas - # pydeck - # scikit-learn - # scipy - # sentence-transformers - # streamlit - # transformers - # vidxp -oauthlib==3.3.1 - # via requests-oauthlib -onnxruntime==1.28.0 - # via - # chromadb - # faster-whisper -openai==2.50.0 - # via pydantic-ai-slim -opencv-python-headless==5.0.0.93 - # via vidxp -opentelemetry-api==1.44.0 - # via - # chromadb - # opentelemetry-exporter-otlp-proto-grpc - # opentelemetry-sdk - # opentelemetry-semantic-conventions - # pydantic-ai-slim -opentelemetry-exporter-otlp-proto-common==1.44.0 - # via opentelemetry-exporter-otlp-proto-grpc -opentelemetry-exporter-otlp-proto-grpc==1.44.0 - # via chromadb -opentelemetry-proto==1.44.0 - # via - # opentelemetry-exporter-otlp-proto-common - # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-sdk==1.44.0 - # via - # chromadb - # opentelemetry-exporter-otlp-proto-grpc -opentelemetry-semantic-conventions==0.65b0 - # via opentelemetry-sdk -orjson==3.11.9 - # via chromadb -overrides==7.7.0 - # via chromadb -packaging==26.2 - # via - # altair - # build - # huggingface-hub - # onnxruntime - # pooch - # streamlit - # transformers - # vidxp -pandas==3.0.5 - # via streamlit -pillow==12.3.0 - # via - # streamlit - # vidxp -platformdirs==4.11.0 - # via - # pooch - # vidxp -pooch==1.9.0 - # via vidxp -propcache==0.5.2 - # via - # aiohttp - # yarl -protobuf==7.35.1 - # via - # googleapis-common-protos - # onnxruntime - # opentelemetry-proto - # streamlit -psutil==7.2.2 - # via vidxp -psycopg==3.3.4 - # via dbos -psycopg-binary==3.3.4 ; implementation_name != 'pypy' - # via psycopg -pyarrow==24.0.0 - # via streamlit -pybase64==1.4.3 - # via chromadb -pydantic==2.13.4 - # via - # chromadb - # genai-prices - # openai - # pydantic-ai-slim - # pydantic-graph - # pydantic-settings - # vidxp -pydantic-ai-slim==2.20.0 - # via vidxp -pydantic-core==2.46.4 - # via pydantic -pydantic-graph==2.20.0 - # via pydantic-ai-slim -pydantic-settings==2.14.2 - # via - # chromadb - # vidxp -pydeck==0.9.3 - # via streamlit -pygments==2.20.0 - # via rich -pyjwt==2.13.0 - # via vidxp -pypika==0.51.1 - # via chromadb -pyproject-hooks==1.2.0 - # via build -python-dateutil==2.9.0.post0 - # via - # dbos - # kubernetes - # pandas -python-dotenv==1.2.2 - # via - # pydantic-settings - # uvicorn -python-multipart==0.0.32 - # via streamlit -pyyaml==6.0.3 - # via - # chromadb - # ctranslate2 - # dbos - # huggingface-hub - # kubernetes - # transformers - # uvicorn -referencing==0.37.0 - # via - # jsonschema - # jsonschema-specifications -regex==2026.7.19 - # via - # tiktoken - # transformers -requests==2.34.2 - # via - # kubernetes - # pooch - # requests-oauthlib - # streamlit - # tiktoken -requests-oauthlib==2.0.0 - # via kubernetes -rich==15.0.0 - # via - # chromadb - # typer - # vidxp -rpds-py==2026.6.3 - # via - # jsonschema - # referencing -safetensors==0.8.0 - # via transformers -scikit-learn==1.9.0 - # via sentence-transformers -scipy==1.17.1 ; python_full_version < '3.12' - # via - # scikit-learn - # sentence-transformers -scipy==1.18.0 ; python_full_version >= '3.12' - # via - # scikit-learn - # sentence-transformers -sentence-transformers==5.6.1 - # via vidxp -setuptools==83.0.0 - # via - # ctranslate2 - # torch -shellingham==1.5.4 - # via typer -six==1.17.0 - # via - # kubernetes - # python-dateutil -smmap==5.0.3 - # via gitdb -sniffio==1.3.1 - # via openai -sqlalchemy==2.0.51 - # via - # dbos - # vidxp -starlette==1.3.1 - # via streamlit -streamlit==1.60.0 - # via vidxp -sympy==1.14.0 - # via torch -tenacity==9.1.4 - # via - # chromadb - # streamlit -threadpoolctl==3.6.0 - # via scikit-learn -tiktoken==0.13.0 - # via pydantic-ai-slim -tokenizers==0.22.2 - # via - # chromadb - # faster-whisper - # transformers -toml==0.10.2 - # via streamlit -torch==2.13.0 ; sys_platform != 'linux' and sys_platform != 'win32' - # via - # sentence-transformers - # vidxp -torch==2.13.0+cpu ; sys_platform == 'linux' or sys_platform == 'win32' - # via - # sentence-transformers - # vidxp -tqdm==4.70.0 - # via - # chromadb - # faster-whisper - # huggingface-hub - # openai - # sentence-transformers - # transformers -transformers==5.14.1 - # via - # sentence-transformers - # vidxp -truststore==0.10.4 - # via - # httpcore2 - # httpx2 -typer==0.27.0 - # via - # chromadb - # transformers - # typer-slim - # vidxp -typer-slim==0.24.0 - # via dbos -typing-extensions==4.16.0 - # via - # aiohttp - # aiosignal - # altair - # anyio - # chromadb - # grpcio - # httpx2 - # huggingface-hub - # openai - # opentelemetry-api - # opentelemetry-exporter-otlp-proto-grpc - # opentelemetry-sdk - # opentelemetry-semantic-conventions - # psycopg - # pydantic - # pydantic-core - # referencing - # sentence-transformers - # sqlalchemy - # starlette - # streamlit - # torch - # typing-inspection -typing-inspection==0.4.2 - # via - # pydantic - # pydantic-ai-slim - # pydantic-graph - # pydantic-settings -tzdata==2026.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' - # via - # pandas - # psycopg -urllib3==2.7.0 - # via - # kubernetes - # requests -uvicorn==0.51.0 - # via - # chromadb - # streamlit -uvloop==0.22.1 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' - # via uvicorn -watchdog==6.0.0 ; sys_platform != 'darwin' - # via streamlit -watchfiles==1.2.0 - # via uvicorn -websocket-client==1.9.0 - # via kubernetes -websockets==16.1.1 - # via - # dbos - # streamlit - # uvicorn -yarl==1.24.5 - # via aiohttp diff --git a/desktop/runtime-manifest.json b/desktop/runtime-manifest.json index f52d292..dfef92f 100644 --- a/desktop/runtime-manifest.json +++ b/desktop/runtime-manifest.json @@ -4,7 +4,6 @@ "package_name": "vidxp", "package_version": "0.4.0-b", "dependency_index": "https://pypi.org/simple", - "dependency_constraints_sha256": "4225babc5298044494772e13c59a3346c706468069f0490025c988244e0b5bbb", "python_version": "3.14.6", "uv_version": "0.12.0", "surfaces": { diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index c1f957d..ea20a56 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] [build-dependencies] serde_json = "1.0.151" +sha2 = "0.11.0" tauri-build = { version = "2.6.3", features = [] } [dependencies] diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index ae6d059..f5c6f33 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -1,5 +1,9 @@ fn main() { - let manifest: serde_json::Value = + use sha2::{Digest, Sha256}; + use std::fmt::Write; + use std::path::{Path, PathBuf}; + + let mut manifest: serde_json::Value = serde_json::from_slice(include_bytes!("../runtime-manifest.json")) .expect("desktop/runtime-manifest.json must be valid JSON"); let expected = manifest["uv_version"] @@ -11,7 +15,7 @@ fn main() { } else { "" }; - let sidecar = std::path::PathBuf::from("binaries").join(format!("uv-{target}{suffix}")); + let sidecar = PathBuf::from("binaries").join(format!("uv-{target}{suffix}")); let output = std::process::Command::new(&sidecar) .arg("--version") .output() @@ -29,5 +33,62 @@ fn main() { actual.trim(), expected ); + + let constraints = + PathBuf::from(std::env::var_os("OUT_DIR").expect("Cargo must provide OUT_DIR")) + .join("runtime-constraints.txt"); + let project = Path::new("../.."); + let export = std::process::Command::new(&sidecar) + .args([ + "export", + "--frozen", + "--extra", + "local-worker", + "--extra", + "frontend", + "--no-dev", + "--no-emit-project", + "--no-hashes", + "--format", + "requirements-txt", + "--project", + ]) + .arg(project) + .output() + .unwrap_or_else(|error| { + panic!( + "{} could not export the desktop runtime constraints: {error}", + sidecar.display() + ) + }); + assert!( + export.status.success(), + "{} failed to export the desktop runtime constraints: {}", + sidecar.display(), + String::from_utf8_lossy(&export.stderr).trim() + ); + let normalized = String::from_utf8(export.stdout) + .expect("the desktop runtime constraints must be UTF-8") + .replace("\r\n", "\n"); + std::fs::write(&constraints, normalized.as_bytes()) + .expect("Cargo must be able to normalize the desktop runtime constraints"); + let digest = Sha256::digest(normalized.as_bytes()).iter().fold( + String::with_capacity(64), + |mut encoded, byte| { + write!(&mut encoded, "{byte:02x}").expect("writing to a string cannot fail"); + encoded + }, + ); + manifest["dependency_constraints_sha256"] = serde_json::Value::String(digest); + let embedded_manifest = constraints.with_file_name("runtime-manifest.json"); + let mut serialized = serde_json::to_vec_pretty(&manifest) + .expect("desktop/runtime-manifest.json must be serializable"); + serialized.push(b'\n'); + std::fs::write(&embedded_manifest, serialized) + .expect("Cargo must be able to write the embedded runtime manifest"); + println!("cargo:rerun-if-changed=../../pyproject.toml"); + println!("cargo:rerun-if-changed=../../uv.lock"); + println!("cargo:rerun-if-changed=../runtime-manifest.json"); + tauri_build::build() } diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index cede931..f098d92 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -39,8 +39,10 @@ use lifecycle::{ }; use media_setup::{SystemInstallPlan, display_command, required_encoder_missing}; -const RUNTIME_MANIFEST_BYTES: &[u8] = include_bytes!("../../runtime-manifest.json"); -const RUNTIME_CONSTRAINTS_BYTES: &[u8] = include_bytes!("../../runtime-constraints.txt"); +const RUNTIME_MANIFEST_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/runtime-manifest.json")); +const RUNTIME_CONSTRAINTS_BYTES: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/runtime-constraints.txt")); const MODEL_CACHE_CATALOG_BYTES: &[u8] = include_bytes!("../../model-cache-catalog.json"); const PRODUCT_DATA_DIRECTORY_NAME: &str = "VidXP"; const MAX_SETUP_OUTPUT_BYTES: usize = 4 * 1024 * 1024; diff --git a/docs/desktop.md b/docs/desktop.md index 335a1e7..db81d7d 100644 --- a/docs/desktop.md +++ b/docs/desktop.md @@ -127,10 +127,11 @@ Windows and Linux resolve CPU-only PyTorch wheels using uv's `--torch-backend cpu`; macOS uses native PyPI wheels. The custom PyTorch index is therefore a resolver input and is not embedded as a package URL, avoiding the prior package-publication failure mode. Every selected profile is also -constrained by `desktop/runtime-constraints.txt`, exported from the repository -lock for the complete local-worker and frontend dependency set. Capability -selection controls which packages are installed; the constraints prevent those -packages from drifting independently after the desktop binary is published. +constrained by a requirements snapshot exported from `uv.lock` and embedded +during the desktop build for the complete local-worker and frontend dependency +set. Capability selection controls which packages are installed; the constraints +prevent those packages from drifting independently after the desktop binary is +published. FFmpeg and ffprobe are host prerequisites. When WinGet is available, Windows can show and run the supported FFmpeg install command after consent. When