From a07a8d54f9d1e1985bfe9387c7aa515b4e8137d2 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 7 Sep 2026 05:50:16 -0700 Subject: [PATCH 1/5] fix(release): publish native binaries for all supported platforms --- .github/workflows/ci.yml | 6 + .github/workflows/prebuilt.yml | 178 +++++++++++++++++++++++++++++ .github/workflows/release.yml | 40 ++++++- .gitignore | 2 + Cargo.toml | 13 +++ README.md | 36 ++++++ mise.toml | 1 + scripts/release/release.py | 191 ++++++++++++++++++++++++++++++++ scripts/release/targets.json | 52 +++++++++ scripts/release/test_release.py | 88 +++++++++++++++ 10 files changed, 604 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/prebuilt.yml create mode 100644 scripts/release/release.py create mode 100644 scripts/release/targets.json create mode 100644 scripts/release/test_release.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 235616c..81b5b70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,3 +110,9 @@ jobs: buf generate cargo fmt --all git diff --exit-code -- src + + prebuilt: + permissions: + contents: read + name: Verify prebuilt distribution + uses: ./.github/workflows/prebuilt.yml diff --git a/.github/workflows/prebuilt.yml b/.github/workflows/prebuilt.yml new file mode 100644 index 0000000..bd347cc --- /dev/null +++ b/.github/workflows/prebuilt.yml @@ -0,0 +1,178 @@ +name: Prebuilt binaries + +on: + workflow_call: + inputs: + ref: + type: string + default: "" + verify-install: + type: boolean + default: false + +permissions: + contents: read + +defaults: + run: + shell: bash + +env: + CARGO_TERM_COLOR: always + MANIFEST: Cargo.toml + CARGO_PACKAGE: sqlc-gen-sqlx + +jobs: + metadata: + permissions: + contents: read + runs-on: ubuntu-22.04 + timeout-minutes: 10 + outputs: + matrix: ${{ steps.matrix.outputs.matrix }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.sha }} + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install_args: python + cache: false + - name: Test release packaging + run: python -m unittest discover -s scripts/release -p 'test_*.py' -v + - id: matrix + run: | + python - <<'PYTHON' + import json + import os + from pathlib import Path + matrix = json.loads(Path("scripts/release/targets.json").read_text()) + with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: + output.write("matrix=" + json.dumps(matrix) + "\n") + PYTHON + + build: + permissions: + contents: read + if: ${{ !inputs.verify-install }} + needs: metadata + name: Build ${{ matrix.target }} + runs-on: ${{ matrix.build_runner }} + timeout-minutes: 90 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.sha }} + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install_args: python rust + cache: false + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + key: release-${{ matrix.target }} + - name: Install Linux build dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config musl-tools libdbus-1-dev + - name: Configure musl linker + if: contains(matrix.target, 'musl') + env: + TARGET: ${{ matrix.target }} + run: | + target_env="${TARGET^^}" + target_env="${target_env//-/_}" + echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" >> "$GITHUB_ENV" + echo "CC_${TARGET//-/_}=musl-gcc" >> "$GITHUB_ENV" + + - name: Link the Windows runtime statically + if: contains(matrix.target, 'windows') + env: + TARGET: ${{ matrix.target }} + run: | + target_env="${TARGET^^}" + target_env="${target_env//-/_}" + echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" + - name: Read package metadata + id: package + env: + TARGET: ${{ matrix.target }} + run: python scripts/release/release.py metadata --manifest "$MANIFEST" --target "$TARGET" + - name: Build executable + env: + TARGET: ${{ matrix.target }} + VERSION: ${{ steps.package.outputs.version }} + BUILD_TOOL: ${{ matrix.build_tool }} + run: | + rustup target add "$TARGET" + cargo build --locked --release -p "$CARGO_PACKAGE" --target "$TARGET" + - name: Package executable and checksum + env: + TARGET: ${{ matrix.target }} + BINARY: ${{ steps.package.outputs.binary }} + run: | + python scripts/release/release.py package --manifest "$MANIFEST" --target "$TARGET" \ + --binary "target/$TARGET/release/$BINARY" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: binary-${{ matrix.target }} + path: dist/* + if-no-files-found: error + + smoke: + permissions: + contents: read + if: ${{ !inputs.verify-install }} + needs: [metadata, build] + name: Run archive ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.sha }} + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install_args: python + cache: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: binary-${{ matrix.target }} + path: dist + - name: Verify checksum and run extracted executable + env: + TARGET: ${{ matrix.target }} + run: python scripts/release/release.py verify-archive --manifest "$MANIFEST" --target "$TARGET" + + install: + permissions: + contents: read + if: inputs.verify-install + needs: metadata + name: Install without compiling ${{ matrix.target }} + runs-on: ${{ matrix.runner }} + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.ref || github.sha }} + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install_args: python cargo-binstall + cache: false + - name: Install published binaries through binstall and mise + env: + TARGET: ${{ matrix.target }} + GH_TOKEN: ${{ github.token }} + run: python scripts/release/release.py verify-install --manifest "$MANIFEST" --target "$TARGET" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 81cf04a..6f2ae46 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,9 @@ concurrency: group: release-${{ github.workflow }}-${{ inputs.tag || github.ref }} cancel-in-progress: false +permissions: + contents: read + jobs: verify: runs-on: ubuntu-latest @@ -71,13 +74,23 @@ jobs: env: DATABASE_URL: postgres://sqlc:sqlc@localhost:5432/sqlc_test + prebuilt: + permissions: + contents: read + needs: verify + uses: ./.github/workflows/prebuilt.yml + with: + ref: ${{ inputs.tag || github.sha }} + publish-release: environment: release - needs: verify + needs: [verify, prebuilt] runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write + id-token: write + attestations: write env: RELEASE_TAG: ${{ inputs.tag || github.ref_name }} @@ -88,6 +101,13 @@ jobs: persist-credentials: false ref: ${{ inputs.tag || github.ref }} + - name: Download native archives + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: binary-* + path: dist + merge-multiple: true + - name: Install Rust stable uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable with: @@ -106,6 +126,11 @@ jobs: shasum -a 256 dist/sqlc-gen-sqlx.wasm > dist/sqlc-gen-sqlx.wasm.sha256 fi + - name: Attest native archives + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-path: dist/* + - name: Ensure GitHub release exists env: GH_TOKEN: ${{ github.token }} @@ -131,8 +156,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | gh release upload "${RELEASE_TAG}" \ - dist/sqlc-gen-sqlx.wasm \ - dist/sqlc-gen-sqlx.wasm.sha256 \ + dist/* \ --clobber publish-crate: @@ -165,3 +189,13 @@ jobs: run: cargo publish --locked env: CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} + + verify-install: + permissions: + contents: read + name: Verify published installations + needs: [publish-crate] + uses: ./.github/workflows/prebuilt.yml + with: + verify-install: true + ref: ${{ inputs.tag || github.sha }} diff --git a/.gitignore b/.gitignore index 4e31434..ffabef3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target/ _sqlc_dev.yaml .memsearch +/dist/ +__pycache__/ diff --git a/Cargo.toml b/Cargo.toml index 832230e..ced9862 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,19 @@ description = "A sqlc plugin that generates type-safe sqlx Rust code from SQL qu repository = "https://github.com/mathematic-inc/sqlc-gen-sqlx" license = "MIT OR Apache-2.0" +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }.tar.gz" +bin-dir = "{ bin }{ binary-ext }" +pkg-fmt = "tgz" + +[package.metadata.binstall.overrides.x86_64-pc-windows-msvc] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }.zip" +pkg-fmt = "zip" + +[package.metadata.binstall.overrides.aarch64-pc-windows-msvc] +pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ version }-{ target }.zip" +pkg-fmt = "zip" + [workspace] resolver = "2" members = [".", "examples/*"] diff --git a/README.md b/README.md index 6f96c1d..2b451aa 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,42 @@ A [sqlc](https://sqlc.dev) plugin that generates type-safe [sqlx](https://github.com/transact-rs/sqlx) Rust code from SQL queries. +## Prebuilt installation + +Release archives contain the executable and install without a Rust compiler. +Install with cargo-binstall, with source compilation disabled: + +```sh +cargo binstall --disable-strategies compile sqlc-gen-sqlx +``` + +Or declare the GitHub release directly in `mise.toml`: + +```toml +[tools] +"github:mathematic-inc/sqlc-gen-sqlx" = "latest" +``` + +Run `mise install` to download and activate the executable. No custom mise plugin +is required. The Cargo backend (`cargo:sqlc-gen-sqlx`) also supports these releases; +set `cargo.binstall_only = true` to reject source compilation. + +| Platform | Architectures | Archive | +| --- | --- | --- | +| macOS | x64, ARM64 | `.tar.gz` | +| Linux GNU (glibc 2.35 or newer) | x64, ARM64 | `.tar.gz` | +| Linux musl | x64, ARM64 | `.tar.gz` | +| Windows MSVC | x64, ARM64 | `.zip` | + +Every archive has a SHA-256 sidecar and GitHub build provenance. CI builds all +eight targets and runs the extracted executables on the matching architecture. +After publication, the release workflow installs through cargo-binstall and mise +and runs both installations. A missing prebuilt binary fails the release checks. + +The release also includes `sqlc-gen-sqlx.wasm`. Continue using `plugins[].wasm` +with its URL and checksum for sqlc's WASM plugin mode. The native executable +installed above is used through `plugins[].process.cmd: sqlc-gen-sqlx` instead. + ## What it generates For each SQL query annotated with a sqlc command, the plugin emits: diff --git a/mise.toml b/mise.toml index 9068e16..8048c78 100644 --- a/mise.toml +++ b/mise.toml @@ -6,6 +6,7 @@ npm.package_manager = "npm" [tools] actionlint = "1.7.12" +cargo-binstall = "1.22.0" buf = "1.72.0" ghalint = "1.5.6" gitleaks = "8.30.1" diff --git a/scripts/release/release.py b/scripts/release/release.py new file mode 100644 index 0000000..dfb9533 --- /dev/null +++ b/scripts/release/release.py @@ -0,0 +1,191 @@ +"""Package and verify the executable described by a Cargo manifest.""" + +import argparse +import hashlib +import json +import os +import re +import subprocess +import tarfile +import tempfile +import zipfile +from pathlib import Path + +import tomllib + + +def configuration(manifest, target): + document = tomllib.loads(manifest.read_text(encoding="utf-8")) + package = document["package"] + repository = package["repository"] + if isinstance(repository, dict): + for parent in manifest.resolve().parents: + root = parent / "Cargo.toml" + if root.is_file(): + workspace = tomllib.loads(root.read_text(encoding="utf-8")).get( + "workspace", {} + ) + if "repository" in workspace.get("package", {}): + repository = workspace["package"]["repository"] + break + else: + raise ValueError("workspace repository is missing") + binaries = document.get("bin", [{"name": package["name"]}]) + if len(binaries) != 1: + raise ValueError("release packaging requires exactly one binary") + metadata = package["metadata"]["binstall"] + metadata = metadata | metadata.get("overrides", {}).get(target, {}) + values = { + "repo": repository.rstrip("/"), + "name": package["name"], + "version": package["version"], + "target": target, + "bin": binaries[0]["name"], + "binary-ext": ".exe" if "windows" in target else "", + } + + def render(template): + return re.sub(r"\{\s*([\w-]+)\s*\}", lambda match: values[match[1]], template) + + url = render(metadata["pkg-url"]) + binary = render(metadata["bin-dir"]) + if Path(binary).name != binary or "/" in binary or "\\" in binary: + raise ValueError("the release binary must be at the archive root") + return { + "package": package["name"], + "version": package["version"], + "binary": binary, + "archive": url.rsplit("/", 1)[1], + "tag": url.split("/releases/download/", 1)[1].split("/", 1)[0], + "repository": repository.removeprefix("https://github.com/"), + "format": metadata["pkg-fmt"], + } + + +def package_binary(config, binary, output): + if not binary.is_file() or binary.stat().st_size == 0: + raise ValueError(f"missing or empty executable: {binary}") + output.mkdir(parents=True, exist_ok=True) + archive = output / config["archive"] + if config["format"] == "zip": + with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as bundle: + bundle.write(binary, config["binary"]) + elif config["format"] == "tgz": + with tarfile.open(archive, "w:gz") as bundle: + info = bundle.gettarinfo(binary, arcname=config["binary"]) + info.mode = 0o755 + info.uid = info.gid = 0 + info.uname = info.gname = "" + with binary.open("rb") as executable: + bundle.addfile(info, executable) + else: + raise ValueError(f"unsupported archive format: {config['format']}") + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + archive.with_name(archive.name + ".sha256").write_text( + f"{digest} {archive.name}\n", encoding="utf-8" + ) + print(f"Packaged {archive}", flush=True) + return archive + + +def smoke(binary, package): + args = ( + [] + if package in {"protoc-gen-protovalidate-buffa", "sqlc-gen-sqlx"} + else ["--help"] + ) + result = subprocess.run( + [str(binary.resolve()), *args], + input=b"", + capture_output=True, + timeout=30, + check=True, + ) + if not result.stdout: + raise ValueError(f"{binary} returned an empty smoke-test response") + print(f"Smoke test passed: {binary}", flush=True) + + +def verify_archive(config, output): + archive = output / config["archive"] + expected = archive.with_name(archive.name + ".sha256").read_text().split()[0] + if hashlib.sha256(archive.read_bytes()).hexdigest() != expected: + raise ValueError(f"checksum mismatch: {archive}") + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + if config["format"] == "zip": + with zipfile.ZipFile(archive) as bundle: + if bundle.namelist() != [config["binary"]]: + raise ValueError("unexpected files in release archive") + bundle.extractall(directory) + else: + with tarfile.open(archive) as bundle: + if bundle.getnames() != [config["binary"]]: + raise ValueError("unexpected files in release archive") + bundle.extractall(directory, filter="data") + smoke(directory / config["binary"], config["package"]) + + +def verify_install(config, target): + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + binstall = directory / "binstall" + subprocess.run( + [ + "cargo-binstall", + "--no-confirm", + "--disable-telemetry", + "--disable-strategies", + "compile,quick-install", + "--targets", + target, + "--install-path", + str(binstall), + "--no-track", + f"{config['package']}@{config['version']}", + ], + check=True, + ) + smoke(binstall / config["binary"], config["package"]) + destination = directory / "mise" + tool = f"github:{config['repository']}[asset_pattern={config['archive']}]@{config['tag']}" + subprocess.run(["mise", "install-into", tool, str(destination)], check=True) + matches = list(destination.rglob(config["binary"])) + if len(matches) != 1: + raise ValueError(f"expected one mise executable, found {matches}") + smoke(matches[0], config["package"]) + if ( + hashlib.sha256(matches[0].read_bytes()).digest() + != hashlib.sha256((binstall / config["binary"]).read_bytes()).digest() + ): + raise ValueError("mise and cargo-binstall installed different executables") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "command", choices=["metadata", "package", "verify-archive", "verify-install"] + ) + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--target", required=True) + parser.add_argument("--binary", type=Path) + parser.add_argument("--output", type=Path, default=Path("dist")) + args = parser.parse_args() + config = configuration(args.manifest, args.target) + if args.command == "metadata": + print(json.dumps(config)) + if output := os.environ.get("GITHUB_OUTPUT"): + with Path(output).open("a", encoding="utf-8") as stream: + stream.writelines(f"{key}={value}\n" for key, value in config.items()) + elif args.command == "package": + if args.binary is None: + parser.error("package requires --binary") + package_binary(config, args.binary, args.output) + elif args.command == "verify-archive": + verify_archive(config, args.output) + else: + verify_install(config, args.target) + + +if __name__ == "__main__": + main() diff --git a/scripts/release/targets.json b/scripts/release/targets.json new file mode 100644 index 0000000..7f00227 --- /dev/null +++ b/scripts/release/targets.json @@ -0,0 +1,52 @@ +{ + "include": [ + { + "target": "x86_64-apple-darwin", + "runner": "macos-15-intel", + "build_runner": "macos-15-intel", + "build_tool": "cargo" + }, + { + "target": "aarch64-apple-darwin", + "runner": "macos-15", + "build_runner": "macos-15", + "build_tool": "cargo" + }, + { + "target": "x86_64-unknown-linux-gnu", + "runner": "ubuntu-22.04", + "build_runner": "ubuntu-22.04", + "build_tool": "cargo" + }, + { + "target": "aarch64-unknown-linux-gnu", + "runner": "ubuntu-22.04-arm", + "build_runner": "ubuntu-22.04-arm", + "build_tool": "cargo" + }, + { + "target": "x86_64-unknown-linux-musl", + "runner": "ubuntu-22.04", + "build_runner": "ubuntu-22.04", + "build_tool": "cargo" + }, + { + "target": "aarch64-unknown-linux-musl", + "runner": "ubuntu-22.04-arm", + "build_runner": "ubuntu-22.04-arm", + "build_tool": "cargo" + }, + { + "target": "x86_64-pc-windows-msvc", + "runner": "windows-2025", + "build_runner": "windows-2025", + "build_tool": "cargo" + }, + { + "target": "aarch64-pc-windows-msvc", + "runner": "windows-11-arm", + "build_runner": "windows-11-arm", + "build_tool": "cargo" + } + ] +} diff --git a/scripts/release/test_release.py b/scripts/release/test_release.py new file mode 100644 index 0000000..75a8023 --- /dev/null +++ b/scripts/release/test_release.py @@ -0,0 +1,88 @@ +import hashlib +import sys +import tarfile +import tempfile +import unittest +import zipfile +from pathlib import Path + +import release + + +class PackagingTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.binary = self.root / "tool" + self.binary.write_bytes(Path(sys.executable).read_bytes()) + self.config = { + "archive": "tool-1.2.3-target.tar.gz", + "binary": "tool", + "format": "tgz", + } + + def test_tar_preserves_binary_and_executable_permissions(self): + archive = release.package_binary(self.config, self.binary, self.root / "dist") + with tarfile.open(archive) as bundle: + self.assertEqual(bundle.getnames(), ["tool"]) + self.assertEqual(bundle.getmember("tool").mode, 0o755) + self.assertEqual( + bundle.extractfile("tool").read(), self.binary.read_bytes() + ) + digest, filename = ( + archive.with_name(archive.name + ".sha256").read_text().split() + ) + self.assertEqual(digest, hashlib.sha256(archive.read_bytes()).hexdigest()) + self.assertEqual(filename, archive.name) + + def test_zip_preserves_windows_executable_name(self): + config = self.config | { + "archive": "tool-1.2.3-target.zip", + "binary": "other-name.exe", + "format": "zip", + } + archive = release.package_binary(config, self.binary, self.root / "dist") + with zipfile.ZipFile(archive) as bundle: + self.assertEqual(bundle.namelist(), ["other-name.exe"]) + self.assertEqual(bundle.read("other-name.exe"), self.binary.read_bytes()) + + def test_empty_executable_is_rejected(self): + self.binary.write_bytes(b"") + with self.assertRaisesRegex(ValueError, "empty executable"): + release.package_binary(self.config, self.binary, self.root / "dist") + + def test_modified_archive_is_rejected_before_execution(self): + archive = release.package_binary(self.config, self.binary, self.root / "dist") + with archive.open("ab") as stream: + stream.write(b"tampered") + with self.assertRaisesRegex(ValueError, "checksum mismatch"): + release.verify_archive(self.config, self.root / "dist") + + def test_component_tags_and_binary_names_are_resolved(self): + manifest = self.root / "Cargo.toml" + manifest.write_text("""[package] +name = "crate-name" +version = "1.2.3" +repository = "https://github.com/owner/repo" +[[bin]] +name = "executable" +[package.metadata.binstall] +pkg-url = "{ repo }/releases/download/crate-name-v{ version }/{ name }-{ version }-{ target }.tar.gz" +bin-dir = "{ bin }{ binary-ext }" +pkg-fmt = "tgz" +[package.metadata.binstall.overrides.aarch64-pc-windows-msvc] +pkg-url = "{ repo }/releases/download/crate-name-v{ version }/{ name }-{ version }-{ target }.zip" +pkg-fmt = "zip" +""") + config = release.configuration(manifest, "aarch64-pc-windows-msvc") + self.assertEqual(config["binary"], "executable.exe") + self.assertEqual(config["tag"], "crate-name-v1.2.3") + self.assertEqual( + config["archive"], "crate-name-1.2.3-aarch64-pc-windows-msvc.zip" + ) + self.assertEqual(config["format"], "zip") + + +if __name__ == "__main__": + unittest.main() From a2b9832023550c5608cddad0256236e853248f84 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 7 Sep 2026 05:56:18 -0700 Subject: [PATCH 2/5] fix(release): package and verify binaries with standard tools --- .github/workflows/prebuilt.yml | 196 +++++++++++++++++++------------- .gitignore | 1 - scripts/release/release.py | 191 ------------------------------- scripts/release/targets.json | 52 --------- scripts/release/test_release.py | 88 -------------- 5 files changed, 116 insertions(+), 412 deletions(-) delete mode 100644 scripts/release/release.py delete mode 100644 scripts/release/targets.json delete mode 100644 scripts/release/test_release.py diff --git a/.github/workflows/prebuilt.yml b/.github/workflows/prebuilt.yml index bd347cc..0ff76d2 100644 --- a/.github/workflows/prebuilt.yml +++ b/.github/workflows/prebuilt.yml @@ -21,48 +21,45 @@ env: CARGO_TERM_COLOR: always MANIFEST: Cargo.toml CARGO_PACKAGE: sqlc-gen-sqlx + BINARY_NAME: sqlc-gen-sqlx + RELEASE_PREFIX: v jobs: - metadata: - permissions: - contents: read - runs-on: ubuntu-22.04 - timeout-minutes: 10 - outputs: - matrix: ${{ steps.matrix.outputs.matrix }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ inputs.ref || github.sha }} - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - with: - install_args: python - cache: false - - name: Test release packaging - run: python -m unittest discover -s scripts/release -p 'test_*.py' -v - - id: matrix - run: | - python - <<'PYTHON' - import json - import os - from pathlib import Path - matrix = json.loads(Path("scripts/release/targets.json").read_text()) - with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: - output.write("matrix=" + json.dumps(matrix) + "\n") - PYTHON - build: + if: ${{ !inputs.verify-install }} permissions: contents: read - if: ${{ !inputs.verify-install }} - needs: metadata name: Build ${{ matrix.target }} runs-on: ${{ matrix.build_runner }} timeout-minutes: 90 strategy: fail-fast: false - matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + matrix: &targets + include: + - target: x86_64-apple-darwin + runner: macos-15-intel + build_runner: macos-15-intel + - target: aarch64-apple-darwin + runner: macos-15 + build_runner: macos-15 + - target: x86_64-unknown-linux-gnu + runner: ubuntu-22.04 + build_runner: ubuntu-22.04 + - target: aarch64-unknown-linux-gnu + runner: ubuntu-22.04-arm + build_runner: ubuntu-22.04-arm + - target: x86_64-unknown-linux-musl + runner: ubuntu-22.04 + build_runner: ubuntu-22.04 + - target: aarch64-unknown-linux-musl + runner: ubuntu-22.04-arm + build_runner: ubuntu-22.04-arm + - target: x86_64-pc-windows-msvc + runner: windows-2025 + build_runner: windows-2025 + - target: aarch64-pc-windows-msvc + runner: windows-11-arm + build_runner: windows-11-arm steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -70,7 +67,7 @@ jobs: ref: ${{ inputs.ref || github.sha }} - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - install_args: python rust + install_args: rust cache: false - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: @@ -78,44 +75,43 @@ jobs: - name: Install Linux build dependencies if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config musl-tools libdbus-1-dev - - name: Configure musl linker - if: contains(matrix.target, 'musl') + - name: Configure target linking env: TARGET: ${{ matrix.target }} run: | - target_env="${TARGET^^}" - target_env="${target_env//-/_}" - echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" >> "$GITHUB_ENV" - echo "CC_${TARGET//-/_}=musl-gcc" >> "$GITHUB_ENV" - - - name: Link the Windows runtime statically - if: contains(matrix.target, 'windows') - env: - TARGET: ${{ matrix.target }} - run: | - target_env="${TARGET^^}" - target_env="${target_env//-/_}" - echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" - - name: Read package metadata - id: package - env: - TARGET: ${{ matrix.target }} - run: python scripts/release/release.py metadata --manifest "$MANIFEST" --target "$TARGET" - - name: Build executable + target_env="$(printf '%s' "$TARGET" | tr '[:lower:]-' '[:upper:]_')" + if [[ "$TARGET" == *musl ]]; then + echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" + echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" >> "$GITHUB_ENV" + echo "CC_${TARGET//-/_}=musl-gcc" >> "$GITHUB_ENV" + elif [[ "$TARGET" == *windows* ]]; then + echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" + fi + + - name: Build and package executable env: TARGET: ${{ matrix.target }} - VERSION: ${{ steps.package.outputs.version }} - BUILD_TOOL: ${{ matrix.build_tool }} run: | + version="$(awk -F'"' '/^version = / { print $2; exit }' "$MANIFEST")" + test -n "$version" rustup target add "$TARGET" cargo build --locked --release -p "$CARGO_PACKAGE" --target "$TARGET" - - name: Package executable and checksum - env: - TARGET: ${{ matrix.target }} - BINARY: ${{ steps.package.outputs.binary }} - run: | - python scripts/release/release.py package --manifest "$MANIFEST" --target "$TARGET" \ - --binary "target/$TARGET/release/$BINARY" + mkdir -p dist + if [[ "$TARGET" == *windows* ]]; then + archive="$(pwd)/dist/$CARGO_PACKAGE-$version-$TARGET.zip" + (cd "target/$TARGET/release" && 7z a -tzip "$archive" "$BINARY_NAME.exe") + else + tar -C "target/$TARGET/release" -czf "dist/$CARGO_PACKAGE-$version-$TARGET.tar.gz" "$BINARY_NAME" + fi + cd dist + for archive in *.tar.gz *.zip; do + [[ -f "$archive" ]] || continue + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$archive" > "$archive.sha256" + else + shasum -a 256 "$archive" > "$archive.sha256" + fi + done - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: binary-${{ matrix.target }} @@ -123,25 +119,17 @@ jobs: if-no-files-found: error smoke: + if: ${{ !inputs.verify-install }} permissions: contents: read - if: ${{ !inputs.verify-install }} - needs: [metadata, build] + needs: build name: Run archive ${{ matrix.target }} runs-on: ${{ matrix.runner }} timeout-minutes: 15 strategy: fail-fast: false - matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + matrix: *targets steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ inputs.ref || github.sha }} - - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - with: - install_args: python - cache: false - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: binary-${{ matrix.target }} @@ -149,19 +137,44 @@ jobs: - name: Verify checksum and run extracted executable env: TARGET: ${{ matrix.target }} - run: python scripts/release/release.py verify-archive --manifest "$MANIFEST" --target "$TARGET" + run: | + cd dist + mkdir unpack + if [[ "$TARGET" == *windows* ]]; then + archive=("$CARGO_PACKAGE"-*.zip) + binary="$BINARY_NAME.exe" + else + archive=("$CARGO_PACKAGE"-*.tar.gz) + binary="$BINARY_NAME" + fi + test "${#archive[@]}" -eq 1 + if command -v sha256sum >/dev/null 2>&1; then + sha256sum -c "${archive[0]}.sha256" + else + shasum -a 256 -c "${archive[0]}.sha256" + fi + if [[ "$TARGET" == *windows* ]]; then + 7z x "${archive[0]}" -ounpack + else + tar -xzf "${archive[0]}" -C unpack + fi + if [[ "$CARGO_PACKAGE" == protoc-gen-* || "$CARGO_PACKAGE" == sqlc-gen-sqlx ]]; then + "unpack/$binary" < /dev/null > smoke.out + else + "unpack/$binary" --help > smoke.out + fi + test -s smoke.out install: + if: inputs.verify-install permissions: contents: read - if: inputs.verify-install - needs: metadata name: Install without compiling ${{ matrix.target }} runs-on: ${{ matrix.runner }} timeout-minutes: 15 strategy: fail-fast: false - matrix: ${{ fromJSON(needs.metadata.outputs.matrix) }} + matrix: *targets steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -169,10 +182,33 @@ jobs: ref: ${{ inputs.ref || github.sha }} - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 with: - install_args: python cargo-binstall + install_args: cargo-binstall cache: false - name: Install published binaries through binstall and mise env: TARGET: ${{ matrix.target }} GH_TOKEN: ${{ github.token }} - run: python scripts/release/release.py verify-install --manifest "$MANIFEST" --target "$TARGET" + run: | + version="$(awk -F'"' '/^version = / { print $2; exit }' "$MANIFEST")" + test -n "$version" + binary="$BINARY_NAME" + archive="$CARGO_PACKAGE-$version-$TARGET.tar.gz" + if [[ "$TARGET" == *windows* ]]; then + binary="$binary.exe" + archive="$CARGO_PACKAGE-$version-$TARGET.zip" + fi + cargo-binstall "$CARGO_PACKAGE@$version" --no-confirm --disable-telemetry \ + --disable-strategies compile,quick-install --targets "$TARGET" \ + --install-path "$RUNNER_TEMP/binstall" --no-track + mise install-into \ + "github:${GITHUB_REPOSITORY}[asset_pattern=${archive}]@${RELEASE_PREFIX}${version}" \ + "$RUNNER_TEMP/mise" + for prefix in "$RUNNER_TEMP/binstall" "$RUNNER_TEMP/mise"; do + if [[ "$CARGO_PACKAGE" == protoc-gen-* || "$CARGO_PACKAGE" == sqlc-gen-sqlx ]]; then + "$prefix/$binary" < /dev/null > "$RUNNER_TEMP/smoke.out" + else + "$prefix/$binary" --help > "$RUNNER_TEMP/smoke.out" + fi + test -s "$RUNNER_TEMP/smoke.out" + done + cmp "$RUNNER_TEMP/binstall/$binary" "$RUNNER_TEMP/mise/$binary" diff --git a/.gitignore b/.gitignore index ffabef3..3002281 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ _sqlc_dev.yaml .memsearch /dist/ -__pycache__/ diff --git a/scripts/release/release.py b/scripts/release/release.py deleted file mode 100644 index dfb9533..0000000 --- a/scripts/release/release.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Package and verify the executable described by a Cargo manifest.""" - -import argparse -import hashlib -import json -import os -import re -import subprocess -import tarfile -import tempfile -import zipfile -from pathlib import Path - -import tomllib - - -def configuration(manifest, target): - document = tomllib.loads(manifest.read_text(encoding="utf-8")) - package = document["package"] - repository = package["repository"] - if isinstance(repository, dict): - for parent in manifest.resolve().parents: - root = parent / "Cargo.toml" - if root.is_file(): - workspace = tomllib.loads(root.read_text(encoding="utf-8")).get( - "workspace", {} - ) - if "repository" in workspace.get("package", {}): - repository = workspace["package"]["repository"] - break - else: - raise ValueError("workspace repository is missing") - binaries = document.get("bin", [{"name": package["name"]}]) - if len(binaries) != 1: - raise ValueError("release packaging requires exactly one binary") - metadata = package["metadata"]["binstall"] - metadata = metadata | metadata.get("overrides", {}).get(target, {}) - values = { - "repo": repository.rstrip("/"), - "name": package["name"], - "version": package["version"], - "target": target, - "bin": binaries[0]["name"], - "binary-ext": ".exe" if "windows" in target else "", - } - - def render(template): - return re.sub(r"\{\s*([\w-]+)\s*\}", lambda match: values[match[1]], template) - - url = render(metadata["pkg-url"]) - binary = render(metadata["bin-dir"]) - if Path(binary).name != binary or "/" in binary or "\\" in binary: - raise ValueError("the release binary must be at the archive root") - return { - "package": package["name"], - "version": package["version"], - "binary": binary, - "archive": url.rsplit("/", 1)[1], - "tag": url.split("/releases/download/", 1)[1].split("/", 1)[0], - "repository": repository.removeprefix("https://github.com/"), - "format": metadata["pkg-fmt"], - } - - -def package_binary(config, binary, output): - if not binary.is_file() or binary.stat().st_size == 0: - raise ValueError(f"missing or empty executable: {binary}") - output.mkdir(parents=True, exist_ok=True) - archive = output / config["archive"] - if config["format"] == "zip": - with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as bundle: - bundle.write(binary, config["binary"]) - elif config["format"] == "tgz": - with tarfile.open(archive, "w:gz") as bundle: - info = bundle.gettarinfo(binary, arcname=config["binary"]) - info.mode = 0o755 - info.uid = info.gid = 0 - info.uname = info.gname = "" - with binary.open("rb") as executable: - bundle.addfile(info, executable) - else: - raise ValueError(f"unsupported archive format: {config['format']}") - digest = hashlib.sha256(archive.read_bytes()).hexdigest() - archive.with_name(archive.name + ".sha256").write_text( - f"{digest} {archive.name}\n", encoding="utf-8" - ) - print(f"Packaged {archive}", flush=True) - return archive - - -def smoke(binary, package): - args = ( - [] - if package in {"protoc-gen-protovalidate-buffa", "sqlc-gen-sqlx"} - else ["--help"] - ) - result = subprocess.run( - [str(binary.resolve()), *args], - input=b"", - capture_output=True, - timeout=30, - check=True, - ) - if not result.stdout: - raise ValueError(f"{binary} returned an empty smoke-test response") - print(f"Smoke test passed: {binary}", flush=True) - - -def verify_archive(config, output): - archive = output / config["archive"] - expected = archive.with_name(archive.name + ".sha256").read_text().split()[0] - if hashlib.sha256(archive.read_bytes()).hexdigest() != expected: - raise ValueError(f"checksum mismatch: {archive}") - with tempfile.TemporaryDirectory() as temporary: - directory = Path(temporary) - if config["format"] == "zip": - with zipfile.ZipFile(archive) as bundle: - if bundle.namelist() != [config["binary"]]: - raise ValueError("unexpected files in release archive") - bundle.extractall(directory) - else: - with tarfile.open(archive) as bundle: - if bundle.getnames() != [config["binary"]]: - raise ValueError("unexpected files in release archive") - bundle.extractall(directory, filter="data") - smoke(directory / config["binary"], config["package"]) - - -def verify_install(config, target): - with tempfile.TemporaryDirectory() as temporary: - directory = Path(temporary) - binstall = directory / "binstall" - subprocess.run( - [ - "cargo-binstall", - "--no-confirm", - "--disable-telemetry", - "--disable-strategies", - "compile,quick-install", - "--targets", - target, - "--install-path", - str(binstall), - "--no-track", - f"{config['package']}@{config['version']}", - ], - check=True, - ) - smoke(binstall / config["binary"], config["package"]) - destination = directory / "mise" - tool = f"github:{config['repository']}[asset_pattern={config['archive']}]@{config['tag']}" - subprocess.run(["mise", "install-into", tool, str(destination)], check=True) - matches = list(destination.rglob(config["binary"])) - if len(matches) != 1: - raise ValueError(f"expected one mise executable, found {matches}") - smoke(matches[0], config["package"]) - if ( - hashlib.sha256(matches[0].read_bytes()).digest() - != hashlib.sha256((binstall / config["binary"]).read_bytes()).digest() - ): - raise ValueError("mise and cargo-binstall installed different executables") - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "command", choices=["metadata", "package", "verify-archive", "verify-install"] - ) - parser.add_argument("--manifest", type=Path, required=True) - parser.add_argument("--target", required=True) - parser.add_argument("--binary", type=Path) - parser.add_argument("--output", type=Path, default=Path("dist")) - args = parser.parse_args() - config = configuration(args.manifest, args.target) - if args.command == "metadata": - print(json.dumps(config)) - if output := os.environ.get("GITHUB_OUTPUT"): - with Path(output).open("a", encoding="utf-8") as stream: - stream.writelines(f"{key}={value}\n" for key, value in config.items()) - elif args.command == "package": - if args.binary is None: - parser.error("package requires --binary") - package_binary(config, args.binary, args.output) - elif args.command == "verify-archive": - verify_archive(config, args.output) - else: - verify_install(config, args.target) - - -if __name__ == "__main__": - main() diff --git a/scripts/release/targets.json b/scripts/release/targets.json deleted file mode 100644 index 7f00227..0000000 --- a/scripts/release/targets.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "include": [ - { - "target": "x86_64-apple-darwin", - "runner": "macos-15-intel", - "build_runner": "macos-15-intel", - "build_tool": "cargo" - }, - { - "target": "aarch64-apple-darwin", - "runner": "macos-15", - "build_runner": "macos-15", - "build_tool": "cargo" - }, - { - "target": "x86_64-unknown-linux-gnu", - "runner": "ubuntu-22.04", - "build_runner": "ubuntu-22.04", - "build_tool": "cargo" - }, - { - "target": "aarch64-unknown-linux-gnu", - "runner": "ubuntu-22.04-arm", - "build_runner": "ubuntu-22.04-arm", - "build_tool": "cargo" - }, - { - "target": "x86_64-unknown-linux-musl", - "runner": "ubuntu-22.04", - "build_runner": "ubuntu-22.04", - "build_tool": "cargo" - }, - { - "target": "aarch64-unknown-linux-musl", - "runner": "ubuntu-22.04-arm", - "build_runner": "ubuntu-22.04-arm", - "build_tool": "cargo" - }, - { - "target": "x86_64-pc-windows-msvc", - "runner": "windows-2025", - "build_runner": "windows-2025", - "build_tool": "cargo" - }, - { - "target": "aarch64-pc-windows-msvc", - "runner": "windows-11-arm", - "build_runner": "windows-11-arm", - "build_tool": "cargo" - } - ] -} diff --git a/scripts/release/test_release.py b/scripts/release/test_release.py deleted file mode 100644 index 75a8023..0000000 --- a/scripts/release/test_release.py +++ /dev/null @@ -1,88 +0,0 @@ -import hashlib -import sys -import tarfile -import tempfile -import unittest -import zipfile -from pathlib import Path - -import release - - -class PackagingTests(unittest.TestCase): - def setUp(self): - self.temporary = tempfile.TemporaryDirectory() - self.addCleanup(self.temporary.cleanup) - self.root = Path(self.temporary.name) - self.binary = self.root / "tool" - self.binary.write_bytes(Path(sys.executable).read_bytes()) - self.config = { - "archive": "tool-1.2.3-target.tar.gz", - "binary": "tool", - "format": "tgz", - } - - def test_tar_preserves_binary_and_executable_permissions(self): - archive = release.package_binary(self.config, self.binary, self.root / "dist") - with tarfile.open(archive) as bundle: - self.assertEqual(bundle.getnames(), ["tool"]) - self.assertEqual(bundle.getmember("tool").mode, 0o755) - self.assertEqual( - bundle.extractfile("tool").read(), self.binary.read_bytes() - ) - digest, filename = ( - archive.with_name(archive.name + ".sha256").read_text().split() - ) - self.assertEqual(digest, hashlib.sha256(archive.read_bytes()).hexdigest()) - self.assertEqual(filename, archive.name) - - def test_zip_preserves_windows_executable_name(self): - config = self.config | { - "archive": "tool-1.2.3-target.zip", - "binary": "other-name.exe", - "format": "zip", - } - archive = release.package_binary(config, self.binary, self.root / "dist") - with zipfile.ZipFile(archive) as bundle: - self.assertEqual(bundle.namelist(), ["other-name.exe"]) - self.assertEqual(bundle.read("other-name.exe"), self.binary.read_bytes()) - - def test_empty_executable_is_rejected(self): - self.binary.write_bytes(b"") - with self.assertRaisesRegex(ValueError, "empty executable"): - release.package_binary(self.config, self.binary, self.root / "dist") - - def test_modified_archive_is_rejected_before_execution(self): - archive = release.package_binary(self.config, self.binary, self.root / "dist") - with archive.open("ab") as stream: - stream.write(b"tampered") - with self.assertRaisesRegex(ValueError, "checksum mismatch"): - release.verify_archive(self.config, self.root / "dist") - - def test_component_tags_and_binary_names_are_resolved(self): - manifest = self.root / "Cargo.toml" - manifest.write_text("""[package] -name = "crate-name" -version = "1.2.3" -repository = "https://github.com/owner/repo" -[[bin]] -name = "executable" -[package.metadata.binstall] -pkg-url = "{ repo }/releases/download/crate-name-v{ version }/{ name }-{ version }-{ target }.tar.gz" -bin-dir = "{ bin }{ binary-ext }" -pkg-fmt = "tgz" -[package.metadata.binstall.overrides.aarch64-pc-windows-msvc] -pkg-url = "{ repo }/releases/download/crate-name-v{ version }/{ name }-{ version }-{ target }.zip" -pkg-fmt = "zip" -""") - config = release.configuration(manifest, "aarch64-pc-windows-msvc") - self.assertEqual(config["binary"], "executable.exe") - self.assertEqual(config["tag"], "crate-name-v1.2.3") - self.assertEqual( - config["archive"], "crate-name-1.2.3-aarch64-pc-windows-msvc.zip" - ) - self.assertEqual(config["format"], "zip") - - -if __name__ == "__main__": - unittest.main() From 325604c7cd97718702fccaeb123cc10f28e2ccc2 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 7 Sep 2026 05:57:07 -0700 Subject: [PATCH 3/5] fix(release): package and verify binaries with standard tools --- .github/workflows/prebuilt.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/prebuilt.yml b/.github/workflows/prebuilt.yml index 0ff76d2..d7c709d 100644 --- a/.github/workflows/prebuilt.yml +++ b/.github/workflows/prebuilt.yml @@ -81,12 +81,12 @@ jobs: run: | target_env="$(printf '%s' "$TARGET" | tr '[:lower:]-' '[:upper:]_')" if [[ "$TARGET" == *musl ]]; then - echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" - echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" >> "$GITHUB_ENV" - echo "CC_${TARGET//-/_}=musl-gcc" >> "$GITHUB_ENV" + echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" + echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" + echo "CC_${TARGET//-/_}=musl-gcc" elif [[ "$TARGET" == *windows* ]]; then - echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" >> "$GITHUB_ENV" - fi + echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" + fi >> "$GITHUB_ENV" - name: Build and package executable env: From 4e29691b2ca711c57ff5196afc8748358f0ef36c Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 7 Sep 2026 05:58:26 -0700 Subject: [PATCH 4/5] docs(install): clarify release checksums --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b451aa..021817a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ set `cargo.binstall_only = true` to reject source compilation. | Linux musl | x64, ARM64 | `.tar.gz` | | Windows MSVC | x64, ARM64 | `.zip` | -Every archive has a SHA-256 sidecar and GitHub build provenance. CI builds all +Every archive includes SHA-256 checksums and GitHub build provenance. CI builds all eight targets and runs the extracted executables on the matching architecture. After publication, the release workflow installs through cargo-binstall and mise and runs both installations. A missing prebuilt binary fails the release checks. From 2551d48f6635dea630887b452ee2047a2fe47851 Mon Sep 17 00:00:00 2001 From: Randolf Jung Date: Mon, 7 Sep 2026 06:04:47 -0700 Subject: [PATCH 5/5] fix(release): use Rust bundled runtime for static musl binaries --- .github/workflows/prebuilt.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prebuilt.yml b/.github/workflows/prebuilt.yml index d7c709d..afe3fa0 100644 --- a/.github/workflows/prebuilt.yml +++ b/.github/workflows/prebuilt.yml @@ -82,7 +82,7 @@ jobs: target_env="$(printf '%s' "$TARGET" | tr '[:lower:]-' '[:upper:]_')" if [[ "$TARGET" == *musl ]]; then echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static" - echo "CARGO_TARGET_${target_env}_LINKER=musl-gcc" + # Rust supplies its musl runtime; musl-gcc is for C dependencies. echo "CC_${TARGET//-/_}=musl-gcc" elif [[ "$TARGET" == *windows* ]]; then echo "CARGO_TARGET_${target_env}_RUSTFLAGS=-C target-feature=+crt-static"