diff --git a/.github/scripts/install-zig.sh b/.github/scripts/install-zig.sh new file mode 100644 index 0000000..8bdf3a6 --- /dev/null +++ b/.github/scripts/install-zig.sh @@ -0,0 +1,166 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 1 ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +version="$1" + +python_bin="${PYTHON:-python3}" +if ! command -v "$python_bin" >/dev/null 2>&1; then + python_bin="python" +fi +if ! command -v "$python_bin" >/dev/null 2>&1; then + echo "python is required to install Zig" >&2 + exit 1 +fi + +runner_os="${RUNNER_OS:-$(uname -s)}" +runner_arch="${RUNNER_ARCH:-$(uname -m)}" + +case "$runner_os" in + Linux | linux) + zig_os="linux" + ;; + Darwin | macOS) + zig_os="macos" + ;; + Windows | MINGW* | MSYS* | CYGWIN*) + zig_os="windows" + ;; + *) + echo "unsupported runner OS: $runner_os" >&2 + exit 1 + ;; +esac + +case "$runner_arch" in + X64 | x86_64 | amd64) + zig_arch="x86_64" + ;; + ARM64 | arm64 | aarch64) + zig_arch="aarch64" + ;; + *) + echo "unsupported runner architecture: $runner_arch" >&2 + exit 1 + ;; +esac + +host_key="${zig_arch}-${zig_os}" +tool_root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/wasm3-zig" +install_dir="${tool_root}/${version}/${host_key}" +zig_bin="zig" +if [ "$zig_os" = "windows" ]; then + zig_bin="zig.exe" +fi + +if [ ! -x "${install_dir}/${zig_bin}" ]; then + mkdir -p "$(dirname "$install_dir")" + + zig_metadata="$( + "$python_bin" - "$version" "$host_key" <<'PY' +import json +import sys +import urllib.request + +version = sys.argv[1] +host_key = sys.argv[2] + +with urllib.request.urlopen("https://ziglang.org/download/index.json") as response: + data = json.load(response) + +host = data.get(version, {}).get(host_key) +if not host: + raise SystemExit(f"missing Zig download metadata for version={version!r} host={host_key!r}") + +archive_url = host.get("tarball") or host.get("zip") +checksum = host.get("shasum") or "" +if not archive_url: + raise SystemExit(f"missing archive URL for version={version!r} host={host_key!r}") + +print(archive_url) +print(checksum) +PY + )" + + archive_url="$(printf '%s\n' "$zig_metadata" | sed -n '1p')" + expected_sha="$(printf '%s\n' "$zig_metadata" | sed -n '2p')" + if [ -z "$archive_url" ]; then + echo "failed to resolve Zig download URL" >&2 + exit 1 + fi + + archive_name="${archive_url##*/}" + archive_dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/zig-archive.XXXXXX")" + archive_path="${archive_dir}/${archive_name}" + extract_dir="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/zig-extract.XXXXXX")" + trap 'rm -rf "$archive_dir"; rm -rf "$extract_dir"' EXIT + + curl -fsSL --retry 3 --retry-all-errors "$archive_url" -o "$archive_path" + + "$python_bin" - "$archive_path" "$expected_sha" <<'PY' +import hashlib +import sys + +path = sys.argv[1] +expected = sys.argv[2].strip().lower() +if not expected: + raise SystemExit(0) + +digest = hashlib.sha256() +with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + +actual = digest.hexdigest().lower() +if actual != expected: + raise SystemExit(f"checksum mismatch for {path}: expected {expected}, got {actual}") +PY + + "$python_bin" - "$archive_path" "$extract_dir" <<'PY' +import pathlib +import sys +import tarfile +import zipfile + +archive = pathlib.Path(sys.argv[1]) +destination = pathlib.Path(sys.argv[2]) +destination.mkdir(parents=True, exist_ok=True) + +def ensure_within_destination(relative_name: str) -> None: + target = (destination / relative_name).resolve() + if destination.resolve() not in target.parents and target != destination.resolve(): + raise SystemExit(f"archive entry escapes destination: {relative_name}") + +if archive.suffix == ".zip": + with zipfile.ZipFile(archive) as handle: + for member in handle.namelist(): + ensure_within_destination(member) + handle.extractall(destination) +else: + with tarfile.open(archive, "r:*") as handle: + for member in handle.getnames(): + ensure_within_destination(member) + handle.extractall(destination) +PY + + extracted_dir="$(find "$extract_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)" + if [ -z "$extracted_dir" ]; then + echo "failed to extract Zig archive: $archive_url" >&2 + exit 1 + fi + + rm -rf "$install_dir" + mv "$extracted_dir" "$install_dir" +fi + +if [ -n "${GITHUB_PATH:-}" ]; then + printf '%s\n' "$install_dir" >> "$GITHUB_PATH" +else + echo "GITHUB_PATH is not set; add this directory to PATH manually: $install_dir" >&2 +fi + +"${install_dir}/${zig_bin}" version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..40ead64 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + ZIG_VERSION: "0.16.0" + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: linux-x86_64 + zig_target: x86_64-linux-musl + - os: macos-latest + target: macos-aarch64 + zig_target: aarch64-macos + - os: windows-latest + target: windows-x86_64 + zig_target: x86_64-windows + + steps: + - uses: actions/checkout@v6 + + - name: Install Zig 0.16.0 + run: bash .github/scripts/install-zig.sh "${ZIG_VERSION}" + + - name: Cache Zig build outputs + uses: actions/cache@v5 + with: + path: | + .zig-cache + ~/.cache/zig + key: zig-${{ matrix.target }}-${{ hashFiles('build.zig', 'build.zig.zon', 'source/**') }} + restore-keys: zig-${{ matrix.target }}- + + - name: Build native Debug + run: zig build + + - name: Build ReleaseSmall + run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseSmall + + - name: Preserve package + run: | + mkdir -p ci-artifacts/include ci-artifacts/lib + find zig-out/include -maxdepth 1 -type f -name '*.h' -exec cp {} ci-artifacts/include/ \; + cp LICENSE ci-artifacts/ + found_lib=0 + for artifact in zig-out/lib/libwasm3.a zig-out/lib/wasm3.lib; do + if [ -f "$artifact" ]; then + cp "$artifact" ci-artifacts/lib/ + found_lib=1 + fi + done + if [ "$found_lib" -eq 0 ]; then + echo "No wasm3 library artifact found in zig-out/lib" >&2 + ls -la zig-out/lib >&2 || true + exit 1 + fi + + - name: Upload package + if: success() + uses: actions/upload-artifact@v7 + with: + name: wasm3-${{ matrix.target }} + path: ci-artifacts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..17262b8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,127 @@ +name: Release + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + ZIG_VERSION: "0.16.0" + +on: + push: + tags: ['v*'] + workflow_dispatch: + +jobs: + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + strategy: + matrix: + include: + - os: ubuntu-latest + target: linux-x86_64 + zig_target: x86_64-linux-musl + - os: ubuntu-latest + target: linux-aarch64 + zig_target: aarch64-linux-musl + - os: ubuntu-latest + target: linux-riscv64 + zig_target: riscv64-linux-musl + - os: macos-latest + target: macos-aarch64 + zig_target: aarch64-macos + - os: macos-latest + target: macos-x86_64 + zig_target: x86_64-macos + - os: windows-latest + target: windows-x86_64 + zig_target: x86_64-windows + - os: windows-latest + target: windows-aarch64 + zig_target: aarch64-windows + + steps: + - uses: actions/checkout@v6 + + - name: Install Zig 0.16.0 + run: bash .github/scripts/install-zig.sh "${ZIG_VERSION}" + + - name: Build ReleaseSmall + run: zig build -Doptimize=ReleaseSmall -Dtarget=${{ matrix.zig_target }} + + - name: Prepare package + run: | + mkdir -p release-package/include release-package/lib + find zig-out/include -maxdepth 1 -type f -name '*.h' -exec cp {} release-package/include/ \; + cp LICENSE release-package/ + found_lib=0 + for artifact in zig-out/lib/libwasm3.a zig-out/lib/wasm3.lib; do + if [ -f "$artifact" ]; then + cp "$artifact" release-package/lib/ + found_lib=1 + fi + done + if [ "$found_lib" -eq 0 ]; then + echo "No wasm3 library artifact found in zig-out/lib" >&2 + ls -la zig-out/lib >&2 || true + exit 1 + fi + + - name: Upload package + uses: actions/upload-artifact@v7 + with: + name: wasm3-package-${{ matrix.target }} + path: release-package + + source: + name: Prepare source archive + runs-on: ubuntu-latest + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v6 + + - name: Create source archive + run: | + archive_name="wasm3-source-${GITHUB_REF_NAME}.tar.gz" + tar \ + --exclude='.git' \ + --exclude='.zig-cache' \ + --exclude='zig-out' \ + -czf "/tmp/${archive_name}" . + mv "/tmp/${archive_name}" . + echo "ARCHIVE_NAME=${archive_name}" >> "$GITHUB_ENV" + + - name: Upload source archive + uses: actions/upload-artifact@v7 + with: + name: wasm3-source + path: ${{ env.ARCHIVE_NAME }} + + release: + needs: [build, source] + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v8 + + - name: Package release archives + run: | + for dir in wasm3-package-*; do + target="${dir#wasm3-package-}" + tar -czf "wasm3-${target}.tar.gz" -C "$dir" . + done + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + files: | + wasm3-*.tar.gz + wasm3-source/*.tar.gz + generate_release_notes: true diff --git a/build.zig b/build.zig index 715ae99..60fb001 100644 --- a/build.zig +++ b/build.zig @@ -24,6 +24,7 @@ pub fn build(b: *std.Build) void { lib.root_module.addCSourceFile(.{ .file = b.path("source/m3_info.c") }); lib.root_module.addCSourceFile(.{ .file = b.path("source/m3_module.c") }); lib.root_module.addCSourceFile(.{ .file = b.path("source/m3_parse.c") }); + lib.installHeadersDirectory(b.path("source"), "", .{}); b.installArtifact(lib); } diff --git a/build.zig.zon b/build.zig.zon index 51143fe..08be92b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .wasm3, .version = "0.5.1", .fingerprint = 0x377ef951351db386, - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .paths = .{ "build.zig", "build.zig.zon",