Goose

Builds and packages

How goose build replaces the hand-written C compiler invocations, and how a program pulls in libraries, Goose or C, from git repositories. This is the design and the migration plan, written against the tree at de24a156, from the discussion that settled the decisions in §1; §10 is the order it was implemented in, one working commit at a time. The user's side of it is docs/packages.md.

1. Decisions

2. What there is today

A JIT run is goose foo.goose. An AOT build of the same program is

goose --emit-runtime goose_runtime.c && cc -O2 -c goose_runtime.c
goose -o foo.c foo.goose && cc -O2 foo.c goose_runtime.o -o foo -lm -pthread

and with graphics, physics and ui on top

cc cube.c goose_runtime.o -o cube @$(goose --ui-link cc) @$(goose --gfx-link cc) @$(goose --physics-link cc)

in that order, because GNU ld resolves static archives left to right. The knowledge of how to do this lives in scripts/toolchain.py, which the test, sample and benchmark runners share: it finds MSVC through vswhere and imports the vcvars64 environment, finds gcc and clang on PATH, spells flags per driver style, emits the runtime once and compiles it per configuration, and appends the response files CMake wrote. The compiler itself never starts a process.

A native stdlib module (gfx, audio, physics, ui) touches eight places: stdlib/<m>.goose, the C layer in src/<m>/ with an X-macro list of its functions in <m>_api.h, cmake/<m>.cmake, a submodule under third_party/, src/<m>.h with have_<m> and the response-file lookup, symbol registration in jit.h, prefix sniffing of gs_<m>_ in codegen_calls.h and typecheck.h (which native layer a program calls, and the main-thread-only rule for thread_fn), and the runners' skip logic. The JIT reaches a layer's functions by address, through tcc_add_symbol on functions linked into goose itself, which is why a layer the compiler was not built with cannot be run. The layers never call back into the program: gs_gfx_error fills a caller's slice. So they can be shared libraries.

import a.b; resolves a/b.goose against the root program's directory, then the stdlib; import .a.b; against the importing file. A library using the first form would resolve against the program. All names are public, and namespaces are global, so two libraries declaring namespace util; collide. embed_shader is the only compile-time file read. There is no compiler version number; the runtime's text hash gates linking.

3. The command line

goose <verb> [options] file.goose [-- program args]. If the first argument is not a verb, the command line is read exactly as today, so goose foo.goose, goose -o foo.c foo.goose, goose --emit-runtime r.c and the rest keep working; goose foo.goose is goose run foo.goose.

Verb Does
run fetches what the root declares, builds the native parts the program reaches, compiles the program in-process (TinyCC) and runs it; -O1 by default, as today
build the same, then writes the C, compiles it and links an executable beside the root source (foo, foo.exe); -O2 by default, Goose's inlining, while the C compiler optimizes at every level
check fetches the Goose sources it needs and typechecks; never builds C
fetch fetches every package the root declares, and builds nothing; --update re-fetches refs that are not commit hashes
toolchain prints the C compiler, cmake and git it would use, and why, or what it could not find; --json for scripts
version the compiler's version (§11: a commit count, when there is one) and the runtime hash

