Repository navigation
Add C/C++ bundle support with preprocessor include graph analysis - #71
ChristopherTuring wants to merge 3 commits into
Conversation
`stasis bundle` takes C/C++ entries (.c/.cc/.cpp/.cxx/.c++, or a header .h/.hh/.hpp/.hxx/.h++) and walks their `#include`/`#import`/`#include_next`/ `#embed` lines as GCC and Clang search for them: the includer's directory for "x", then -iquote, -I, -isystem, -idirafter. Directives are found past comments, literals (raw strings, digit separators) and line splices; `#if 0` code is skipped, an include under any other conditional is followed but not required, and a file's include guard is no condition. Headers hold code, so an include lands on the header, and a bundled header pulls in its implementation file (same name beside it, or in the src/ mirroring its include/): main.c -> util.h -> util.c. That edge is a guess, so nothing an implementation file lacks is fatal. The search path comes from --include-dirs (-I for every unit) or --compile-commands (a compile_commands.json: each unit's own -I/-iquote/ -isystem/-idirafter/--embed-dir/-include). A quoted include the tree holds above its includer stops the build, naming the -I it needs; one found nowhere (a system header in quotes, a generated one) is reported; <x> found nowhere is a system header. Nothing outside the root is read; a symlink out of it and a .env are refused. Files of a GitHub git submodule are bucketed as a `github` dependency, as Solidity's are; edges are keyed under "c", files tagged c/cpp/c-header/cpp-header, #embed files as resources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
…therwise A header's implementation is found by name (util.h -> util.c), which misses libraries whose sources aren't named after their headers: from Node.js's src/node_main.cc, 78 of its 221 sources and nearly all of V8 and OpenSSL weren't reached. A small table of known links, matched from a library's root wherever it is vendored, fills that in: Node.js's embedder API (node.h -> src/api/*), its bindings (every source registering one through NODE_BINDING_CONTEXT_AWARE_INTERNAL/NODE_BINDING_PER_ISOLATE_INIT) and the headers it implements across files; V8's API (include/v8*.h -> src/api/*); OpenSSL's subsystems, TLS and providers; libuv, uvwasi, c-ares, llhttp, nghttp2/nghttp3/ngtcp2, brotli, zstd, zlib and ICU. From node_main.cc the bundle now reaches 8,807 files instead of 1,587: 218 of Node.js's 221 sources (the rest only the build picks), all of libuv, 1,054 of OpenSSL's 1,164 and 869 of V8's 1,264 sources. An implementation edge is now keyed by the file's path from the header's directory (`impl ../src/api/api.cc`), as two links of one header can share a name (libuv's src/unix/fs.c and src/win/fs.c). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
…eir builds scope them A tree vendoring projects stasis knows -- Node.js and its deps/, V8 and its third_party/, OpenSSL, libuv, ICU, zlib, c-ares, nghttp2/nghttp3/ngtcp2, Brotli, Zstandard and some 20 more -- is now searched with no -I given. A directory holding a project's marker files is its root; each project has its own -I directories and those it exports (from Node.js's gyp files). A unit's search path is its innermost project's own directories, then the exports of every other project but those around it, nearest first. So Node.js's sources find deps/zlib and V8's its own third_party/zlib, and ICU's "util.h" is ICU's: a single merged -I list resolved those to the wrong copy. Node.js's deps/openssl wraps OpenSSL's tree with its own directories; where a project carries configurations for several platforms, Linux x86-64's is taken. --include-dirs come first and a compile command replaces it for the units it lists. `stasis bundle` says which projects it found. Bundling a Node.js checkout from src/node_main.cc with no flag reaches 8,949 files: every file the hand-built merged list did, and 104 more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 56b367948c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for (const { flag, path, dir } of ctx.forced) { | ||
| const spec = `${flag} ${path}` | ||
| let result = isAbsolute(path) ? probe(makeDir(baseDir, realBase, path), '.') : probe(dir, path) | ||
| if (result === null && !isAbsolute(path)) result = resolveInclude(node, { directive: 'include', form: 'quote', path }) |
There was a problem hiding this comment.
Skip the source directory for forced includes
When a compile command uses -include or -imacros, the named file is absent from the command's working directory, and a same-named file exists beside the translation unit, this fallback resolves to that source-local file. GCC specifies that the working directory is searched instead of the main source's directory, followed by the remainder of the quote search chain (GCC preprocessor options); calling resolveInclude() here reintroduces the source directory and can silently record and bundle a different header from the one the compiler uses.
Useful? React with 👍 / 👎.
| if (edge.result?.refused !== undefined) { | ||
| missing.push({ spec: edge.spec, from: node.file, reason: edge.result.refused }) | ||
| seen.add(id) | ||
| } else if (edge.result === null && (edge.forced || (edge.inc.form === 'quote' && edge.inc.directive !== 'include_next'))) { |
There was a problem hiding this comment.
Fail when a live embed resource is absent
When an unconditional #embed "asset" cannot be resolved and no ancestor directory happens to contain that path, this generic quoted-reference branch classifies it as merely unfound, so buildCBundle emits a warning and produces a supposedly complete bundle without the required bytes. Unlike a quoted header, an embed has no implicit system-header fallback—GCC searches only the source directory and --embed-dir directories (GCC binary resource inclusion)—so an unresolved live #embed must be added to missing.
Useful? React with 👍 / 👎.
| function evalCondition(expr) { | ||
| const m = /^\(*\s*(\d+)[uUlL]*\s*\)*$/u.exec(expr.trim()) | ||
| return m ? Number(m[1]) !== 0 : undefined |
There was a problem hiding this comment.
Recognize non-decimal integer conditions
For an always-true condition written as a hexadecimal or binary integer literal, such as #if 0x1, this decimal-only regex returns undefined and marks the branch as maybe. Consequently a live unresolved include in that branch is neither required nor even reported by the definite-edge pass, allowing an incomplete bundle to succeed; parse the standard integer literal bases rather than only \d+.
Useful? React with 👍 / 👎.
This PR adds support for bundling C and C++ projects by analyzing their preprocessor include graphs without compilation.
Summary
Introduces a new C/C++ loader (
stasis/src/loaders/c.js) that builds bundles from C/C++ source files by:#include,#import,#include_next, and#embeddirectives-iquote,-I,-isystem,-idirafterdirectories)src/directory)#if/#ifdef/#ifndef) to track live, maybe, and dead code regionscompile_commands.jsonfor per-translation-unit include search pathsKey Changes
New loader module (
stasis/src/loaders/c.js, ~916 lines):scanIncludes(): Parses C/C++ source respecting translation phase 4 (line splicing, comment/literal skipping)collectCBundle(): Builds the include graph with GCC-style path resolutionloadCompileCommands(): Readscompile_commands.jsonfor per-file include flagsdetectProjects(): Identifies known projects and their include structuresBundle command integration (
stasis/src/cmd/bundle.js):buildCBundle()function to create C/C++ bundles--include-dirsand--compile-commandsCLI flagsc-bundlefor C projects,cpp-bundlefor C++ projectsComprehensive test suite (
tests/c-loader.test.js, ~445 lines):#include_next)Test fixtures (
tests/fixtures/c-bundle/):#embeddirective supportDocumentation updates:
doc/file-formats.mdto document C/C++ bundle format and resolutiondoc/bundle.mdto mention C/C++ bundle supportstasis/bin/stasis.jsNotable Implementation Details
#if/#ifdef/#ifndefnesting with three states (live/maybe/dead) to include directives that might be compiled in some configurations#ifndef X/#define Xpatterns as include guards, treating their body as unconditionalR"delim(...)") which can contain unescaped quotes and backslashes.c/.cpp) for bundled headers, enabling complete dependency trackinghttps://claude.ai/code/session_01Hg9tYjaNiR19bvkZUJP8hH