Skip to content

build: migrate remaining tests to freestanding compilation#1500

Draft
Mearman wants to merge 26 commits into
coredevices:mainfrom
Mearman:build/freestanding-nonlibc-tests
Draft

build: migrate remaining tests to freestanding compilation#1500
Mearman wants to merge 26 commits into
coredevices:mainfrom
Mearman:build/freestanding-nonlibc-tests

Conversation

@Mearman

@Mearman Mearman commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Three more test migrations to freestanding compilation, building on the freestanding-host-tests branch.

blob_db_sync migrated to freestanding.

Nine remaining util tests migrated (utf8, crc, ring_buffer, etc.).

test_time migrated to freestanding.

Merge order: 3 (after #1499)
Depends on: #1497#1499

@Mearman Mearman force-pushed the build/freestanding-nonlibc-tests branch from 76292ed to 270e26f Compare June 12, 2026 08:41
Mearman and others added 25 commits June 12, 2026 12:28
Document the goal of compiling host unit tests under -nostdinc against
pblibc's own headers so the host libc is never in scope, the work
already landed in this track (fno-common, include-order fix, FP
determinism flags, reference-table math tests), and the next step:
a freestanding compilation group with a thin host-abstraction shim.

Captures the open questions (fenv.h, clar stdio back-end, transitive
system-header pulls) and explains why the migration is a separate piece
of work rather than part of this change.

Linked from docs/index.md toctree.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
The design note described freestanding compilation as the next step; it is now
implemented for the libc math/string/printf groups, and the rename shim is
retired for them.

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>
Move the test_blob_db_sync clar() entry to freestanding=True so it
compiles -nostdinc against pblibc rather than the host libc.

Include path additions required:
- tests/overrides/default  (cmsis_core.h override)
- src/fw, src/libos/include  (system headers)
- src/libutil/includes  (list_* declarations)
- include  (sdk include root)
- tests/fakes, tests/stubs, tests  (test helpers)
- third_party/freertos and portable/GCC/ARM_CM3  (FreeRTOS headers
  pulled in transitively via os/tick.h)

Added defines=["CONFIG_PLATFORM_GABBRO=1"] to satisfy
applib/platform.h, and use=["libutil"] to satisfy the list_*
symbols used in ram_storage.c and fake_blobdb.c.

Also set __DARWIN_UNIX03=1 in src/libc/include/sys/cdefs.h for
macOS SDK compatibility: without it, the SDK's assert.h fallback
macro uses __unsafe_forge_null_terminated, a clang pointer-safety
extension that is not available in all build modes.  Setting the
flag causes assert.h to use the safe __assert_rtn() path instead.

Verified: tests pass 1/1 natively (macOS) and in the Linux
container (ghcr.io/coredevices/pebbleos-docker:v5).

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert test_base64, test_graphics, test_hdlc, test_ihex,
test_legacy_checksum, test_shared_circular_buffer, test_sle,
test_stats, and test_stringlist to freestanding=True in
tests/fw/util/wscript_build.

Each entry gains freestanding=True and appropriate add_includes
so the test and its sources build under -nostdinc against pblibc:

- test_base64: adds src/libc/ctype_ptr.c for __ctype_data (used by
  ctype.h macros in base64.c) and src/libc to the include path so
  the UNITTEST branch of ctype_ptr.c resolves <include/ctype.h>.
- test_shared_circular_buffer: adds src/libutil/list.c and
  src/libutil/platform.c to provide list_* and util_assertion_failed.
- test_stats: adds src/libutil/sort.c for sort_bubble.
- test_sle: uses tests (not tests/stubs) so the test-side
  "stubs/stubs_passert.h" include resolves with its prefix intact.

Also adds #include "util/attributes.h" to src/fw/util/graphics.h so
ALWAYS_INLINE is defined when that header is pulled in under -nostdinc.

test_rand is left hosted: its tinymt submodule is not checked out so
the source cannot be compiled.

All nine tests verified natively (macOS) and in the Docker container
(ghcr.io/coredevices/pebbleos-docker:v5, linux/amd64). All pass.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guard host-only includes (<time.h>, <stdlib.h>) in fake_rtc.h and
fake_rtc.c behind #ifndef CLAR_FREESTANDING so pblibc headers satisfy
the translation units under -nostdinc. Add FreeRTOS include paths via
add_includes so time.c compiles (configTICK_RATE_HZ from FreeRTOSConfig.h
is only needed for time_get_uptime_seconds, not exercised by this test).

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old size-header trick prepended a size_t before the returned
pointer so realloc() could discover the old allocation length. This
broke whenever product code called kernel_realloc() (which calls
shim realloc) and then kernel_free() (which calls host free):
free() received an interior pointer rather than the raw malloc
block, triggering an allocator assertion.

Replace the size-header approach with malloc_size (macOS) /
malloc_usable_size (Linux) to query the old size, keeping the
returned pointer as a plain malloc block compatible with free().

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test_attribute.c only exercises attribute.c with pblibc-compatible
stubs, making it a clean freestanding candidate. Add freestanding=True
and the required add_includes so the test compiles under -nostdinc.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
localtime_r is a POSIX function that pblibc does not provide. Under
-nostdinc the call fails to compile. Return NULL as a safe no-op stub
when CLAR_FREESTANDING is defined so the stub header is usable in
freestanding test builds.

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

Set freestanding=True on the test_timeline_resources.c clar() entry.
The only host-libc obstacle was stubs_syscalls.h calling localtime_r;
that is guarded in the preceding commit.

The entry adds the include paths that the hosted dummy_board build
received automatically: platform SDK headers (freertos, libos, uPNG),
the default override directory (font/shell auto-headers), and the
obelix resource override (resource_ids.auto.h). CONFIG_PLATFORM_EMERY=1
is injected to satisfy platform.h, which the default platform build
does not set.

UUID functions (uuid_equal, uuid_is_system, uuid_to_string) come from
src/libutil/uuid.c. Listing use=['libutil'] pulls in the pre-compiled
static library that already contains those symbols and a weak rand32
stub, so no tinymt dependency is introduced.

Passes natively (macOS arm64) and in the pebbleos-docker:v5 container
(Linux amd64) with a clean build.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert the following tests from standard hosted compilation to
freestanding (nostdinc against pblibc):

  test_compass_cal, test_evented_timer, test_vibe_score,
  test_vibe_score_info, test_touch, test_light, test_phone_pp,
  test_phone_call, test_accel_manager, test_audio_endpoint,
  test_app_glance_service

Each entry now carries freestanding=True, explicit add_includes
covering pblibc, src/fw, src/libos, src/libutil, tests/fakes,
tests/stubs, tests/overrides/default, and the FreeRTOS headers
(where the service under test pulls in kernel/events.h or os/mutex.h).
Tests that need a board override place tests/overrides/dummy_board
first so it shadows src/fw/board/board.h. Platform-dependent code
that requires CONFIG_PLATFORM_EMERY and PBL_DISPLAY_WIDTH/HEIGHT
carries those defines explicitly. Libutil is linked where list_*
or util_assertion_failed symbols are needed.

Skipped (host libc dependency, cannot compile under -nostdinc):
  test_shared_prf_storage_v3 — fake_spi_flash.c uses stat/fopen/fread
  test_weather_service        — same dependency on fake_spi_flash.c
  test_app_cache              — fake_spi_flash.c + test uses <stdio.h>
  test_voice_endpoint         — tinymt submodule not initialised in
                                this worktree (pre-existing build gap)

All 11 migrated tests verified passing on macOS (native) and in the
ghcr.io/coredevices/pebbleos-docker:v5 container.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert test_cron, test_contacts, test_music_endpoint,
test_debounced_connection_service, test_timeline_item,
test_do_not_disturb, test_migrate_wakeup, test_vibe,
test_vibe_intensity, and test_hrm_manager from hosted
to the freestanding group (compiled -nostdinc against pblibc).

Tests using the full PFS/flash stack need CONFIG_FLASH_QEMU=1
in their defines; the freestanding path in _add_freestanding_clar_test
does not inject platform flash defines, so they must be explicit.

Guard fake_spi_flash_populate_from_file under #ifndef CLAR_FREESTANDING
since it calls stat/fopen/fread which are not in pblibc. The function
is never called by any of the migrated tests.

All 10 tests pass both natively (macOS) and in the Linux container.

Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Mearman Mearman force-pushed the build/freestanding-nonlibc-tests branch from 270e26f to eb28bec Compare June 12, 2026 11:29
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Joseph Mearman <joseph@mearman.co.uk>
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