Options of build: -O0/-O1/-O2 (Goose inlining; the C compiler optimizes at every level), --debug (-DGS_DEBUG=1 for the generated C, debug information and no optimization for the C compiler; packages stay release builds), --standalone (no runtime object), -o <exe>, --cc <name|path> (§6.1), --dry-run (print every command and write nothing), -v (print every command as it runs), and the ones shared with the plumbing: -D, --include, --stack-reserve, --stdlib, and --offline (fetch nothing: a package not in the cache is an error at its import, which fetch resolves; what an editor's checks pass). run takes the same except -o, --standalone and --debug.

4. Packages

4.1 Terms

A package is a directory with an entry file <name>.goose at its root, optionally a sidecar <name>.build beside it (§4.7), optionally C sources or a CMake project (§5), and whatever else it wants. A program is a package whose entry is its root file, package root its directory, sidecar foo.build. A module is one .goose file; import pkg.sub; is the module <pkg root>/sub.goose. The standard library is a package found as today (--stdlib, GOOSE_STDLIB, then beside the compiler); its modules that have native parts move out of it (§7), the pure ones stay.

A package's name is the name the root imports it by. It names the cache directory and the entry file, and by convention the namespace its files declare, which nothing enforces.

4.2 Declaring a dependency

import gfx from "aardappel/goose_gfx@v1";                 // GitHub, tag v1
import mylib from "github.com/joe/mylib@3f2c1a9";         // a host, a commit
import other from "https://git.example.com/x/other.git@main";  // any git URL, a branch
import local from "../local";                             // a path, relative to this file

import name from "source"; declares package name. from is read as the contextual identifier it already is in return … from f. The source is one string:

The grammar line becomes

import := "import" "."? ident ("." ident)* ("from" strlit)? ";"

4.3 Root only, and flat

A sourced import is honored only in the root file. Anywhere else, a sourced import is a requirement: compiled as part of a program, the root must declare a package of that name, or the error names the fix:

joe/mylib needs the package vec_extras: add
    import vec_extras from "github.com/x/vec_extras@v2";
to foo.goose (packages are declared by the root file only)

The same name from a different source is an error naming both. The same source at a different ref is a warning naming both, since the root is the one that knows what works together. A requirement's package is loaded only when the root declares it; nothing is fetched because a library asked.

So a program's dependencies are flat and visible: the root lists everything, and a library documents its needs in a form the compiler reads. A library's examples and tests are roots of their own, so the same line in them is honored there. Should transitive dependencies ever be wanted, the lines are already in place: a goose deps add or an --auto-deps would do what the error message says, with no change of format.

Two packages' C parts cannot depend on each other (§5.5); a library's C dependencies (SDL inside the gfx package) are inside the library, invisible to the root.

4.4 Resolving import

For import a.b; (no source) in a file of package P:

  1. the file a/b.goose under P's root (import a; is a.goose); for a program this is the root's directory, which is what happens today;
  2. a package named a that the root declared, or a blessed name (§4.5): <a's root>/b.goose, and import a; is <a's root>/a.goose;
  3. the standard library's a/b.goose.

A file under P's root with the name of a package the root declared explicitly is an error (the root meant the package); a local file still shadows a blessed name or a stdlib module, as it shadows the stdlib today. import .a.b; is relative to the importing file, unchanged.

Spec §11.1 changes accordingly: "resolved relative to the importing file's package root (the root file's directory for the program itself), then among the root's declared packages, then in the standard library".

4.5 Blessed packages

A table in the compiler, src/blessed.txt, generated into the binary by CMake and read by CMake for §7:

# import-name  package  source                             ref
physics        physics  github.com/aardappel/goose_physics  v1
gfx            gfx      github.com/aardappel/goose_gfx      v1
audio          gfx      github.com/aardappel/goose_gfx      v1
ui             gfx      github.com/aardappel/goose_gfx      v1

A naked import gfx; that step 1 of §4.4 does not satisfy and the root did not declare is read as if the root had written the table's line; import audio; is module audio of package gfx. A root may still declare one of these names itself with any source, to try a fork or another ref: an explicit declaration always wins over the table and over a compiled-in copy. The pins move with the compiler: when a blessed package's API moves, the compiler that expects the new API bumps the ref.

4.6 Fetching and the cache

GOOSE_HOME, default ~/.goose (%USERPROFILE%\.goose on Windows):

pkg/<host>/<owner>/<repo>/<ref>/                 the checkout, submodules included
build/pkg/<host>/<owner>/<repo>/<ref>/<toolchain>/   its native part built: objects or archives,
                                                 the JIT shared library, link-cc.rsp, link-msvc.rsp, logs
build/pkg/local/<hash of the absolute path>/<toolchain>/   the same for a path source
build/runtime/<runtime hash>/<toolchain>-<config>/goose_runtime.o
build/prog/<stem>-<hash of the root's absolute path>/   a program's C, objects, logs
config                                           optional settings (§6.1)

A tag or branch is git clone --depth 1 --branch <ref> --recurse-submodules --shallow-submodules <url>; a commit hash is git init, git fetch --depth 1 origin <hash>, git checkout FETCH_HEAD, then the submodules. A fetch lands in <ref>.tmp and is renamed when complete, so an interrupted one leaves nothing half there. Fetches and updates take a cross-process lock on the persistent <ref>.lock file beside the checkout; after waiting, a fetch rechecks the cache before touching <ref>.tmp. run, build and check fetch what is missing with one line of notice per package; fetch fetches everything up front. A checkout at a hash or tag is never touched again; fetch --update re-fetches branch refs. Path sources are used where they are.

Without git on PATH:

cannot find `git` on PATH, needed to fetch the package gfx from
github.com/aardappel/goose_gfx@v1. Install git, or clone that repository
yourself and declare it in foo.goose as
    import gfx from "<the directory>";

Building a fetched package runs its C compiler, and a CMake package runs CMake: code from the repository runs on the machine at build time, as with every language's build scripts. Pinning a commit hash is what protects against a tag moving under you.

4.7 Layout and the sidecar

A library repository, minimally:

mylib.goose          the entry: `namespace mylib;`, its declarations, its requirements
mylib.build          the sidecar, when the package has a native part
sub.goose            further modules, `import mylib.sub;`
src/*.c, include/    its C, if any
examples/, test/     programs that are roots of their own

The sidecar is parsed by the Goose lexer, so it looks like Goose: // comments, key = value;, strings, true/false, integers and [a, b, c] lists. Unknown keys are errors naming the known ones.

Key In Meaning
c_sources both C files, relative to the package root, compiled by the discovered C compiler (§5.1); a single-header library is one impl.c that defines its implementation macro and includes the header
c_include both include directories
c_defines both NAME or NAME=VALUE
c_flags_cc, c_flags_msvc both extra compile flags per driver style
c_libs both system libraries, by name (m, user32); spelled per style by the compiler
cmake_target both the package root holds a CMake project, and this is the static library target to build and link (§5.2)
cmake_options both NAME=VALUE cache entries for its configure
main_thread_only both its functions may not be reached from a thread_fn (§5.4)
exclude both directories, relative to the package root, whose .goose files are not the package's own: the JIT library's symbol table (§5.3) looks for extern fns in neither these nor a directory below the root that holds a sidecar of its own
output program the executable's name, relative to the root's directory

Every c_* key takes a _windows, _linux or _macos suffix for a platform-only part. A program's sidecar carries its own C the same way; a program importing a library's C through --include keeps working, since the header is spliced into the generated C as today.

5. Native parts

A package with any c_* key or a cmake_target has a native part, built once per toolchain (§6.1) under build/pkg/…/<toolchain>/, release configuration, static C runtime on Windows, in two forms: what an AOT program links, and a shared library the JIT loads. The first run or build that reaches the package builds the first, and the first run the library, from the same objects; check never builds. Inputs are stamped: a path package rebuilds when any of its C sources (by path, modification time and size), its sidecar or the set of its extern fns changes, and every package, a git checkout included, when goose.h does: the header the compiler gives native C for the values it cannot declare itself, a builder's gs_rref (spec §7.10). Every program and process that reaches a package builds it in that one directory, under a cross-process lock on its persistent build.lock: one that waited checks the stamp again before building anything, and the response file and the stamp, which a build that finds the stamp current reads without the lock, are written beside and renamed into place, the stamp last.

5.1 Plain C sources

Each file of c_sources is compiled to an object with the package's includes, defines and flags, named for the file's name and a hash of its path, so that two of one name in different directories do not share one. The AOT form is the objects themselves, listed in the response file ahead of c_libs: no archiver is needed. The JIT form links the objects, goose_symbols.c (§5.3) and the system libraries into goose_jit_<name>.{dll,so,dylib} through the same driver (-shared, or /LD). Only .c is compiled this way; C++ goes through CMake.

5.2 CMake packages

For cmake_target, the compiler writes a wrapper project into the package's build directory and configures and builds that, never the package's own CMakeLists directly:

cmake_minimum_required(VERSION 3.20)
project(goose_pkg_physics C)
# C++, and on macOS Objective-C and Objective-C++, where there is a compiler.
include(CheckLanguage)
check_language(CXX)
if(CMAKE_CXX_COMPILER)
    enable_language(CXX)
endif()
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded")
add_subdirectory("<package root>" pkg EXCLUDE_FROM_ALL)
if(NOT TARGET <cmake_target>)
    message(FATAL_ERROR "... names the CMake target <cmake_target>, which ... does not define")
endif()
# The JIT library: the symbol table pulls what it names out of the target.
add_library(goose_jit_physics SHARED goose_symbols.c)
target_link_libraries(goose_jit_physics PRIVATE <cmake_target>)
# A probe whose link line is the package's whole link closure.
add_executable(goose_linkprobe goose_linkprobe.c)
target_link_libraries(goose_linkprobe PRIVATE <cmake_target>)

configured with -DCMAKE_BUILD_TYPE=Release, the discovered C and C++ compilers, the cmake_options, and a generator chosen as Ninja if on PATH, else the platform default; built with --config Release. The configure command that last succeeded is kept beside the build; any other one (an edited cmake_options, or a configure that failed) configures again from a fresh cache. The wrapper and its two C files are written only when they change, so that a build with nothing new compiles and relinks nothing: on Windows a JIT library another process has loaded cannot be written over.

The response file for AOT programs is the link closure of <cmake_target>: its archive, every static library it depends on, and the system libraries and link options behind them (-lm, -framework Cocoa, user32.lib), in order. CMake has no generator expression for a transitive closure, which is what today's cmake/sdl.cmake digs out of SDL's collector target by hand. The general answer is CMake's file API: the compiler writes .cmake/api/v1/query/codemodel-v2 into the build directory before configuring, and reads the reply's target object for goose_linkprobe, whose link.commandFragments are the link line, roles libraries, libraryPath, frameworkPath and flags, as the generator would pass them; paths relative to the build directory are made absolute. That needs a small JSON reader in the compiler. Anything with a CMakeLists becomes a Goose package this way, including an upstream library bound directly (cmake_target = "box3d"), with no CMake written for Goose's sake.

Without cmake on PATH:

cannot find `cmake` on PATH, needed to build the C part of the package
physics (github.com/aardappel/goose_physics@v1, target goose_physics).
Install CMake 3.20 or later.

5.3 The symbol table and the JIT

The JIT needs the address of every C function the program calls. Today jit.h gets them from the layer's X-macro list, which is why only compiled-in layers can be run. Instead, the compiler generates, from the package's own declarations, a C file every package's JIT library is linked with:

/* goose_symbols.c, generated for the package physics. */
typedef struct { const char *name; const void *addr; } goose_symbol;
void gs_phys_create_world(void); void gs_phys_step(void); /* … */
GOOSE_EXPORT const goose_symbol *goose_symbols(void) {
    static const goose_symbol table[] = {
        { "gs_phys_create_world", (const void *)&gs_phys_create_world },
        { "gs_phys_step", (const void *)&gs_phys_step },
        /* … */
        { 0, 0 } };
    return table;
}

The names are every extern fn's C symbol in every .goose file under the package root (but for the sidecar's exclude directories and those below the root holding a sidecar of their own, §4.7), which the compiler parses for this (dummy prototypes: taking a function's address needs no signature). One exported symbol per library, so Windows needs no export lists. Linking the table pulls exactly the objects that define those functions, and their dependencies, into the shared library.

A JIT run then, for each package whose extern functions the program's code reaches (codegen already collects usedexterns; each extern's declaring file knows its package), loads goose_jit_<name> with LoadLibrary or dlopen, calls goose_symbols, and hands every entry to tcc_add_symbol before relocation, where AddGfxSymbols and its siblings do it from static tables today. A package compiled into the compiler (§7) has the same table linked in under the name goose_symbols_<name>, listed in a generated builtin_packages.h: a builtin package needs no library, a declared one always goes through its library. NativeLayers and the gs_<m>_ prefix sniffing go away.

