From 03a770c2e80cb02490cc4f50d5d23ba3728473df Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 13 Aug 2026 09:01:03 -0400 Subject: [PATCH 1/5] ext-cli: resolve $CC from the cross-canadian bindir, not the target sysroot avocado-cli-compile.sh prepended $SDKTARGETSYSROOT/usr/bin to PATH so the `cc` crate (via aws-lc-sys) could find the compiler $CC names. That bindir holds the target-*native* toolchain, so "-gcc" resolved to a target ELF; binfmt_misc handed it to qemu-user, which cannot resolve the target loader and failed every compiler probe with exit 255: qemu-aarch64: Could not open '/usr/lib/ld-linux-aarch64.so.1' Compiler family detection failed ... exit status: 255 The cross-canadian compiler the SDK actually means lives under the SDK *native* sysroot, at $OECORE_NATIVE_SYSROOT/usr/bin//. Point PATH there and fail loudly if it is absent. This shipped because both ext workflows only build qemux86-64, where the target gcc is same-arch and runs natively, so the wrong PATH entry is harmless. Add a raspberrypi4 leg to ext-test.yml, which reproduces the failure and now guards the fix. Reported via avocado-linux/references#23, where the rubicon reference cannot install avocado-ext-cli for raspberrypi4. --- .github/workflows/ext-test.yml | 5 +++++ avocado-cli-compile.sh | 25 ++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ext-test.yml b/.github/workflows/ext-test.yml index 761f67d9..4cf89729 100644 --- a/.github/workflows/ext-test.yml +++ b/.github/workflows/ext-test.yml @@ -43,6 +43,11 @@ jobs: matrix: include: - { target: qemux86-64, distro-release: "2024", distro-channel: next } + # Keep a cross-arch leg. A same-arch target cannot catch a host/target + # toolchain mix-up in avocado-cli-compile.sh: on qemux86-64 the target + # gcc runs natively, so picking the wrong one still builds. On aarch64 + # the wrong pick goes through qemu-user and fails every C probe. + - { target: raspberrypi4, distro-release: "2024", distro-channel: next } uses: avocado-linux/actions/.github/workflows/extension-test.yml@v1 with: target: ${{ matrix.target }} diff --git a/avocado-cli-compile.sh b/avocado-cli-compile.sh index c2af9802..25ae0cdf 100755 --- a/avocado-cli-compile.sh +++ b/avocado-cli-compile.sh @@ -44,13 +44,24 @@ rustflags = ["--sysroot=$SDKTARGETSYSROOT/usr", "-C", "link-arg=--sysroot=$SDKTA EOF # The SDK exports $CC as the cross-compiler command (bare name + target flags + -# --sysroot), but in the `ext build` environment that compiler binary lives in -# the SDK target-sysroot bindir, which is not on PATH. Without this, the `cc` -# crate (pulled in by the remaining C dep aws-lc-sys) can't resolve the compiler -# from $CC and falls back to guessing "-gcc", failing with ToolNotFound. -# Put the SDK compiler bindir on PATH so the build uses exactly the $CC the SDK -# configured. Arch-agnostic: derived from $SDKTARGETSYSROOT, no hardcoded triple. -export PATH="$SDKTARGETSYSROOT/usr/bin:$PATH" +# --sysroot), so the `cc` crate (pulled in by the C dep aws-lc-sys) needs that +# binary on PATH or it fails with ToolNotFound. +# +# It lives in the cross-canadian bindir under the SDK *native* sysroot -- NOT in +# $SDKTARGETSYSROOT/usr/bin, which holds the target-*native* toolchain. Putting +# the target sysroot bindir on PATH resolves "-gcc" to a target ELF; +# binfmt_misc then hands it to qemu-user, which dies on the unresolvable target +# loader ("qemu-aarch64: Could not open '/usr/lib/ld-linux-aarch64.so.1'") and +# fails every compiler probe with exit 255. That is invisible on a same-arch +# target like qemux86-64, where the target gcc happens to run natively. +# +# Arch-agnostic: the triple comes from $CROSS_COMPILE, no hardcoded value. +CROSS_BINDIR="$OECORE_NATIVE_SYSROOT/usr/bin/${CROSS_COMPILE%-}" +if [ ! -x "$CROSS_BINDIR/${CROSS_COMPILE}gcc" ]; then + echo "Error: cross compiler not found at $CROSS_BINDIR/${CROSS_COMPILE}gcc" >&2 + exit 1 +fi +export PATH="$CROSS_BINDIR:$PATH" # --locked: published builds run from staged package_files; fail loudly on a # missing/stale Cargo.lock instead of silently re-resolving dependencies. From 215e2f39a93f6d9fb7dd1d8475391805f491339a Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 13 Aug 2026 09:11:05 -0400 Subject: [PATCH 2/5] ext-cli: install the C cross-canadian toolchain packagegroup-rust-cross-canadian-avocado- brings rust-cross-canadian plus nativesdk-gcc -- a HOST compiler. It does not bring a C cross-compiler, so gcc-cross-canadian- was never installed and $OECORE_NATIVE_SYSROOT/usr/bin//-gcc did not exist. That is why the target-sysroot PATH hack appeared to work: with no cross compiler installed, the only -gcc anywhere was the target-native one in $SDKTARGETSYSROOT, which happens to be host-executable when target arch == host arch (qemux86-64). On a cross-arch target it is a foreign ELF and every compiler probe died under qemu-user. Pull packagegroup-cross-canadian-avocado- so the real cross compiler is present for the PATH fix to find. --- avocado.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/avocado.yaml b/avocado.yaml index 7ee983eb..073ae806 100644 --- a/avocado.yaml +++ b/avocado.yaml @@ -40,6 +40,11 @@ extensions: nativesdk-rust: '*' nativesdk-git: '*' packagegroup-rust-cross-canadian-avocado-{{ avocado.target }}: '*' + # The rust- packagegroup brings rust-cross-canadian + nativesdk-gcc (a + # HOST compiler); it does NOT bring a C cross-compiler. aws-lc-sys needs + # one, so pull the C cross-canadian toolchain explicitly -- this is what + # provides $OECORE_NATIVE_SYSROOT/usr/bin//-gcc. + packagegroup-cross-canadian-avocado-{{ avocado.target }}: '*' sdk: image: docker.io/avocadolinux/sdk:{{ env.AVOCADO_DISTRO_RELEASE }} From 60953ec7dd236d7a92bc6387463b5820cb819f52 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 13 Aug 2026 09:41:41 -0400 Subject: [PATCH 3/5] ext-cli: require the SDK env vars before deriving the cross bindir If OECORE_NATIVE_SYSROOT and CROSS_COMPILE expand empty, CROSS_BINDIR collapses to /usr/bin and the -x check finds the host gcc, so the build silently produces a native binary packaged as a target extension -- the same silently-wrong class of failure this branch is fixing. The RUST_TARGET_PATH loop above already aborts when the env is entirely unsourced, but not when it is partially set, and the script runs with set -e and not set -u. Reported by Copilot on #200. --- avocado-cli-compile.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/avocado-cli-compile.sh b/avocado-cli-compile.sh index 25ae0cdf..743a3ac0 100755 --- a/avocado-cli-compile.sh +++ b/avocado-cli-compile.sh @@ -56,6 +56,14 @@ EOF # target like qemux86-64, where the target gcc happens to run natively. # # Arch-agnostic: the triple comes from $CROSS_COMPILE, no hardcoded value. +# +# Require both vars rather than letting them expand empty: that would collapse +# CROSS_BINDIR to "/usr/bin" and the -x check below would find the *host* gcc, +# quietly producing a native binary packaged as a target extension. This script +# runs with `set -e` but not `set -u`, so nothing else catches it. +: "${OECORE_NATIVE_SYSROOT:?not set -- SDK environment-setup was not sourced}" +: "${CROSS_COMPILE:?not set -- SDK environment-setup was not sourced}" + CROSS_BINDIR="$OECORE_NATIVE_SYSROOT/usr/bin/${CROSS_COMPILE%-}" if [ ! -x "$CROSS_BINDIR/${CROSS_COMPILE}gcc" ]; then echo "Error: cross compiler not found at $CROSS_BINDIR/${CROSS_COMPILE}gcc" >&2 From a4910bfe623a47ea545cae53fd4f272f16fe5a9c Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 13 Aug 2026 17:26:19 -0400 Subject: [PATCH 4/5] ext-cli: qemuarm64 for the cross-arch leg, and harden the compiler guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI legs stay on qemu targets — swap the raspberrypi4 leg for qemuarm64. It is aarch64/cortexa57, so it reproduces the host/target toolchain mix-up exactly the same way, and packagegroup-cross-canadian-avocado-qemuarm64 is in the 2024 feed. Review feedback on the guard, all in avocado-cli-compile.sh: - Move the env guards above the .cargo/config.toml write. Both abort paths used to run after the file was created, leaving a stale gitignored file behind. - Guard $SDKTARGETSYSROOT too. It is the one SDK var the script actually consumes, and an empty one bakes a bogus --sysroot into the cargo config — the same silently-wrong class the other two guards exist to prevent. - Check the binary `cc` will really invoke: the first token of $CC, falling back to "-gcc" only when $CC is unset. The old check hardcoded gcc, so it verified a binary the build might not use and aborted a working clang SDK. - Name the recovery in the abort message. An SDK installed before the packagegroup was added to avocado.yaml does not have it, and nothing invalidates that install (the sdk-install stamp's config hash is written but never compared — no caller passes SDK CurrentInput), so the message has to say `avocado sdk install --force`. --- .github/workflows/ext-test.yml | 3 ++- avocado-cli-compile.sh | 32 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ext-test.yml b/.github/workflows/ext-test.yml index 4cf89729..53e8d7ee 100644 --- a/.github/workflows/ext-test.yml +++ b/.github/workflows/ext-test.yml @@ -47,7 +47,8 @@ jobs: # toolchain mix-up in avocado-cli-compile.sh: on qemux86-64 the target # gcc runs natively, so picking the wrong one still builds. On aarch64 # the wrong pick goes through qemu-user and fails every C probe. - - { target: raspberrypi4, distro-release: "2024", distro-channel: next } + # qemuarm64, not a board: CI legs stay on qemu targets. + - { target: qemuarm64, distro-release: "2024", distro-channel: next } uses: avocado-linux/actions/.github/workflows/extension-test.yml@v1 with: target: ${{ matrix.target }} diff --git a/avocado-cli-compile.sh b/avocado-cli-compile.sh index 743a3ac0..abc30af2 100755 --- a/avocado-cli-compile.sh +++ b/avocado-cli-compile.sh @@ -32,6 +32,17 @@ for var in $(env | grep -o 'CARGO_TARGET_[A-Z0-9_]*_RUSTFLAGS'); do unset "$var" done +# Require the SDK vars before touching the tree or deriving any path. Letting +# them expand empty would collapse CROSS_BINDIR to "/usr/bin", where the -x check +# below finds the *host* gcc and quietly produces a native binary packaged as a +# target extension; an empty SDKTARGETSYSROOT would bake a bogus --sysroot into +# .cargo/config.toml. This script runs with `set -e` but not `set -u`, so nothing +# else catches it. Guarding here also means neither abort leaves a half-written +# .cargo/config.toml behind. +: "${OECORE_NATIVE_SYSROOT:?not set -- SDK environment-setup was not sourced}" +: "${SDKTARGETSYSROOT:?not set -- SDK environment-setup was not sourced}" +: "${CROSS_COMPILE:?not set -- SDK environment-setup was not sourced}" + # Remove only the generated cross-compile config, preserving any committed # .cargo files used for development. rm -f .cargo/config.toml @@ -56,17 +67,18 @@ EOF # target like qemux86-64, where the target gcc happens to run natively. # # Arch-agnostic: the triple comes from $CROSS_COMPILE, no hardcoded value. -# -# Require both vars rather than letting them expand empty: that would collapse -# CROSS_BINDIR to "/usr/bin" and the -x check below would find the *host* gcc, -# quietly producing a native binary packaged as a target extension. This script -# runs with `set -e` but not `set -u`, so nothing else catches it. -: "${OECORE_NATIVE_SYSROOT:?not set -- SDK environment-setup was not sourced}" -: "${CROSS_COMPILE:?not set -- SDK environment-setup was not sourced}" - CROSS_BINDIR="$OECORE_NATIVE_SYSROOT/usr/bin/${CROSS_COMPILE%-}" -if [ ! -x "$CROSS_BINDIR/${CROSS_COMPILE}gcc" ]; then - echo "Error: cross compiler not found at $CROSS_BINDIR/${CROSS_COMPILE}gcc" >&2 + +# Check the binary `cc` will actually run: it takes the FIRST token of $CC (the +# rest are target flags) and falls back to guessing "-gcc" when $CC is +# unset. Hardcoding gcc here would also abort a working clang SDK. +CC_BIN="${CC:-${CROSS_COMPILE}gcc}" +CC_BIN="${CC_BIN%% *}" +if [ ! -x "$CROSS_BINDIR/$CC_BIN" ]; then + echo "Error: cross compiler '$CC_BIN' not found in $CROSS_BINDIR" >&2 + echo "The SDK is missing the C cross-canadian toolchain. An SDK installed" >&2 + echo "before it was added to avocado.yaml will not have it -- reinstall with:" >&2 + echo " avocado sdk install --force" >&2 exit 1 fi export PATH="$CROSS_BINDIR:$PATH" From a9feb3afe84d7845c8eba859ecca815d8fe0d878 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Thu, 13 Aug 2026 21:18:15 -0400 Subject: [PATCH 5/5] compile: name the cross compiler from CROSS_COMPILE, not $CC Parsing $CC's first token to find the compiler had three failure modes and no upside. oe-core writes CC and CROSS_COMPILE from the same TARGET_PREFIX (toolchain-scripts.bbclass), so the derived name is byte-identical to ${CROSS_COMPILE}gcc in every SDK this runs against, and meta-clang appends CLANGCC rather than overriding CC -- so the clang SDK the derivation was meant to accommodate aborted either way. What it did do: - an empty first token (a $CC with a leading space) made `-x` test the bindir itself, which is executable, so the guard passed on exactly the case it exists to catch - a ccache/distcc wrapper resolved to "ccache" and hard-aborted a working SDK, telling the user to run a multi-minute no-op reinstall - an absolute-path $CC produced "$CROSS_BINDIR//abs/path/..." and aborted - a tab-separated $CC kept its flags attached and aborted Simulated all four against a fake SDK layout plus the happy path, a stock flag-carrying $CC and a clang-only bindir: every case that regressed a working SDK now passes, the empty-token case now fires, and the control and clang cases are unchanged. --- avocado-cli-compile.sh | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/avocado-cli-compile.sh b/avocado-cli-compile.sh index abc30af2..a4d9cc02 100755 --- a/avocado-cli-compile.sh +++ b/avocado-cli-compile.sh @@ -69,11 +69,13 @@ EOF # Arch-agnostic: the triple comes from $CROSS_COMPILE, no hardcoded value. CROSS_BINDIR="$OECORE_NATIVE_SYSROOT/usr/bin/${CROSS_COMPILE%-}" -# Check the binary `cc` will actually run: it takes the FIRST token of $CC (the -# rest are target flags) and falls back to guessing "-gcc" when $CC is -# unset. Hardcoding gcc here would also abort a working clang SDK. -CC_BIN="${CC:-${CROSS_COMPILE}gcc}" -CC_BIN="${CC_BIN%% *}" +# Name the compiler from $CROSS_COMPILE rather than parsing $CC. oe-core writes +# both from ${TARGET_PREFIX} in toolchain-scripts.bbclass, so $CC's first token +# is byte-identical to this in every SDK we ship against, and meta-clang only +# appends CLANGCC without overriding CC. Deriving it from $CC bought nothing and +# mis-read three real inputs: an empty first token (making -x test the bindir +# itself, so the guard passed), a ccache/distcc wrapper, and an absolute path. +CC_BIN="${CROSS_COMPILE}gcc" if [ ! -x "$CROSS_BINDIR/$CC_BIN" ]; then echo "Error: cross compiler '$CC_BIN' not found in $CROSS_BINDIR" >&2 echo "The SDK is missing the C cross-canadian toolchain. An SDK installed" >&2