diff --git a/.github/workflows/prbuild.yaml b/.github/workflows/prbuild.yaml index 0c18201..6638f3d 100644 --- a/.github/workflows/prbuild.yaml +++ b/.github/workflows/prbuild.yaml @@ -1,23 +1,32 @@ --- name: PR build on: - pull_request_target: + pull_request: paths-ignore: - ".github/workflows/**" branches: - "main" +# This workflow does not push or consume any secrets, so `pull_request` +# (not `pull_request_target`) is appropriate: fork PRs still build but +# their GITHUB_TOKEN is read-only and secrets aren't exposed, which +# matters because the smoketest job runs a script from the PR tree. +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true +env: + # PR-scoped local image tag used by the smoketest job after hydrating + # from the GHA buildx cache written by the build job. + CI_IMAGE: eve-rust-ci:pr-${{ github.event.pull_request.number }} + jobs: build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - fail-fast: false + runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Starting Report run: | @@ -29,17 +38,51 @@ jobs: free -m - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 - name: Set up QEMU uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + # Validates the Dockerfile builds for both host arches, and populates + # the GHA buildx cache that the smoketest job rehydrates from. The + # tag is purely local — nothing pushes or loads this image. - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 with: context: . platforms: linux/amd64,linux/arm64 push: false - tags: | - ${{ github.event.pull_request.head.repo.full_name }}:latest + tags: eve-rust-ci:multiarch + cache-to: type=gha,mode=max + cache-from: type=gha + + smoketest: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + # Pull the amd64 image from the GHA cache written by the build job + # and load it into the local docker daemon so `docker run` in run.sh + # can see it. All layers are cache hits — this step is effectively + # just the --load export. + - name: Hydrate eve-rust image from build cache + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 + with: + context: . + platforms: linux/amd64 + load: true + tags: ${{ env.CI_IMAGE }} + cache-from: type=gha + # Regression test: cross-compile the smoketest crate for each supported + # musl target, assert ELF arch, then run each binary under qemu-user. + # Catches silent cross-compile breakage of the kind we hit enabling + # riscv64 (wrong-arch libc picked up from host /usr/lib, tier-3 target + # spec missing crt-static-default, etc). + - name: Run smoketest + env: + RUST_IMAGE: ${{ env.CI_IMAGE }} + run: ./test/smoketest/run.sh diff --git a/test/smoketest/.gitignore b/test/smoketest/.gitignore new file mode 100644 index 0000000..88494fb --- /dev/null +++ b/test/smoketest/.gitignore @@ -0,0 +1,3 @@ +/target/ +/Cargo.lock +/.cargo/ diff --git a/test/smoketest/Cargo.toml b/test/smoketest/Cargo.toml new file mode 100644 index 0000000..96123dd --- /dev/null +++ b/test/smoketest/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "eve-rust-smoketest" +version = "0.1.0" +edition = "2021" +publish = false + +[[bin]] +name = "smoketest" +path = "src/main.rs" diff --git a/test/smoketest/cargo-config.toml b/test/smoketest/cargo-config.toml new file mode 100644 index 0000000..54d71d8 --- /dev/null +++ b/test/smoketest/cargo-config.toml @@ -0,0 +1,15 @@ +# Shape matches what real consumers (e.g. EVE's pkg/vector) use: profile +# settings plus per-cfg rustflags that merge with the base image's +# [target.] entries in /usr/local/cargo/config.toml. + +[profile.release] +opt-level = "z" +lto = "fat" +codegen-units = 1 +strip = "symbols" + +[target.'cfg(target_env = "musl")'] +rustflags = [ + "-C", "embed-bitcode=yes", + "-A", "dead_code", +] diff --git a/test/smoketest/run.sh b/test/smoketest/run.sh new file mode 100755 index 0000000..bd2061a --- /dev/null +++ b/test/smoketest/run.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# Regression test for the eve-rust toolchain image: +# - cross-compile a tiny crate for all three supported musl targets using +# $RUST_IMAGE as the toolchain +# - assert each resulting ELF has the expected machine type +# - execute each binary under qemu-user (via binfmt_misc) to prove it +# actually runs, not just links +# +# Used both locally and in the eve-rust CI pipeline. For non-amd64 execution +# the caller is responsible for registering qemu handlers +# (docker/setup-qemu-action in CI, `docker run --privileged tonistiigi/binfmt +# --install all` locally). + +set -euo pipefail + +RUST_IMAGE="${RUST_IMAGE:-lfedge/eve-rust:latest}" +cd "$(dirname "$0")" + +declare -A TARGETS=( + [amd64]=x86_64-unknown-linux-musl + [arm64]=aarch64-unknown-linux-musl + [riscv64]=riscv64gc-unknown-linux-musl +) + +declare -A EXPECTED_MACHINE=( + [amd64]="Advanced Micro Devices X86-64" + [arm64]="AArch64" + [riscv64]="RISC-V" +) + +# Build all three targets in one invocation of the rust container, reusing +# the target/ cache across them. We `docker run` directly (no buildx) so a +# just-loaded local image like `eve-rust-ci:pr` is visible without pushing +# to a registry. +echo "=== Cross-compiling smoketest for all 3 targets ===" +docker run --rm \ + -v "$(pwd):/app" \ + -w /app \ + "${RUST_IMAGE}" \ + sh -c ' + set -eu + mkdir -p .cargo + cp cargo-config.toml .cargo/config.toml + for t in x86_64-unknown-linux-musl aarch64-unknown-linux-musl riscv64gc-unknown-linux-musl; do + echo "--- compile: $t ---" + CARGO_BUILD_TARGET=$t cargo build --release + done + ' + +fail=0 +for arch in amd64 arm64 riscv64; do + target="${TARGETS[$arch]}" + expected_machine="${EXPECTED_MACHINE[$arch]}" + binary="target/${target}/release/smoketest" + + echo + echo "================ $target ($arch) ================" + + if [ ! -f "$binary" ]; then + echo "FAIL: $binary was not produced" + fail=1 + continue + fi + + # readelf is provided by binutils (preinstalled on ubuntu-latest); fall + # back to running it inside the rust image if missing. + if command -v readelf >/dev/null 2>&1; then + elf_hdr=$(readelf -h "$binary") + else + elf_hdr=$(docker run --rm -v "$(pwd):/app" -w /app "${RUST_IMAGE}" llvm-readelf -h "$binary") + fi + echo "$elf_hdr" | grep -E "Class|Data|Machine|Type" + + actual_machine=$(echo "$elf_hdr" | awk -F: '/Machine:/ {sub(/^ +/, "", $2); print $2}') + if [ "$actual_machine" != "$expected_machine" ]; then + echo "FAIL: $arch has ELF Machine=\"${actual_machine}\", expected \"${expected_machine}\"" + fail=1 + continue + fi + + # Execute the binary inside a matching-arch alpine container. Docker plus + # binfmt_misc routes through qemu-user for non-host arches. + if docker run --rm --platform="linux/${arch}" -v "$(pwd):/app" -w /app alpine:3.22 "./${binary}"; then + echo "PASS: $arch" + else + rc=$? + echo "FAIL: $arch (exit $rc)" + fail=1 + fi +done + +echo +if [ "$fail" -eq 0 ]; then + echo "All three targets linked, are the correct arch, and ran under qemu-user." +else + echo "One or more targets failed." >&2 + exit 1 +fi diff --git a/test/smoketest/src/main.rs b/test/smoketest/src/main.rs new file mode 100644 index 0000000..364b7c4 --- /dev/null +++ b/test/smoketest/src/main.rs @@ -0,0 +1,12 @@ +use std::collections::HashMap; + +fn main() { + let mut m: HashMap<&str, u32> = HashMap::new(); + m.insert("riscv64", 64); + m.insert("musl", 1); + for (k, v) in &m { + println!("{k} = {v}"); + } + assert_eq!(m.values().sum::(), 65); + println!("ok"); +}