A package may declare an extern fn only for a function its own C part, or the C library, defines. The runtime's gs_os_* and gs_sqrt belong to os and math, which stay in the stdlib; a package naming one would fail to link its JIT library, and the error says which symbol and that it is the runtime's.

5.4 Main-thread-only packages

main_thread_only = true; in the sidecar is what the hard-coded prefix list in CheckThreadGlobals becomes: a call from a thread_fn to an extern declared in such a package is the same error as today, naming the package. gfx, audio, physics and ui all set it.

5.5 Rules

6. Building programs

6.1 Toolchains

The order: --cc, then GOOSE_CC, then cc = "…"; in GOOSE_HOME/config (the sidecar format), then discovery:

One command finds its toolchain once and builds everything with it: the native parts, the runtime object and the link. "On PATH" is the PATH the compiler was started with: building a CMake package on Windows imports the vcvars64 environment, which puts cl on PATH, and discovery finds the same toolchain whether that happened earlier in the process or not.

--cc takes a name (msvc, clang-cl, clang, gcc) or a path. Each toolchain has a style, msvc or cc, which decides the spelling of every flag and which response file a package's build writes, exactly as toolchain.py's CC class and --gfx-link msvc|cc divide them today; and an id, <name>-<version>-<8 hex of the driver's path and style>, which names the cache directories. cmake and git are found on PATH, and on Windows cmake also where Visual Studio installs it. goose toolchain prints all of it.

