# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 name: Rust Image Binary Build (openshell-gateway / openshell-sandbox / openshell-cli) # Build Rust binaries per Linux architecture before the Docker image build # consumes them as prebuilt artifacts. Gateway images use GNU-linked binaries # for the NVIDIA distroless C/C++ runtime; supervisor and cli images use musl/static # binaries so the final image can remain scratch. Gateway GNU binaries are # built with an explicit glibc 2.28 floor so image, package, and tarball # artifacts share the same host portability contract. on: workflow_call: inputs: component: description: "Binary component to build (gateway, sandbox, or cli)" required: true type: string arch: description: "Linux architecture to build (amd64 or arm64)" required: true type: string cargo-version: description: "Pre-computed cargo version (skips internal git-based computation)" required: false type: string default: "" features: description: "Cargo features to enable" required: false type: string default: "" retention-days: description: "Artifact retention period" required: false type: number default: 5 artifact-name: description: "Artifact name override" required: false type: string default: "" checkout-ref: description: "Git ref to check out for build inputs (defaults to the workflow SHA)" required: false type: string default: "" image-tag: description: "Supervisor image tag to bake into gateway binaries" required: false type: string default: "" permissions: contents: read packages: read env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: "0" MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Route sccache (already RUSTC_WRAPPER in mise.toml) to the GHA cache # backend instead of the EKS memcached used by ARC. SCCACHE_GHA_ENABLED: "true" defaults: run: shell: bash jobs: rust-native-build: name: ${{ inputs.component }} (${{ inputs.arch }}) runs-on: ${{ inputs.arch == 'arm64' && 'linux-arm64-cpu8' || 'linux-amd64-cpu8' }} timeout-minutes: 60 env: COMPONENT: ${{ inputs.component }} ARCH: ${{ inputs.arch }} FEATURES: ${{ inputs.features }} # Partition the GHA sccache cache per (component, arch). Without this, # concurrent jobs collide on the same cache key and later-starting # writers hit 409 Conflict. SCCACHE_GHA_VERSION: ${{ inputs.component }}-${{ inputs.arch }} container: image: ghcr.io/nvidia/openshell/ci:latest credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ inputs['checkout-ref'] || github.sha }} fetch-depth: 0 - name: Mark workspace safe for git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Fetch tags run: git fetch --tags --force - name: Configure GHA sccache backend # Exposes ACTIONS_CACHE_URL / ACTIONS_RUNTIME_TOKEN before `mise install` # compiles cargo-installed tools through RUSTC_WRAPPER=sccache. uses: mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696 # v0.0.10 - name: Install tools run: mise install --locked - name: Resolve build target id: target run: | set -euo pipefail case "$COMPONENT" in gateway) crate=openshell-server binary=openshell-gateway zig_target= ;; sandbox) crate=openshell-sandbox binary=openshell-sandbox zig_target= ;; cli) crate=openshell-cli binary=openshell zig_target= ;; *) echo "unsupported component: $COMPONENT" >&2 exit 1 ;; esac case "$ARCH" in amd64) if [[ "$COMPONENT" == "sandbox" || "$COMPONENT" == "cli" ]]; then target=x86_64-unknown-linux-musl zig_target=x86_64-linux-musl else target=x86_64-unknown-linux-gnu zig_target=x86_64-unknown-linux-gnu.2.28 fi ;; arm64) if [[ "$COMPONENT" == "sandbox" || "$COMPONENT" == "cli" ]]; then target=aarch64-unknown-linux-musl zig_target=aarch64-linux-musl else target=aarch64-unknown-linux-gnu zig_target=aarch64-unknown-linux-gnu.2.28 fi ;; *) echo "unsupported arch: $ARCH" >&2 exit 1 ;; esac { echo "crate=$crate" echo "binary=$binary" echo "target=$target" echo "zig_target=$zig_target" } >> "$GITHUB_OUTPUT" - name: Cache Rust target and registry uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 with: shared-key: rust-native-${{ inputs.component }}-${{ inputs.arch }}-zig-wrapper-${{ hashFiles('tasks/scripts/setup-zig-cc-wrapper.sh') }} cache-directories: .cache/sccache cache-targets: "true" - name: Compute cargo version id: version run: | set -euo pipefail if [[ -n "${{ inputs['cargo-version'] }}" ]]; then echo "cargo_version=${{ inputs['cargo-version'] }}" >> "$GITHUB_OUTPUT" else echo "cargo_version=$(uv run python tasks/scripts/release.py get-version --cargo)" >> "$GITHUB_OUTPUT" fi - name: Patch workspace version if: steps.version.outputs.cargo_version != '' run: | set -euo pipefail sed -i -E '/^\[workspace\.package\]/,/^\[/{s/^version[[:space:]]*=[[:space:]]*".*"/version = "'"${{ steps.version.outputs.cargo_version }}"'"/}' Cargo.toml - name: Set up zig musl wrappers if: contains(steps.target.outputs.target, 'musl') run: | set -euo pipefail ZIG="$(mise which zig)" ZIG_TARGET="${{ steps.target.outputs.zig_target }}" mkdir -p /tmp/zig-musl # cc-rs injects --target=, which zig does not parse. # Strip caller-provided --target and use the wrapper's zig target. for tool in cc c++; do printf '#!/bin/bash\nargs=()\nfor arg in "$@"; do\n case "$arg" in\n --target=*) ;;\n *) args+=("$arg") ;;\n esac\ndone\nexec "%s" %s --target=%s "${args[@]}"\n' \ "$ZIG" "$tool" "$ZIG_TARGET" > "/tmp/zig-musl/${tool}" chmod +x "/tmp/zig-musl/${tool}" done TARGET_ENV=$(echo "${{ steps.target.outputs.target }}" | tr '-' '_') TARGET_ENV_UPPER=${TARGET_ENV^^} echo "CC_${TARGET_ENV}=/tmp/zig-musl/cc" >> "$GITHUB_ENV" echo "CXX_${TARGET_ENV}=/tmp/zig-musl/c++" >> "$GITHUB_ENV" echo "CARGO_TARGET_${TARGET_ENV_UPPER}_LINKER=/tmp/zig-musl/cc" >> "$GITHUB_ENV" echo "CARGO_TARGET_${TARGET_ENV_UPPER}_RUSTFLAGS=-Clink-self-contained=no" >> "$GITHUB_ENV" - name: Set up zig glibc wrappers if: inputs.component == 'gateway' run: tasks/scripts/setup-zig-cc-wrapper.sh "${{ steps.target.outputs.zig_target }}" "${{ steps.target.outputs.zig_target }}" /tmp/zig-gnu - name: Build ${{ steps.target.outputs.binary }} (${{ steps.target.outputs.zig_target || steps.target.outputs.target }}) env: # Preserve the release-codegen setting used by the old Dockerfile # Rust build path so image artifacts keep the same release profile. CARGO_PROFILE_RELEASE_CODEGEN_UNITS: "1" OPENSHELL_IMAGE_TAG: ${{ inputs['image-tag'] }} run: | set -euo pipefail # z3 built with zig c++ uses libc++ symbols (std::__1::*). # Override z3-sys default (stdc++) so Rust links the matching runtime. if [[ "${{ inputs.component }}" == "cli" ]]; then echo "CXXSTDLIB=c++" >> "$GITHUB_ENV" fi mise x -- rustup target add "${{ steps.target.outputs.target }}" cargo_cmd=(cargo build) build_target="${{ steps.target.outputs.target }}" args=() if [[ "${{ inputs.component }}" == "gateway" ]]; then cargo_cmd=(cargo zigbuild) build_target="${{ steps.target.outputs.zig_target }}" args+=(--features bundled-z3) fi args+=( --release --target "$build_target" -p "${{ steps.target.outputs.crate }}" --bin "${{ steps.target.outputs.binary }}" ) if [[ -n "$FEATURES" ]]; then args+=(--features "$FEATURES") fi if [[ -n "${{ steps.version.outputs.cargo_version }}" ]]; then export GIT_DIR=/nonexistent fi mise x -- "${cargo_cmd[@]}" "${args[@]}" - name: Verify packaged binary run: | set -euo pipefail BIN="target/${{ steps.target.outputs.target }}/release/${{ steps.target.outputs.binary }}" OUTPUT="$("$BIN" --version)" echo "$OUTPUT" grep -q "^${{ steps.target.outputs.binary }} " <<<"$OUTPUT" # Record linkage so image runtime drift is visible in logs. ldd --version ldd "$BIN" || true if [[ "${{ inputs.component }}" == "gateway" ]] && ldd "$BIN" | grep -q 'libz3'; then echo "gateway binary must not depend on shared libz3; enable bundled-z3 for image artifacts" >&2 exit 1 fi - name: Verify glibc symbol floor if: inputs.component == 'gateway' run: | set -euo pipefail BIN="target/${{ steps.target.outputs.target }}/release/${{ steps.target.outputs.binary }}" tasks/scripts/verify-glibc-symbols.sh 2.28 "$BIN" - name: Stage binary for prebuilt layout run: | set -euo pipefail STAGE="prebuilt-binaries/$ARCH" mkdir -p "$STAGE" install -m 0755 \ "target/${{ steps.target.outputs.target }}/release/${{ steps.target.outputs.binary }}" \ "$STAGE/${{ steps.target.outputs.binary }}" ls -lh "$STAGE/" - name: Upload artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: ${{ inputs['artifact-name'] != '' && inputs['artifact-name'] || format('rust-binary-{0}-linux-{1}', inputs.component, inputs.arch) }} path: prebuilt-binaries/${{ inputs.arch }}/${{ steps.target.outputs.binary }} retention-days: ${{ inputs['retention-days'] }} if-no-files-found: error - name: sccache stats if: always() run: | set +e stats_bin="${SCCACHE_PATH:-sccache}" "$stats_bin" --show-stats status=$? if [[ $status -ne 0 ]]; then echo "::warning::sccache stats unavailable (exit $status)" fi exit 0