Skip to content

ext-cli: resolve $CC from the cross-canadian bindir, not the target sysroot - #200

Open
mobileoverlord wants to merge 5 commits into
mainfrom
fix/ext-cli-cross-cc-path
Open

ext-cli: resolve $CC from the cross-canadian bindir, not the target sysroot#200
mobileoverlord wants to merge 5 commits into
mainfrom
fix/ext-cli-cross-cc-path

Conversation

@mobileoverlord

Copy link
Copy Markdown
Contributor

What

avocado-cli-compile.sh prepended $SDKTARGETSYSROOT/usr/bin to PATH so the cc crate (pulled in by aws-lc-sys) could resolve the compiler $CC names. That bindir holds the target-native toolchain, not the cross compiler — the target gcc package owns /usr/bin/<triple>-gcc in the target sysroot. So <triple>-gcc resolved to a target ELF, binfmt_misc handed it to qemu-user, and every compiler probe died:

qemu-aarch64: Could not open '/usr/lib/ld-linux-aarch64.so.1'
Compiler family detection failed due to error: ToolExecError: ... exit status: 255
error: failed to run custom build command for `aws-lc-sys v0.40.0`

The cross-canadian compiler the SDK actually means lives under the SDK native sysroot:

$OECORE_NATIVE_SYSROOT/usr/bin/<triple>/<triple>-gcc     # x86_64 ELF, cross-canadian
$SDKTARGETSYSROOT/usr/bin/<triple>-gcc                   # target ELF, target-native

Point PATH at the former and fail loudly if it is missing.

Why it shipped

Both ext-test.yml and ext-release.yml only build qemux86-64. On a same-arch target the target gcc runs natively, so picking the wrong one still builds — the bug is invisible. It only fires on a cross-arch target, and only since aws-lc-sys entered the dependency graph (reqwest 0.13 + rustlsaws-lc-rs), because nothing before it had to invoke a C compiler.

Regression test

Adds a raspberrypi4 leg to ext-test.yml. That leg reproduces the failure on main and passes with this fix. Verified against the live 2024/edge feed: gcc-cross-canadian-aarch64 is installed in the SDK and provides $OECORE_NATIVE_SYSROOT/usr/bin/aarch64-avocado-linux/aarch64-avocado-linux-gcc.

Blast radius

Every consumer of avocado-ext-cli on a cross-arch target is currently broken — the published 0.41.2 cannot build. Found via avocado-linux/references#23, where the rubicon reference fails to install it for raspberrypi4. Needs a release to republish once merged.

…ysroot

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 "<triple>-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/<triple>/. 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.
Copilot AI lite review requested due to automatic review settings August 13, 2026 13:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes cross-compilation of avocado-ext-cli by ensuring the Rust cc crate resolves the correct cross-canadian compiler from the SDK native sysroot (instead of accidentally picking up a target-native gcc from the target sysroot), and adds CI coverage to catch cross-arch regressions.

Changes:

  • Update avocado-cli-compile.sh to prepend the cross-canadian bindir under $OECORE_NATIVE_SYSROOT to PATH, with a loud failure if the expected compiler binary is missing.
  • Add a raspberrypi4 (cross-arch) leg to ext-test.yml to reproduce and prevent the original failure mode.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
avocado-cli-compile.sh Switch PATH setup to use the SDK native sysroot cross-canadian bindir and validate the cross compiler is present.
.github/workflows/ext-test.yml Add a cross-arch CI matrix entry (raspberrypi4) to detect host/target toolchain mix-ups.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread avocado-cli-compile.sh
packagegroup-rust-cross-canadian-avocado-<target> brings
rust-cross-canadian plus nativesdk-gcc -- a HOST compiler. It does not
bring a C cross-compiler, so gcc-cross-canadian-<arch> was never
installed and $OECORE_NATIVE_SYSROOT/usr/bin/<triple>/<triple>-gcc did
not exist.

That is why the target-sysroot PATH hack appeared to work: with no cross
compiler installed, the only <triple>-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-<target> so the real cross
compiler is present for the PATH fix to find.
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.
@mobileoverlord
mobileoverlord requested a review from jetm August 13, 2026 13:43