Processes are started by a small cross-platform runner (src/process.h: CreateProcessW, posix_spawn), output captured to a log file in the build directory and shown on failure with the command that failed; -v shows every command, --dry-run only prints them. While they run, src/progress.h keeps a person informed without flooding a log: each step that does real work (a fetch, a package's native part, the runtime object) prints one line, and only where stderr is a terminal does a live line, redrawn in place, show the running command, its time and the last line it printed. GOOSE_PROGRESS=plain|live overrides the terminal check.

Without any C compiler, on Windows:

cannot find a C compiler (looked for cl, clang-cl and clang on PATH and in
Visual Studio, and for gcc and cc on PATH); install Visual Studio with C++
or LLVM, or set GOOSE_CC

6.2 The runtime object

build/runtime/<runtime hash>/<toolchain>-<config>/goose_runtime.o, written from --emit-runtime and compiled on first use, as toolchain.py's GooseRuntime does; the hash is RuntimeVersion(), so a new compiler never links a stale object. --standalone skips it.

6.3 What goose build does

  1. Parse the root; fetch declared packages that are missing (§4.6); parse the rest; typecheck; generate C into build/prog/<stem>-<hash>/foo.c.
  2. For each package with a native part that the program reaches, build it if it is not built for this toolchain (§5).
  3. Compile the runtime object if missing (§6.2) and the program's own c_sources, if any.
  4. Compile foo.c and link: the program's objects, the runtime object, the packages' response files in link order, -lm -pthread where the style needs them; /Fe: or -o to the executable beside the root source, or output from the sidecar, or -o.

--dry-run prints steps 2 to 4 as the commands they are, with the response files' paths, for a Makefile or CMake project to copy. The flags that exist today, -o foo.c, --emit-runtime, --gfx-link and the others, remain as the plumbing for the same purpose; --<m>-link becomes a thin spelling of "the response file of package m" and is kept until the runners move (§10).

6.4 goose run with packages

The same up to step 2, then the program is compiled in-process and the packages' JIT libraries are loaded (§5.3). A thread_fn program still needs build (TinyCC cannot place thread-local storage in memory, unchanged).

6.5 embed_file

embed_file("path") -> const u8[:]: the file's bytes, read when the program is compiled, resolved relative to the calling file like embed_shader's file form, emitted into static data the same way, each distinct file once. The argument is a string literal or a let/const global holding one, under embed_shader's rules. It is how a package ships a font or a table without a runtime file, which resource_dir() cannot locate for a library.

7. The compiler's own build

7.1 Builtin packages

GOOSE_BUILTIN_LIBS is a CMake cache variable: empty by default, all, or a list of blessed package names. For each, CMake reads src/blessed.txt and

FetchContent_Declare(goose_pkg_gfx
    GIT_REPOSITORY https://github.com/aardappel/goose_gfx
    GIT_TAG v1 GIT_SHALLOW TRUE GIT_SUBMODULES_RECURSE TRUE
    SOURCE_DIR "${CMAKE_BINARY_DIR}/pkg/github.com/aardappel/goose_gfx/v1")
FetchContent_MakeAvailable(goose_pkg_gfx)

fetches it at configure time with CMake's own fetcher (no Python, no goose yet) into the cache layout of §4.6 under the build directory, adds its project, links its cmake_target into goose, generates its symbol table (the same goose_symbols.c, produced by a CMake regex over extern "…" fn and extern fn name lines, since no compiler exists yet to parse them), and writes link-cc.rsp and link-msvc.rsp under pkg/…/<config>/ through the same wrapper and file-API probe as §5.2 (cmake/package.cmake, shared by the two). The compiler is built knowing that directory, as it knows GOOSE_GFX_LINK_PATH today, and looks there, then beside its own binary, for a builtin package's response file, as it does for the package's sources. A builtin package for a plain-C sidecar is the same minus add_subdirectory: CMake compiles c_sources itself.

FETCHCONTENT_SOURCE_DIR_GOOSE_PKG_GFX points a build at a local checkout, which is also how a package is developed against the compiler.

The release binaries and CI configure with all; a developer's first cmake -B build builds the compiler alone in a fraction of the time, and the graphics samples still run, after one fetch and build into ~/.goose, shared by every worktree.

A release (.github/workflows/release.yml, laid out by scripts/package_release.py) has no build tree to look in: each builtin package is under pkg/<name>/ beside the compiler, its sources without their vendored dependencies, and the static libraries of its link closure in lib/, which its response files there name relative to themselves. A link takes a copy of those under GOOSE_HOME/build/builtin/ with the paths made absolute, one per place the compiler is unpacked.

7.2 What stays in the compiler

embed_shader, src/shaderc.c and third_party/cute_spirv: the shader compiler is a compiler feature any graphics package may use. The blob it produces is a contract between compiler and gfx package: src/gfx/gfx_blob.h moves to src/gfx_blob.h, the package carries a copy, and the header gains a version constant the layer checks when it reads a blob. SDL_GPU's binding rules, checked at compile time, stay where they are.

7.3 TinyCC

The submodule becomes a copied subtree at the same path, third_party/tinycc: the sources cmake/libtcc.cmake builds (libtcc.c, tcc.c, the .c and .h of the x86-64, i386 and arm64 targets, tccpe.c, tccmacho.c, include/, lib/, win32/include and win32/lib), VERSION, COPYING, RELICENSING, and a README recording the upstream commit and how to update, like third_party/cute_spirv/README.md. GOOSE_LIBTCC and GOOSE_HAVE_LIBTCC go; have_jit is true; the no--o-writes-the-C fallback and the runners' have_jit probe go. The compiler builds on x86-64 and arm64, which is where it has ever been tested.

8. Testing

9. Documentation

Spec §11.1 (package roots, the sourced import, the resolution order, flat requirements), Appendix D (the grammar line), §12 (embed_file); implementation.md §1 (the verbs, process.h, toolchain.h, packages.h), §7 (the symbol table replacing the X-macro registration); stdlib.md (gfx, audio, physics and ui documented in their repositories, linked); testing.md; README.md (build with -DGOOSE_BUILTIN_LIBS=all, goose build foo.goose in place of the cc lines); samples/README.md; tutorial.md where it builds; the design docs of the moved packages go with them; vscode/: a Build command calling goose build.

10. The plan

Each step is one commit, or a few, and leaves every test passing. Steps are ordered so that nothing is built twice and the old path keeps working until the new one covers it.

  1. Vendor TinyCC and make the JIT mandatory (§7.3). Delete the submodule entry and the GOOSE_LIBTCC option; remove every have_jit branch, the .c-fallback and the runners' probe. CI drops nothing else yet.
  2. Verbs and the process runner. src/process.h; Main dispatches on a verb and otherwise reads the command line as today; goose run, goose check, goose version (runtime hash only). No new behaviour.
  3. Toolchains and goose toolchain (§6.1): src/toolchain.h, the discovery order, the vcvars import with its cache, GOOSE_HOME and GOOSE_HOME/config.
  4. goose build for what exists: the runtime object cache (§6.2), the program's C, the link, -o, --standalone, --debug, -v, --dry-run, with the native modules linked through today's response files found as --gfx-link finds them. From here goose build samples/27_gfx_cube.goose works. The README shows it; the runners are untouched.
  5. Package roots (§4.4 step 1): the driver gets Package records, every source carries its package, imports resolve against the importing file's package root. No visible change for today's programs.
  6. Declared packages from path sources (§4.2, §4.3, §4.4): the parser's from form, root-only honoring, requirements and their diagnostics, import pkg.sub;, the collision rule. test/packages/ begins. Spec §11.1 and Appendix D updated.
  7. Git sources and the cache (§4.6): fetch, auto-fetch, --update, the git error. The offline file:// test.
  8. embed_file (§6.5), independent of everything else; it can land anywhere from here on.
  9. The sidecar and a program's own C (§4.7, §5.1, §5.3): the sidecar parser, c_* keys on the program, the generated symbol table, the JIT shared library and its loading. The program is the first package with a native part, before any library is.
  10. Plain-C packages (§5.1): the same per package, cached per toolchain, link order. A fixture with a .c file, JIT and AOT.
  11. CMake packages (§5.2): the wrapper, the file-API probe and the JSON reader, the response files. A fixture with a tiny CMakeLists, skipped without cmake.
  12. Builtin packages and the blessed table (§4.5, §5.4, §7.1): src/blessed.txt, GOOSE_BUILTIN_LIBS, cmake/package.cmake with the CMake-side symbol table, builtin_packages.h, main_thread_only. The four in-tree modules are re-expressed as packages in place first: packages/physics/ holding physics.goose, physics.build, a CMakeLists.txt and src/, the Box3D submodule still under third_party/, with blessed.txt pointing at the in-tree path. The layer code moves, the behaviour does not, and cmake/physics.cmake, src/physics.h, the prefix sniffing and jit.h's registration go, one module per commit: physics, ui, audio, gfx.
  13. Move physics out to aardappel/goose_physics: the repository holds what packages/physics/ held plus the Box3D submodule, api_check.py and its design doc; blessed.txt points at it; the in-tree copy and the submodule go; CI configures with GOOSE_BUILTIN_LIBS=all.
  14. Move ui, then gfx and audio the same way, into goose_gfx (ui's place there, or a repository of its own, decided when it moves). The SDL and Nuklear submodules go, the blob header gets its version constant, the Linux build dependencies in ci.yml stay because the builtin build needs them.
  15. The runners call the compiler: toolchain.py asks goose toolchain --json instead of discovering, and the samples runner builds with goose build; the test runner keeps driving the C compiler itself where it needs per-level flags, a second front end or multi-program batches. --<m>-link is retired. The VS Code extension gets Build.

11. Deferred and open