Skip to content

fix(toolchain): the compiler setting selects the compiler - #155

Open
Kartikey1306 wants to merge 9 commits into
embeddedos-org:masterfrom
Kartikey1306:fix/compiler-setting-selects-the-compiler
Open

Kartikey1306 wants to merge 9 commits into
embeddedos-org:masterfrom
Kartikey1306:fix/compiler-setting-selects-the-compiler

Conversation

@Kartikey1306

Copy link
Copy Markdown
Contributor

Closes #154

Stacked on #132; review 4054494..ec2b8f6 for this change alone — one commit, two files (+112/−5). #132 repairs master, which is red, so anything based on master inherits those reds. Merge order: #132, then this. Independent of #144/#146/#148/#151/#153.

The defect

ebuild/build/toolchain.py:77-85 used toolchain.compiler only as a key into PREDEFINED_TOOLCHAINS, then built every tool name as f"{prefix}gcc". A value that is not one of the five toolchain names gave predef = {}, prefix = "", cc = "gcc" — silently:

compiler: built with should be
clang gcc clang
cc gcc cc
arm-none-eabi-gcc gcc (host binary) arm-none-eabi-gcc

Measured on a build.yaml with compiler: arm-none-eabi-gcc, reading the generated ninja file:

before:  cc = gcc                 ar = ar
after:   cc = arm-none-eabi-gcc   ar = arm-none-eabi-ar

ebuild build printed OK toolchain gcc and Ready to flash, and file _build/fw was a Mach-O host binary. The footprint is measured with the host size too (footprint.py:108-115), so the flash/RAM numbers described the wrong architecture.

The change

compiler names the compiler binary — which is how the documentation already spells it: docs/task_cortex_r5_example.md:31-34 uses compiler: gcc with prefix: arm-none-eabi-. A predefined toolchain name remains a shorthand for a prefix and an arch.

Two details that matter:

  • A compiler carrying its own prefix supplies it to ar and objcopy. Setting only cc would leave the archive step running host ar over cross objects — a different wrong artifact rather than a fixed one.
  • An unrecognised driver is used verbatim and infers no prefix. A custom compiler then fails loudly at build time ("command not found") instead of silently becoming host gcc. I deliberately did not add a hard refusal: compiler defaults to gcc (core/config.py:85), and every example in the tree sets compiler: gcc, so refusing unknown values would reject working projects.

Tests

tests/unit/test_toolchain_compiler_is_used.py (new, 9 cases) is deliberately split in two, and the negative control shows the split is real. Against the old resolver:

FAILED CompilerNamesTheCompiler::test_clang_is_clang_and_not_gcc
FAILED CompilerNamesTheCompiler::test_cc_is_cc
FAILED CompilerNamesTheCompiler::test_a_compiler_carrying_its_own_prefix_cross_compiles
FAILED CompilerNamesTheCompiler::test_an_unknown_driver_is_used_verbatim_and_infers_no_prefix
4 failed, 5 passed

The five that pass are DocumentedSpellingsAreUnchanged — the default, None, compiler: gcc + explicit prefix, all five predefined toolchains, and an explicit prefix winning over a derived one. So the four broken behaviours change and the documented ones demonstrably do not.

Verified at ec2b8f6 with CI's own invocations: ruff check . clean, yamllint . clean, mypy . --ignore-missing-imports --no-strict-optional --exclude '^(layers|core|promo)/' no issues in 108 source files, pytest tests/ 692 passed, 1 skipped (683 before, plus these 9). The one skip is test_windows_installer.py, Windows-only, and was skipped before.

Not run here: a real ARM cross-compile — the arm-none-eabi-gcc on this machine ships without newlib, so linking fails for reasons unrelated to this change. What is verified is the resolved toolchain and the generated ninja file, which is where the defect was.

CI -- ebuild has been red on master since the 09-08 batch merge, and the
first failing step (ruff) has hidden the ones behind it.

Lint (ruff, all nine Test legs):
- test_build_dir_resolution.py imported shutil twice (F811).
- test_package_recipe.py lost its trailing newline (W292).
- test_ci_gate.py had `import itertools` / `import re` two hundred lines
  down (E402) -- my own embeddedos-org#103, replayed onto a file that had moved.
  These three hunks are byte-identical to embeddedos-org#122's, so either PR merging
  first leaves the other clean.

Type check and tests (never reached on master since 09-08):
- ebuild/packages/index_sync.py calls PackageRecipe.to_dict(), which embeddedos-org#111
  defined and embeddedos-org#112 -- merged five minutes later from a base without it --
  deleted in its replay. mypy names it once; pytest fails nine
  test_index_sync cases with AttributeError. The method is restored verbatim
  from embeddedos-org#111 (cc90078): it emits the `package:`/`build:` keys parse_recipe()
  reads back, which an asdict() replacement would not.

Vendored core drift:
- embeddedos-org#109 (dba3d83) edited core/eos/docs/three-way-alignment.md, a vendored copy
  pinned to eos 5544c98, so drift went 44 -> 45 and the guard failed as
  designed. Reverted to the pinned content (blob 7f9c8c1, the same bytes as
  eos:docs/three-way-alignment.md at the pin). The alignment note belongs in
  ebuild's own docs or upstream in eos, not in the snapshot.

OSSF Scorecard:
- ossf/scorecard-action@v2.4.0 pulls gcr.io/openssf/scorecard-action, and
  gcr.io now refuses the pull ("requires billing to be enabled"). v2.4.3
  pulls from ghcr.io; eos already pins it and its Scorecard job is green.

Not in this PR: EoSim Sanity's Windows/macOS legs install a wheel that has
never been published; embeddedos-org#121 (srpatcha) already replaces that with the clone
the other legs use.

Verified locally: ruff clean, yamllint clean, mypy clean over 107 files,
680 passed / 1 skipped, scripts/check_vendor_drift.py 44/44 and 46/46.
…/3.11, yamllint on Windows

Both surfaced on this branch's first CI run, once ruff let the job get past
its first step.

- ebuild/plugins/__init__.py: on Python 3.10 and 3.11 the stubs type
  entry_points() as the deprecated mapping, and its .get() wants an
  EntryPoints default, so mypy fails with arg-type. The line carried a
  '# type: ignore[attr-defined]' -- the wrong error code, so it suppressed
  nothing. Spelled out with a cast, byte-identical to embeddedos-org#122's hunk (54605f0).

- .yamllint.yml: the Windows runners check out with core.autocrlf=true, so
  every YAML file arrives as CRLF and the default new-lines: unix rule
  rejected every line. The step was added on 09-03 and had never passed on
  that leg. new-lines: platform accepts the checkout's own convention.
…e checkout's line ending

new-lines: platform was the wrong fix. The Windows runners' autocrlf turns
LF files into CRLF -- except a file that already carries a stray CR, which
git leaves alone, and auto-assign.yml had one on its last line. So under
'platform' Windows expected CRLF and got LF on that file's first line, and
the leg was red again for the opposite reason.

Pin *.yml and *.yaml to eol=lf so every OS lints the same bytes, keep
yamllint's default unix rule, and drop the stray CR.
…h a round-trip test

PackageRecipe.to_dict() was written before install_args existed and was
never taught about it, so a recipe that went through index_sync came back
from the cache with install_args empty while every other field survived.
It also returned the recipe's own list objects, so a caller that appended
to what it got back edited the recipe behind its back.

install_args is now emitted after build_args, matching the order
parse_recipe() reads them, and every list field is copied on the way out.
index_sync's entry-to-recipe mapping carries install_args too; without
that the field could not arrive from an index at all.

The new round-trip test builds a recipe with every field set and asserts
parse_recipe(safe_load(safe_dump(to_dict()))) equals it; against the
previous to_dict() it fails on install_args. A second test checks the
lists are copies, and test_index_sync gains a case that an index entry's
install_args reaches the cached YAML.
actions/checkout, ossf/scorecard-action and codeql-action/upload-sarif
were referenced by moving tags. A tag can be re-pointed; a commit cannot,
and Scorecard itself flags unpinned actions. Each is now pinned to the
commit its tag resolved to on 2026-09-14, with the tag kept in a trailing
comment, the same shape linked-issue.yml already uses.
The changelog records what the master repair changed and why. CONTRIBUTING
gains a note for Windows contributors: .gitattributes now pins YAML to LF,
but the attribute governs future checkouts and commits, not files already
in a working tree, so an existing clone needs one git add --renormalize .
(or a fresh clone) before yamllint stops seeing CRLF.
…working tree

git add --renormalize . re-applies the clean filter to the index; it never
rewrites files, so a clone with CRLF YAML still has CRLF YAML afterwards
(reproduced in a scratch clone with core.autocrlf=true: two CRs before,
two after, status clean). What re-checks the files out is
git rm --cached -r . && git reset --hard HEAD, or a fresh clone. CONTRIBUTING
and the changelog now say that. The changelog entry also lists every file
it describes and the plugins/__init__.py type-check fix it had left out, and
the to_dict() comment no longer claims to follow parse_recipe()'s order.
The review at 43b0337 found the one-line assertion pinning install_args
next to build_args and, six lines above it, recipe.py's docstring saying
key order does not matter. Both are true: order is not a correctness
property, because parse_recipe() reads every key by name, and it is a
stability property, because index_sync writes the dict as cached YAML that
humans diff. The assertion now says which of the two it is guarding and
what to do when a reordering is deliberate, so a future failure reads as
intentional rather than as a mystery.

No behaviour change. ruff clean; pytest 683 passed, 1 skipped.
resolve_toolchain() read toolchain.compiler only as a key into
PREDEFINED_TOOLCHAINS and then built every tool name as f"{prefix}gcc".
A value that is not one of the five toolchain names gave prefix "" and
cc "gcc", with no warning: `clang` and `cc` built with gcc, and
`arm-none-eabi-gcc` -- the spelling the field name invites -- produced a
host binary while reporting "Ready to flash". The footprint was then
measured with the host size, for the wrong architecture.

compiler now names the compiler binary, which is how the documentation
already spells it (compiler: gcc alongside prefix:, in
docs/task_cortex_r5_example.md); a predefined toolchain name stays a
shorthand for a prefix and an arch. A compiler that carries its own
prefix supplies it to ar and objcopy too, so the binutils cannot be left
running host tools over cross objects. An unrecognised driver is used
verbatim and infers nothing, so a custom compiler fails loudly at build
time instead of silently becoming host gcc.

Measured on a build.yaml with compiler: arm-none-eabi-gcc:

  before: cc = gcc                ar = ar
  after:  cc = arm-none-eabi-gcc  ar = arm-none-eabi-ar

Against the old resolver the four new CompilerNamesTheCompiler cases
fail and the five DocumentedSpellingsAreUnchanged cases pass, which is
the split this change intends.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 97.50000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ebuild/build/toolchain.py 94.73% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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.

toolchain.compiler is discarded unless it names a predefined toolchain: clang builds with gcc, and a cross config emits a host binary

2 participants