diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 0000000..a08d530 --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,35 @@ +name: "Setup Rust toolchains" +description: > + Installs the pinned stable and nightly Rust toolchains required by this + project. See .github/scripts/setup-nightly.sh for details on why nightly + is required. + +inputs: + rust-stable: + description: "Stable toolchain version" + default: "1.92.0" + rust-nightly: + description: "Nightly toolchain date (e.g. nightly-2026-04-27)" + default: "nightly-2026-04-27" + +runs: + using: "composite" + steps: + - name: Set up stable Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ inputs.rust-stable }} + target: wasm32-unknown-unknown + components: rustfmt,clippy + # Disable the built-in cargo cache -- callers manage their own. + cache: false + + - name: Cache nightly toolchain + uses: actions/cache@v4 + with: + path: ~/.rustup/toolchains/nightly-* + key: rustup-nightly-${{ runner.os }}-${{ inputs.rust-nightly }}-v1 + + - name: Set up nightly Rust toolchain + shell: bash + run: .github/scripts/setup-nightly.sh ${{ inputs.rust-nightly }} diff --git a/.github/scripts/setup-nightly.sh b/.github/scripts/setup-nightly.sh new file mode 100755 index 0000000..4007592 --- /dev/null +++ b/.github/scripts/setup-nightly.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Install the nightly Rust toolchain required to build this project. +# +# componentize-py's build.rs uses -Z build-std (nightly-only) to compile its +# wasm32-wasip1 runtime. It also invokes `rustup run nightly cargo build` by +# name, so both the pinned dated nightly and the bare 'nightly' channel must +# be installed; rustup rejects 'nightly' as a toolchain link target. +# +# Both installs are skipped if already present (e.g. restored from cache). +# +# Usage: setup-nightly.sh +# e.g. setup-nightly.sh nightly-2026-04-27 + +set -euo pipefail + +RUST_NIGHTLY="${1:?Usage: $0 }" + +if ! rustup toolchain list | grep -q "^${RUST_NIGHTLY}"; then + rustup toolchain install "$RUST_NIGHTLY" --component rust-src + rustup target add wasm32-wasip1 --toolchain "$RUST_NIGHTLY" +else + echo "Nightly toolchain $RUST_NIGHTLY already installed (cache hit)" +fi + +if ! rustup run nightly rustc --version &>/dev/null; then + rustup toolchain install nightly --component rust-src + rustup target add wasm32-wasip1 --toolchain nightly +else + echo "Bare nightly toolchain already installed (cache hit)" +fi diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 832a3f7..2a40d08 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -5,28 +5,25 @@ on: branches: - main pull_request: - workflow_dispatch: # Allows manual triggering from GitHub Actions UI + workflow_dispatch: # allow manual triggering + +# Cancel in-progress runs for the same branch/PR when a new push arrives. +# Omitted from release.yml — you never want to cancel an in-progress release. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: VICEROY_TAG: v0.16.4 + WASM_TOOLS_VERSION: "1.250.0" jobs: build: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - name: Set up Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: 1.92.0 - target: wasm32-unknown-unknown - components: rustfmt, clippy - # Disable the built-in cargo cache - we use our own below - cache: false - - name: Set up nightly Rust toolchain with rust-src - run: | - rustup toolchain install nightly --component rust-src - rustup target add wasm32-wasip1 --toolchain nightly + - uses: actions/checkout@v6 + - name: Set up Rust toolchains + uses: ./.github/actions/setup-rust - name: Set up Python uses: actions/setup-python@v5 with: @@ -34,19 +31,20 @@ jobs: - name: Install uv run: pip install uv - # Cache cargo binaries (viceroy, wasm-tools, etc.) + # Cache cargo binaries (viceroy, wasm-tools). + # wasm-tools is required by scripts/generate_patches, which runs as part + # of make lint to verify patches.py is up to date with the WIT sources. - name: Cache cargo binaries id: cache-cargo-bins uses: actions/cache@v4 with: - key: cargo-bins-${{ runner.os }}-${{ env.VICEROY_TAG }} + key: cargo-bins-${{ runner.os }}-${{ env.VICEROY_TAG }}-wasm-tools-${{ env.WASM_TOOLS_VERSION }} path: | ~/.cargo/bin/viceroy* ~/.cargo/bin/wasm-tools* - ~/.cargo/bin/wac* - - name: Install wasm-tools and wac + - name: Install wasm-tools if: steps.cache-cargo-bins.outputs.cache-hit != 'true' - run: cargo install wasm-tools wac-cli + run: cargo install wasm-tools --version ${{ env.WASM_TOOLS_VERSION }} --locked - name: Install viceroy if: steps.cache-cargo-bins.outputs.cache-hit != 'true' run: cargo install --git https://github.com/fastly/Viceroy.git --tag "$VICEROY_TAG" viceroy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3bcd404 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,238 @@ +name: Release + +# Triggers: +# - Push a version tag (e.g. git tag v0.1.0 && git push --tags) for a real release. +# - workflow_dispatch for ad-hoc test builds without creating a tag. +# Wheels are uploaded as GitHub Actions artifacts and no GitHub Release is created. +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + version: + description: "Version label for artifacts (e.g. v0.1.0-test)" + required: true + default: "v0.0.0-dev" + +# Minimal permissions by default; create-github-release job adds write where needed. +permissions: + contents: read + +env: + # Bump these in one place when upgrading toolchains or maturin. + # RUST_STABLE and RUST_NIGHTLY must also be kept in sync with the defaults + # in .github/actions/setup-rust/action.yml. + PYTHON_VERSION: "3.12" + RUST_STABLE: "1.92.0" + RUST_NIGHTLY: "nightly-2026-04-27" + MATURIN_VERSION: "v1.13.3" + +jobs: + # Verify that the versions in pyproject.toml and Cargo.toml match the tag + # (or the manually-supplied version label) before spending time on builds. + check-version: + name: "Check version consistency" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Verify versions match + run: | + python scripts/check_version_sync.py --tag "${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}" + + # Linux wheels are built inside manylinux_2_28 containers via maturin-action. + # We explicitly target manylinux 2_28; auto likely would too but pinning + # avoids unexpected wheel renames. See https://github.com/pypa/manylinux. + # + # The composite setup-rust action cannot run inside the maturin-action + # container, so toolchain setup is handled via before-script-linux instead. + build-linux: + name: "Linux ${{ matrix.target }}" + runs-on: ubuntu-24.04 + needs: [check-version] + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-unknown-linux-gnu + manylinux: "2_28" + - target: aarch64-unknown-linux-gnu + manylinux: "2_28" + + steps: + - uses: actions/checkout@v6 + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + maturin-version: ${{ env.MATURIN_VERSION }} + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux }} + rust-toolchain: ${{ env.RUST_STABLE }} + # abi3-py312 is set in [tool.maturin] features; no -i needed. + args: --release --locked --compatibility pypi + before-script-linux: | + .github/scripts/setup-nightly.sh ${{ env.RUST_NIGHTLY }} + rustup target add wasm32-unknown-unknown + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.target }} + path: target/wheels/*.whl + if-no-files-found: error + + # macOS wheels — native builds on GitHub-hosted runners. + # macos-14 is Apple Silicon (aarch64); macos-13 is Intel (x86_64). + build-macos: + name: "macOS ${{ matrix.target }}" + runs-on: ${{ matrix.runner }} + needs: [check-version] + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin + runner: macos-26-intel + # Maturin defaults to 10.12 for x86_64, but componentize-py's + # build.rs compiles a native CPython host binary that requires + # >=10.15 (sqlite3_create_window_function) with Xcode 26's SDK. + # Python 3.12 itself requires 10.13+, so 10.15 is a safe minimum. + macosx_deployment_target: "10.15" + - target: aarch64-apple-darwin + runner: macos-26 + macosx_deployment_target: "11.0" + + steps: + - uses: actions/checkout@v6 + + - name: Set up Rust toolchains + uses: ./.github/actions/setup-rust + with: + rust-stable: ${{ env.RUST_STABLE }} + rust-nightly: ${{ env.RUST_NIGHTLY }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + maturin-version: ${{ env.MATURIN_VERSION }} + target: ${{ matrix.target }} + # container: off is implied for non-Linux but set explicitly for clarity. + container: "off" + # abi3-py312 is set in [tool.maturin] features; no -i needed. + args: --release --locked + env: + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} + + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.target }} + path: target/wheels/*.whl + if-no-files-found: error + + # Windows is not supported at the currently pinned componentize-py commit: + # its build.rs calls a POSIX configure script unconditionally when building + # CPython for WASI. Newer releases of componentize-py have Windows support; + # re-enable this job when upgrading componentize-py. + + # Build a source distribution for PyPI alongside the binary wheels. + build-sdist: + name: "Build sdist" + runs-on: ubuntu-24.04 + needs: [check-version] + steps: + - uses: actions/checkout@v6 + + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + maturin-version: ${{ env.MATURIN_VERSION }} + command: sdist + args: --out dist + + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + if-no-files-found: error + + # Collect all release artifacts (wheels + sdist) into a single artifact, + # generate SHA256 checksums, and upload everything together. + collect-artifacts: + name: "Collect release artifacts" + needs: [build-linux, build-macos, build-sdist] + runs-on: ubuntu-24.04 + + steps: + - name: Download all wheel artifacts + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + path: dist/ + merge-multiple: true + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist/ + + - name: Generate SHA256 checksums + run: | + cd dist + sha256sum *.whl *.tar.gz > checksums.txt + cat checksums.txt + + - name: List artifacts + run: ls -lh dist/ + + - name: Upload combined artifact + uses: actions/upload-artifact@v4 + with: + name: release-artifacts + path: dist/ + if-no-files-found: error + + # Create a GitHub Release and attach all artifacts. + # Only runs on tag pushes, workflow_dispatch builds stop at collect-artifacts, + # presumed to be used for testing CI or related. + create-github-release: + name: "Create GitHub Release" + needs: [collect-artifacts] + runs-on: ubuntu-24.04 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + permissions: + contents: write # Required to create releases and upload assets. + + steps: + - uses: actions/checkout@v6 + + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + name: release-artifacts + path: dist/ + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + TAG="${{ github.ref_name }}" + gh release create "$TAG" dist/*.whl dist/*.tar.gz dist/checksums.txt \ + --title "$TAG" \ + --prerelease \ + --generate-notes \ + --notes-start-tag "$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo '')" diff --git a/.gitignore b/.gitignore index 768d3a3..e33a836 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Python/uv artifacts .venv/ *.egg-info -__pycache__ +**/__pycache__/ # Build artifacts /build/ @@ -9,4 +9,10 @@ bin/ # Rust target/ + +# Compiled Python extension (maturin develop output) *.so +*.pyd + +# macOS debug symbol bundles generated by maturin develop +*.dSYM/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b98f651..2c9777a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,9 +135,44 @@ Understanding the build process helps when debugging issues: ## Continuous Integration -Our CI builds wheels for multiple platforms: -- Linux: x86_64, aarch64 -- macOS: x86_64 (Intel), aarch64 (Apple Silicon) -- Windows: x86_64 +The CI workflow (`.github/workflows/python-ci.yml`) validates formatting, +linting, and tests on every push and pull request. The release workflow +(`.github/workflows/release.yml`) builds binary wheels for all supported +platforms (`Linux x86_64/aarch64`, `macOS x86_64/aarch64`) and attaches them +to a GitHub pre-release. + +## Performing a Release + +Releases are driven by a git tag. The release workflow builds binary wheels +and attaches them to a GitHub pre-release for validation before PyPI publishing. + +The version must be kept in sync across two files: +- `pyproject.toml` — `[project] version` +- `crates/fastly-compute-py/Cargo.toml` — `[package] version` + +`make lint` checks these are in sync. + +### Steps + +1. Bump `version` in both files above to the new version (e.g. `0.2.0`). + +2. Verify locally: + ```bash + make lint + ``` + +3. PR the changes and land into main. + +4. Push tag (make sure you are on the right sha first) + ``` + git tag v0.2.0 + git push origin v0.2.0 + ``` + +4. The release workflow runs automatically. Jobs: `check-version` (fails fast + on any mismatch) → parallel wheel + sdist builds → `collect-artifacts` → + `create-github-release`. + +5. (Pending) If the release is built successfully, it will make its way to PyPI + via trusted publishing. -The CI workflow (`.github/workflows/build-wheels.yml`) ensures all required tools are installed automatically. diff --git a/Cargo.lock b/Cargo.lock index 63ed84c..ad317ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -485,7 +485,7 @@ dependencies = [ [[package]] name = "componentize-py" version = "0.22.1" -source = "git+https://github.com/bytecodealliance/componentize-py#81d582a2333198cff13c0b56de35d72c25a652e7" +source = "git+https://github.com/bytecodealliance/componentize-py?rev=81d582a2333198cff13c0b56de35d72c25a652e7#81d582a2333198cff13c0b56de35d72c25a652e7" dependencies = [ "anyhow", "async-trait", @@ -1476,15 +1476,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "io-extras" version = "0.18.4" @@ -1750,15 +1741,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "miette" version = "7.6.0" @@ -1988,35 +1970,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d" +checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6" +checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089" +checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e" dependencies = [ "libc", "pyo3-build-config", @@ -2024,9 +2003,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02" +checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2036,9 +2015,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.27.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9" +checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb" dependencies = [ "heck", "proc-macro2", @@ -2720,7 +2699,7 @@ dependencies = [ [[package]] name = "test-generator" version = "0.1.0" -source = "git+https://github.com/bytecodealliance/componentize-py#81d582a2333198cff13c0b56de35d72c25a652e7" +source = "git+https://github.com/bytecodealliance/componentize-py?rev=81d582a2333198cff13c0b56de35d72c25a652e7#81d582a2333198cff13c0b56de35d72c25a652e7" dependencies = [ "anyhow", "getrandom 0.2.17", @@ -3051,12 +3030,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/crates/fastly-compute-py/Cargo.toml b/crates/fastly-compute-py/Cargo.toml index fdafb41..a7c824c 100644 --- a/crates/fastly-compute-py/Cargo.toml +++ b/crates/fastly-compute-py/Cargo.toml @@ -25,9 +25,9 @@ wit-parser = "0.244" wit-component = "0.244" wasm-metadata = { version = "0.245", default-features = false } tempfile = "3" -componentize-py = { git = "https://github.com/bytecodealliance/componentize-py" } +componentize-py = { git = "https://github.com/bytecodealliance/componentize-py", rev = "81d582a2333198cff13c0b56de35d72c25a652e7" } futures = { version = "0.3", default-features = false, features = ["executor"] } -pyo3 = { version = "0.27.2", optional = true } +pyo3 = { version = "0.28.3", features = ["abi3-py312"], optional = true } serde = { version = "1.0", features = ["derive"] } toml = "0.8" log = "0.4" diff --git a/crates/fastly-compute-py/build.rs b/crates/fastly-compute-py/build.rs index 557f6db..3b3b5ad 100644 --- a/crates/fastly-compute-py/build.rs +++ b/crates/fastly-compute-py/build.rs @@ -3,6 +3,8 @@ use std::env; use std::fs; use std::path::Path; use std::path::PathBuf; +use wit_component::WitPrinter; +use wit_parser::Resolve; fn main() -> Result<()> { println!("cargo:rerun-if-changed=../../wit"); @@ -44,21 +46,31 @@ fn main() -> Result<()> { } fn generate_merged_wit(source_wit_dir: &PathBuf, out_dir: impl AsRef) -> Result<()> { - // Generate merged WIT file using wasm-tools - let merged_wit_path = out_dir.as_ref().join("merged.wit"); - let output = std::process::Command::new("wasm-tools") - .arg("component") - .arg("wit") - .arg(source_wit_dir) - .output() - .context("Failed to run wasm-tools")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - anyhow::bail!("wasm-tools component wit failed: {}", stderr); - } + let mut resolve = Resolve { + all_features: true, + ..Default::default() + }; + let (main_pkg, _) = resolve.push_path(source_wit_dir).with_context(|| { + format!( + "failed to parse WIT directory: {}", + source_wit_dir.display() + ) + })?; + + // Collect all package IDs and remove the main pkg + let nested_pkgs: Vec<_> = resolve + .packages + .iter() + .map(|(id, _)| id) + .filter(|&id| id != main_pkg) + .collect(); + + let merged = WitPrinter::default() + .print(&resolve, main_pkg, &nested_pkgs) + .context("failed to print merged WIT")?; - fs::write(&merged_wit_path, &output.stdout)?; + let merged_wit_path = out_dir.as_ref().join("merged.wit"); + fs::write(&merged_wit_path, merged)?; Ok(()) } @@ -79,6 +91,12 @@ fn build_wasiless_wasm(root_dir: impl AsRef, out_dir: impl AsRef) -> .arg("--manifest-path") .arg(&manifest_path) .env("CARGO_TARGET_DIR", &target_dir) + // Clear host-specific rustflags injected by maturin or other build + // wrappers (e.g. -Csplit-debuginfo=packed). These are valid for the + // host target but are not supported by wasm32-unknown-unknown and + // will cause the cross-compilation to fail. + .env_remove("CARGO_ENCODED_RUSTFLAGS") + .env_remove("RUSTFLAGS") .status() .context("Failed to run cargo build for wasiless")?; @@ -86,22 +104,22 @@ fn build_wasiless_wasm(root_dir: impl AsRef, out_dir: impl AsRef) -> anyhow::bail!("Failed to build wasiless"); } - // Transform wasiless into a component using wasm-tools component new + // Wrap the core wasm module into a wasm component using wit-component's + // ComponentEncoder, replacing the previous `wasm-tools component new` call. let input_wasm = target_dir.join("wasm32-unknown-unknown/release/wasiless.wasm"); let output_wasm = out_dir.as_ref().join("wasiless.wasm"); - let status = std::process::Command::new("wasm-tools") - .arg("component") - .arg("new") - .arg(&input_wasm) - .arg("-o") - .arg(&output_wasm) - .status() - .context("Failed to run wasm-tools component new")?; + let module_bytes = fs::read(&input_wasm) + .with_context(|| format!("failed to read {}", input_wasm.display()))?; - if !status.success() { - anyhow::bail!("Failed to componentize wasiless"); - } + let component_bytes = wit_component::ComponentEncoder::default() + .module(&module_bytes) + .context("failed to set module on ComponentEncoder")? + .encode() + .context("failed to encode wasm component")?; + + fs::write(&output_wasm, component_bytes) + .with_context(|| format!("failed to write {}", output_wasm.display()))?; Ok(()) } diff --git a/pyproject.toml b/pyproject.toml index 8c57099..fb9ca74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,9 @@ dev = [ module-name = "fastly_compute._fastly_compute_py" manifest-path = "crates/fastly-compute-py/Cargo.toml" exclude = ["fastly_compute/tests/**/*"] +# Emit a stable-ABI (abi3) wheel tagged cp312-abi3-* so a single wheel +# works on CPython 3.12 and all future versions without rebuilding. +features = ["pyo3/abi3-py312"] [project.scripts] fastly-compute-py = "fastly_compute.fastly_compute_py:main" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d617235 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,19 @@ +# Rust toolchain configuration for this project. +# +# Pins the stable channel used for the main build. wasm32-unknown-unknown is +# required by build.rs (wasiless cross-compilation). +# +# A nightly toolchain is also required at build time: componentize-py's +# build.rs uses `-Z build-std` to compile its wasm32-wasip1 runtime with +# position-independent code, which is a nightly-only/unstable Cargo flag. +# +# The nightly version is pinned in the CI workflows (RUST_NIGHTLY env var). +# For local development, install it with: +# +# rustup toolchain install nightly-YYYY-MM-DD --component rust-src --target wasm32-wasip1 +# +# See .github/workflows/python-ci.yml for the current pinned date. +[toolchain] +channel = "1.92.0" +targets = ["wasm32-unknown-unknown"] +components = ["rustfmt", "clippy"] diff --git a/scripts/check_version_sync.py b/scripts/check_version_sync.py index bc018d3..ca79cdb 100755 --- a/scripts/check_version_sync.py +++ b/scripts/check_version_sync.py @@ -5,14 +5,26 @@ in crates/fastly-compute-py/Cargo.toml. Both files must be kept in sync to avoid confusion when releasing new versions. +When --tag is passed (or the VERSION environment variable is set), the +in-tree versions are also validated against the expected release version. +The tag is expected to be in the form "vX.Y.Z"; the leading "v" is stripped +before comparison. + Exit codes: - 0: Versions are synchronized + 0: Versions are synchronized (and match the tag, if provided) 1: Version mismatch detected Usage: + # Check file consistency only (used by `make lint`): python scripts/check_version_sync.py + + # Also validate against a release tag (used by the release workflow): + python scripts/check_version_sync.py --tag v1.2.3 + VERSION=v1.2.3 python scripts/check_version_sync.py """ +import argparse +import os import sys import tomllib from pathlib import Path @@ -36,22 +48,64 @@ def get_cargo_version() -> str: return data["package"]["version"] +def parse_tag_version(tag: str) -> str: + """Strip a leading 'v' from a tag to get a bare version string.""" + return tag.lstrip("v") + + def main() -> int: """Check version consistency and exit with appropriate code.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--tag", + metavar="TAG", + default=os.environ.get("VERSION"), + help="Expected release tag (e.g. v1.2.3). Also read from $VERSION.", + ) + args = parser.parse_args() + pyproject_version = get_pyproject_version() cargo_version = get_cargo_version() + ok = True + if pyproject_version == cargo_version: - print(f"✓ Version numbers are synchronized: {pyproject_version}") - return 0 + print(f"✓ pyproject.toml and Cargo.toml are synchronized: {pyproject_version}") else: - print("✗ Version mismatch detected:", file=sys.stderr) - print(f" pyproject.toml: {pyproject_version}", file=sys.stderr) + print( + "✗ Version mismatch between pyproject.toml and Cargo.toml:", file=sys.stderr + ) + print( + f" pyproject.toml: {pyproject_version}", file=sys.stderr + ) print( f" crates/fastly-compute-py/Cargo.toml: {cargo_version}", file=sys.stderr ) - print("\nPlease update both files to use the same version.", file=sys.stderr) - return 1 + print("\nUpdate both files to use the same version.", file=sys.stderr) + ok = False + + if args.tag: + expected = parse_tag_version(args.tag) + if pyproject_version == expected and cargo_version == expected: + print(f"✓ In-tree versions match tag '{args.tag}': {expected}") + else: + print(f"✗ In-tree versions do not match tag '{args.tag}':", file=sys.stderr) + if pyproject_version != expected: + print( + f" pyproject.toml: {pyproject_version} (expected {expected})", + file=sys.stderr, + ) + if cargo_version != expected: + print( + f" crates/fastly-compute-py/Cargo.toml: {cargo_version} (expected {expected})", + file=sys.stderr, + ) + print( + "\nBump both files to match the tag before releasing.", file=sys.stderr + ) + ok = False + + return 0 if ok else 1 if __name__ == "__main__":