@jetm jetm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core fix is right and the evidence for it is solid. I verified the parts that could have been hand-waved: ${CROSS_COMPILE%-} is the correct bindir derivation for every triple, the deleted $SDKTARGETSYSROOT/usr/bin entry took nothing away ($AR/$RANLIB/$LD/$OBJCOPY/$STRIP are all ${TARGET_PREFIX}-prefixed and live in the new dir), prepending cannot shadow host cargo/rustc, and packagegroup-cross-canadian-avocado-* really does resolve — there is a built RPM in the meta-avocado feed snapshot, packagegroup-rust-cross-canadian does not RDEPEND on it, so that line is load-bearing rather than belt-and-braces. shellcheck -s bash and bash -n are clean, and I simulated the new guard across unset / empty / missing-binary / happy paths and it fires correctly in each.

One thing I would want answered before this merges.

The new sdk.packages entry feeds no stamp hash, so a warm SDK never installs it. compute_sdk_input_hash (stamps.rs:968) reads only top-level sdk.*, and ext_build_hash_data (stamps.rs:1168) omits ext.sdk.packages. So an existing install stays cached, never picks up packagegroup-cross-canadian-avocado-*, and lands on the new hard exit 1 at avocado-cli-compile.sh:68 — whose message does not mention avocado sdk install --force. That turns an upgrade into a dead end for anyone who already has the SDK. Either fold ext.sdk.packages into the hash, or at minimum name the recovery in the abort message.

Second, and I could not verify it from here: .github/workflows/ext-release.yml:51 keeps the publish matrix qemux86-64-only, so no raspberrypi4 artifact ships. If the motivating report is the rubicon reference not being able to install avocado-ext-cli for raspberrypi4, this merge does not resolve it — but references#23 and avocado-linux/actions are not readable from here, so treat that as a question rather than a finding.

Nits, all optional:

  • avocado-cli-compile.sh:64$SDKTARGETSYSROOT is the one SDK var the script actually consumes and it is the one left out of the new :? guards. Same silently-wrong class the commit body names.
  • avocado-cli-compile.sh:41 — both abort paths run after .cargo/config.toml is written, leaving a stale gitignored file behind. I reproduced it. Moving the guards above the write fails before mutating the tree.
  • avocado-cli-compile.sh:68 — the guard proves ${CROSS_COMPILE}gcc exists, but the cc crate resolves the first token of $CC, which is never checked. It also aborts a working clang SDK.
  • avocado-cli-compile.sh:8 — pre-existing, not yours: the "${OECORE_TARGET_ARCH}-"* glob cannot match armv7 (arm vs armv7 triple), so supported_targets: '*' is not truly arch-agnostic. avocado-cli-install.sh:4-18 is a byte-identical copy of the same block.
  • Bisectability: 03a770c adds the hard exit 1 and the rpi4 leg, but the packagegroup arrives in 215e2f3, so both legs are red at 03a770c. Cosmetic if this squash-merges.

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 "<triple>-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`.
@mobileoverlord

Copy link
Copy Markdown
Contributor Author

Thanks — the verification work here saved me time, particularly confirming packagegroup-cross-canadian isn't pulled in transitively by the rust packagegroup. Fixed in a4910bf.

The stamp finding

The symptom is real and I hit exactly the dead end you describe. But I don't think folding ext.sdk.packages into compute_sdk_input_hash fixes it, because that hash is written and never compared.

  • StampComponent::Sdk appears nowhere outside stamps.rs.
  • check_prerequisites passes &[] as current_inputs (prerequisites.rs:127), so validate_stamp takes the current_inputs: None branch and only presence is checked.
  • The only callers that build a CurrentInput are ext/build.rs:320, ext/image.rs:322, and runtime/build.rs:282, all for Extension/Runtime components. None for Sdk.
  • Outside stamps.rs, the only is_current call is rootfs/install.rs:585.

So a warm SDK's install stamp never goes stale on a config change today, whatever we put in the hash. Adding ext.sdk.packages to it would be correct-looking and inert — and worse, it'd read as if the invalidation problem were handled. Making the SDK stamp actually hash-validated is a real change with its own blast radius (every ext build on an untouched project would start re-running sdk install), and it isn't this PR.

Took your second option instead, since it's the part that reaches the user:

Error: cross compiler 'aarch64-avocado-linux-gcc' not found in /opt/_avocado/.../usr/bin/aarch64-avocado-linux
The SDK is missing the C cross-canadian toolchain. An SDK installed
before it was added to avocado.yaml will not have it -- reinstall with:
    avocado sdk install --force

Worth noting avocado sdk install installs extension SDK deps unconditionally (install.rs:1758) — there's no stamp gate on that path — so re-running it does pick the packagegroup up. The gap was purely that nothing told you to.

