Skip to content

build: freestanding host-test compilation#1499

Draft
Mearman wants to merge 15 commits into
coredevices:mainfrom
Mearman:build/freestanding-host-tests
Draft

build: freestanding host-test compilation#1499
Mearman wants to merge 15 commits into
coredevices:mainfrom
Mearman:build/freestanding-host-tests

Conversation

@Mearman

@Mearman Mearman commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Host tests compile against the host libc, so they pick up different headers, different libm, different linker behaviour per platform. Freestanding compilation compiles tests with -ffreestanding -nostdlib against pblibc instead, so the test environment matches what the firmware links against and the same test binary is built regardless of host OS.

Freestanding compilation infrastructure: freestanding=True on clar() swaps the host libc for pblibc headers and adds the right compilation flags.

pblibc headers extended for freestanding builds (missing size_t, NULL, etc.), HUGE_VAL added to pblibc math.h.

Math tests migrated: test_floor, test_round, test_log, test_pow. test_log and test_pow no longer need fenv.h because the previous PR replaced the host-libm oracle with static reference tables.

String and printf include-order fix: ctype.h moved after pblibc_private.h in strtol and vsprintf (same pattern as floor.c).

String tests (memcpy, strlen, etc.) and test_sprintf migrated to freestanding.

Rename shim retired for freestanding builds: the #define fn pblibc_fn shim in pblibc_private.h is no longer needed when compiling freestanding, since there's no host libc to rename away from.

Stubs layer made freestanding-compatible (stubs_logging.h).

Util data-structure tests migrated: buffer, pstring, lru_cache, mbuf, dict.

Three service tests migrated: registry_endpoint, transcription, app_file.

Merge order: 2 (after #1497)
Depended on by: #1500

Mearman and others added 15 commits June 12, 2026 13:22
Adds a new "freestanding" build mode for clar unit tests.  Tests built
with freestanding=True are compiled with -nostdinc against pblibc headers
and the compiler's own built-in include directory, with no host libc in
scope.  This lets us prove that firmware libc implementations are correct
without mixing in host-libc behaviour at the compilation or link level.

Infrastructure added:

  wscript — discovers COMPILER_BUILTINS_INCLUDE at configure time using
    -print-resource-dir (clang) or -print-file-name=include (gcc).

  waftools/pebble_test.py — adds _add_freestanding_clar_test() and a
    freestanding=True parameter to clar() / add_clar_test().  The
    freestanding path uses -nostdinc with -isystem ordering: pblibc first,
    then compiler builtins, then the platform SDK/system include directory
    as a last-resort fallback for #include_next chains (e.g. setjmp.h).

  tools/clar/clar.c — guards host-only includes (assert.h, strings.h,
    sys/types.h, sys/stat.h, fflush) with #ifndef CLAR_FREESTANDING.
    Adds forward declarations for POSIX functions missing from pblibc
    (strcasecmp) in the freestanding branch.  Adds the freestanding
    platform-specific block (#elif defined(CLAR_FREESTANDING)).

  tools/clar/clar.py — adds a "freestanding" print_mode that selects the
    clar_sandbox_freestanding / clar_io_freestanding / clar_categorize /
    clar_mock / clar_print_freestanding module set.

  tools/clar/clar_sandbox_freestanding.c — static stubs for clar_sandbox,
    clar_unsandbox, fixture_path, and cl_fs_cleanup.

  tools/clar/clar_io_freestanding.c — freestanding I/O module included
    into clar_main.c.  Routes printf/fprintf/vprintf/vfprintf/fflush
    through freestanding_write() in shim.c.  Declares strcasecmp and
    defines FILE/stderr.

  tools/clar/clar_print_freestanding.c — print backend implementing
    clar_print_init/shutdown/error/ontest/onsuite/onabort.

  tests/freestanding/shim.c — the single host-boundary translation unit
    compiled WITHOUT -nostdinc.  Provides freestanding_write (write(2)-
    backed output), calloc (malloc+memset), realloc (size-header allocator
    so the old size is known on grow), strdup, abort (_exit(1)),
    strcasecmp, and qsort (insertion-sort stub).

  tests/freestanding/wscript_build — builds shim.c as libfreestanding_shim
    after stripping -nostdinc and -isystem flags from CFLAGS.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three header updates needed when pblibc is used as the sole libc under
-nostdinc on a non-ARM host:

stdlib.h — add calloc, realloc, qsort, abort, and strdup declarations.
  malloc and free were already present; the new functions are provided by
  tests/freestanding/shim.c in freestanding builds and by the host libc
  in regular test builds.

sys/cdefs.h — add compiler-attribute shims expected by platform SDK/libc
  headers that are reached through #include_next chains.  macOS SDK headers
  (setjmp.h, assert.h) use __dead2, __cold, __disable_tail_calls; Linux
  glibc headers use __THROW, __THROWNL, __LEAF, __NTH, __NTHNL.  All
  expand to standard GCC/Clang attributes or to nothing on other compilers.

unistd.h — replace empty stub with #include <sys/types.h> so that
  ssize_t is available.  clar_asserts.h includes <unistd.h> for ssize_t,
  and sys/types.h already defines it correctly for non-ARM hosts.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch test_floor from the standard host-test path (which linked against
host libm) to freestanding=True, so it compiles under -nostdinc against
pblibc headers only.

This proves the freestanding infrastructure end-to-end: floor.c and its
test now compile and pass with no host libc in scope, on both macOS
(arm64, Apple Clang) and Linux (x86-64, LLVM 18 in Docker).

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HUGE_VAL is the positive-infinity double constant required by
C99 <math.h>.  It was absent from the pblibc header, which caused
a compilation failure when building test_log.c under -nostdinc
(the host libm's definition is not visible in that mode).

HUGE_VAL evaluates to INFINITY on every IEEE-754 host, so the
definition is correct for both the firmware target and the host
freestanding test build.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both tests included <fenv.h> and called fesetround(FE_TONEAREST) in
their __initialize hook.  This is a host-libc header that does not
exist under -nostdinc, so it blocks freestanding compilation.

The call is a no-op: FE_TONEAREST (round-half-to-even) is the
IEEE-754 power-on default on every host the project targets (x86-64
and arm64), and the build already passes -ffp-contract=off and
-fexcess-precision=standard which remove the remaining FP-divergence
sources.  Removing it has no effect on test behaviour.

Delete the #include <fenv.h>, the fesetround call, and the now-empty
__initialize hooks from test_log.c and test_pow.c.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flip the three remaining libc/math clar() calls to freestanding=True
and drop test_libs=['m']: the product sources (round.c, log.c, pow.c,
scalbn.c, sqrt.c) are pblibc implementations compiled from source, so
the host libm is not needed.

Under -nostdinc each test resolves:
  <math.h>    — pblibc src/libc/include/math.h (HUGE_VAL now present)
  <stdint.h>  — compiler built-in dir
  clar.h      — generated dir
  pblibc_private.h — src/libc add_include

Verified 4/4 pass natively (macOS/clang) and in the pebbleos-docker
container (Linux/x86-64 amd64).

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pblibc_private.h undefines the ctype macros (isspace, isdigit, etc.)
so that test code can be redirected to pblibc_* function names.  Both
strtol.c and vsprintf.c included <ctype.h> before <pblibc_private.h>,
so the macros were undefined before the function bodies executed.

Under hosted builds this was harmless because the host libc provides
the underlying symbols after the #undef.  Under -nostdinc freestanding
builds there is no fallback: the call to isspace() / isdigit() fails
to link.

Fix: include <ctype.h> after <pblibc_private.h> so the pblibc macro
definitions are always present when the function body runs, regardless
of whether pblibc_private.h undefines and re-maps other symbols.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add freestanding=True to all string test entries:
memcmp, memcpy, memset, memchr, atoi, strtol, strcat, strlen,
strcpy, strcmp, strchr, strspn, and strstr.

strtol and atoi also require src/libc/ctype_ptr.c because strtol.c
calls isspace() whose macro expands to a reference to __ctype_data[].

test_ctype is intentionally left hosted: its test strategy compares
pblibc's ctype against the host libc as the oracle.  That comparison
is only meaningful when the host ctype.h is available; under -nostdinc
the pblibc ctype.h would serve as both sides of the comparison.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
vsprintf.c calls isdigit() via the pblibc ctype macro, which expands
to a reference to __ctype_data[].  Add src/libc/ctype_ptr.c as a
product source so the symbol is present at link time.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pblibc_private.h rename block aliased memcpy -> pblibc_memcpy and friends so
host unit tests would not collide with the host libc. Freestanding tests build
under -nostdinc with no host libc in scope, so there is nothing to collide with
and the shim is unnecessary there: gate the rename block on
!defined(CLAR_FREESTANDING), which the freestanding group defines.

For the tests to find their symbols once the shim no longer declares them, the
pblibc headers must be complete: math.h was missing pow and scalbn (both
implemented in src/libc/math), so add the declarations, and test_sprintf.c now
includes <stdio.h> for snprintf rather than leaning on the shim. The hosted test
group keeps the shim, which it still needs.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
<assert.h> from Xcode 26 SDK expands assert() using
__unsafe_forge_null_terminated, a non-standard C extension that does
not parse under -nostdinc.  Gate the include on !CLAR_FREESTANDING
and replace assert(0) with __builtin_trap() in that code path.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
<strings.h> from Xcode 26 SDK uses _LIBC_CSTR and _LIBC_SIZE pointer
annotations that cause parse errors under -nostdinc.  Gate the include
and provide a static inline ffs() wrapper around __builtin_ffs() for
the freestanding path.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…standing

Set freestanding=True on five clar() entries and add the include
paths each test needs (-I src/fw, src/libutil/includes, tests/stubs;
plus src/libos/include for mbuf which needs os/mutex.h).

Tests verified 5/5 passing natively (macOS clang) and in the
PebbleOS Docker container (Linux).

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… freestanding

Freestanding-compile these service tests, adding the required include
paths and marking them freestanding=True in the clar() declaration.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
@Mearman Mearman force-pushed the build/freestanding-host-tests branch from 56e2328 to 6f53bc1 Compare June 12, 2026 12:22
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.

1 participant