Nits

All three taken, and 1 and 2 turned out to be the same fix — moving the guards above the .cargo/config.toml write is also what lets $SDKTARGETSYSROOT be guarded before its first use:

: "${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}"

For nit 3, the check now resolves what cc resolves — first token of $CC, falling back to <triple>-gcc only when $CC is unset — so it stops verifying a binary the build might not use, and stops aborting a working clang SDK:

CC_BIN="${CC:-${CROSS_COMPILE}gcc}"
CC_BIN="${CC_BIN%% *}"

Simulated all of it against a fake SDK layout: unset env, partially-set env, missing compiler, happy path with a flag-carrying $CC, and a clang-only bindir. Both abort paths now leave no .cargo/config.toml behind (I'd reproduced your stale-file case too).

The armv7 glob is a real bug and I'd rather fix it where both copies live — avocado-cli-install.sh:4-18 is byte-identical, as you noted. Separate PR.

Targets

Swapped the raspberrypi4 leg for qemuarm64 rather than dropping it. Keeping real hardware out of this repo's CI: qemuarm64 is aarch64/cortexa57, so it reproduces the host/target mix-up identically — the wrong pick still goes through qemu-user and still fails every C probe. Confirmed packagegroup-cross-canadian-avocado-qemuarm64 is in the 2024 feed's sdk/qemuarm64 repo.

On the release matrix: you're right that this merge doesn't unblock rubicon. extension-release.yml publishes with --targets "${{ inputs.target }}", so the feed is keyed per-target — a qemuarm64 publish wouldn't cover raspberrypi4 either. Which targets we publish for is a product decision I'd rather make deliberately than smuggle into a compiler-path fix. This PR fixes the cause and proves it stays fixed; shipping an rpi4 artifact is a follow-up. Filing it against references#23.

@jetm jetm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the delta since 60953ec (the compiler-guard hardening and the qemuarm64 leg). Three blocking findings, all inline on the same 6-line hunk in avocado-cli-compile.sh and all sharing one fix.

Six advisory notes were withheld rather than appended here, so the blocking ones stay readable.

Comment thread avocado-cli-compile.sh
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty CC_BIN makes -x test the bindir dir, so the guard passes

When $CC's first token resolves to empty, [ ! -x "$CROSS_BINDIR/$CC_BIN" ] tests $CROSS_BINDIR itself, and -x is true for any searchable directory - so the guard silently passes on exactly the case it exists to catch.

Reproduced against this logic: with the toolchain genuinely absent and CC=" aarch64-avocado-linux-gcc -mcpu=cortex-a57" (leading space), ${CC:-...} does not fire because the value is non-empty, and ${CC_BIN%% *} yields the empty string. The control run prints GUARD FIRED; the leading-space run prints GUARD PASSED and proceeds to cargo build with CC_BIN=[]. The build then dies inside aws-lc-sys with the opaque ToolNotFound this block was added to pre-empt.

Worth noting cc-rs handles this input fine - cc-1.2.61 src/lib.rs:3224 trims and :3225 returns None on empty. Only the guard does not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a9feb3a. This is the one that mattered most — a guard that reports success on the exact condition it was added to catch is worse than no guard.

Reproduced your leading-space case first (CC=" aarch64-avocado-linux-gcc -mcpu=cortex-a57", toolchain absent → PASSED), then confirmed it fires after the change. -x on a searchable directory being true is an easy thing to write twice, so it is worth having in the thread.

Comment thread avocado-cli-compile.sh Outdated
# rest are target flags) and falls back to guessing "<triple>-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%% *}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-token CC_BIN falsely aborts on a wrapper or an absolute-path CC

CC_BIN="${CC_BIN%% *}" takes the first whitespace token unconditionally, but cc-rs passes an exact filesystem path through whole and treats a leading known wrapper as a wrapper. So a working SDK gets a hard abort whose message blames a missing toolchain.

Both reproduced with a working compiler present in CROSS_BINDIR: CC="ccache aarch64-avocado-linux-gcc ..." gives CC_BIN=ccache, and CC="/abs/path/aarch64-avocado-linux-gcc ..." gives "$CROSS_BINDIR//abs/path/...". Each prints GUARD FIRED, exits 1, and tells the user to run avocado sdk install --force - a multi-minute no-op. cc-rs handles both: lib.rs:3234 passes an exe path through, and :3262-3281 knows the wrapper list (ccache/distcc/sccache/icecc/cachepot/buildcache) and takes the second token.

There is a third divergence in the same line: %% * splits only on a literal space, so a tab-separated $CC keeps its flags attached and aborts, while cc-rs uses split_whitespace. The ${CROSS_COMPILE}gcc check this replaced was immune to all three.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a9feb3a by dropping the derivation entirely, which is the version of "resolve it the way cc-rs does" that requires no cc-rs-shaped parser in shell.

Both cases confirmed against a fake SDK with a working compiler present — CC="ccache aarch64-avocado-linux-gcc …" and CC="/abs/path/aarch64-avocado-linux-gcc …" each aborted before and pass now. Added the tab case to the same harness: also aborted before, passes now. Aborting a working SDK with a message telling the user to sit through sdk install --force was the worst of the three outcomes.

Comment thread avocado-cli-compile.sh Outdated

# Check the binary `cc` will actually run: it takes the FIRST token of $CC (the
# rest are target flags) and falls back to guessing "<triple>-gcc" when $CC is
# unset. Hardcoding gcc here would also abort a working clang SDK.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clang-SDK justification cannot occur in this SDK stack

The comment justifies reading $CC on the grounds that hardcoding gcc "would abort a working clang SDK". That case cannot arise here, so the stated benefit never materializes while the two divergences above are new costs.

openembedded-core/meta/classes-recipe/toolchain-scripts.bbclass:135 writes export CC="${TARGET_PREFIX}gcc ${TARGET_CC_ARCH} --sysroot=$SDKTARGETSYSROOT" unconditionally, and :158 writes export CROSS_COMPILE=${TARGET_PREFIX} - so $CC's first token is byte-identical to ${CROSS_COMPILE}gcc in every stock SDK. meta-clang/recipes-core/meta/clang-environment.inc only appends CLANGCC/CLANGCXX/CLANGCPP and never overrides CC, and no override of toolchain_shared_env_script exists in oe-core, meta-clang, meta-avocado or meta-peridio.

Reproduced: CC unset with only aarch64-avocado-linux-clang in the bindir still prints GUARD FIRED - it aborts the clang SDK exactly as the old check did.

All three findings sit in this one 6-line hunk and share a single fix: either resolve CC_BIN the way cc-rs does, or drop the derivation, since it is provably identical to ${CROSS_COMPILE}gcc here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken — the derivation is gone in a9feb3a, back to CC_BIN="${CROSS_COMPILE}gcc".

Your toolchain-scripts.bbclass:135/:158 reading is the part that decided it. If both come from ${TARGET_PREFIX} unconditionally then the derivation was computing a value it already had, and the clang case it was justified by aborts either way — I re-ran that one and confirmed it: clang-only bindir fires the guard identically before and after. So the stated benefit was never real and the two divergences were pure cost.

@mobileoverlord
mobileoverlord requested a review from jetm August 14, 2026 01:14
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.
@mobileoverlord

Copy link
Copy Markdown
Contributor Author

All three blocking findings are fixed in a9feb3a, and they did share one fix — the one you named second: drop the derivation. Replies are on each thread.

The deciding argument was your toolchain-scripts.bbclass reading. If oe-core writes CC and CROSS_COMPILE from the same ${TARGET_PREFIX}, the derivation was recomputing a value the script already had, and doing it with a shell parser that diverges from cc-rs in three ways. Writing a correct cc-rs-shaped resolver in POSIX shell — trim, split_whitespace, wrapper list, absolute-path passthrough — is a lot of surface to maintain for a value that is provably equal to ${CROSS_COMPILE}gcc here.

I kept your reproductions as a harness rather than reasoning about them, running each case against both the old and new logic on a fake SDK layout:

case old new
toolchain absent, CC unset (control) FIRED FIRED
toolchain absent, CC with leading space PASSED FIRED
ccache wrapper, compiler present FIRED PASSED
absolute-path CC, compiler present FIRED PASSED
tab-separated CC, compiler present FIRED PASSED
CC unset, compiler present PASSED PASSED
stock flag-carrying CC PASSED PASSED
clang-only bindir FIRED FIRED

The last row is the one worth pointing at: the clang SDK the derivation existed to protect aborts identically either way, so that justification never bought anything. Bolded cells are your three findings.

On the six withheld advisory notes — please do append them. Holding them back kept this review readable and that was the right call for a set of blocking findings, but I would rather have them than not, and they can land as a plain list with no ceremony. Same for anything you set aside on #201 through #205.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants