# Project builds A Nupp project is a `nupp.lua` manifest naming where source lives, what the project depends on, and which targets it produces. `nupp build` compiles the whole source set into the outputs those targets describe. ```lua return { include = { "src" }, build = { default = "app", targets = { app = { kind = "modules" } } } } ``` ## Source sets Running `nupp build` without source arguments loads `nupp.lua` and compiles the project's source set: every `.nupp` under the manifest's include roots, minus the build output. Explicit source builds remain available as `nupp build file.nupp`. A `modules` build executes nothing, so it needs no entry and compiles the whole source set. A bundle, binary or component requires `entries` to say where execution starts and which chunk becomes the body. Entries do not say what exists. A build compiles what the project is written in, the way a compiler compiles a source set, rather than walking `require` edges out from an entry. ::: deepdive The walk answers three questions at once and gets two of them wrong: - a module nothing requires goes unchecked, so `nupp check` stops meaning "this project is well typed" and starts meaning "the part I could reach is"; - a module reached only through `require(name)`, where the name is computed, is absent from the build and therefore from any binary, and fails in front of a user with a filesystem search path that means nothing inside one file. Removing unused code is a linker's job and should be invisible when it happens. Measured on this compiler, the walk was removing one module out of seventy-one and costing about sixteen kilobytes on a 1.6 MB binary. ::: ### Target source sets A module or bundle target may narrow the initial source set with `sources`. Files and directories use the same recursive expansion as a documentation target: ```lua targets = { compiler = { kind = "bundle", entries = { "compiler.browser" }, sources = { "src/compiler/browser.nupp", "src/runtime/portable" } } } ``` The target checks and packages every selected `.nupp`, `.g.nupp`, and `.lua` file. It also adds dependencies reached through a constant `require`, even when they sit outside the selected paths. Omitting `sources` retains the whole project source set. A computed `require(name)` cannot add a source during the build. Its eventual module must have been selected independently, or the built program reports an ordinary module-not-found error when that call runs. The build does not claim that every possible computed name is present. Every source entry must match at least one compilable file inside the project. The normalized set participates in the target cache key. ## Targets and outputs A target names its kind, where execution starts, and the inputs it needs: ```lua build = { outDir = "build", default = "app", targets = { app = { kind = "modules", description = "Build the application", entries = { "app.main" }, resources = { "src/app/*.d.nupp" }, dependencies = { "fast", "codec" } } } } ``` Entries are optional for a `modules` target and required for a bundle, binary, or component. They may be module names or `.nupp` paths. Generated Lua preserves module paths beneath `outDir`, so `app.main` becomes `build/app/main.lua`. ### Dialect selection Every target resolves one source-lowering dialect. `luajit` is the default. `luajit-compat` lowers newer LuaJIT syntax while retaining LuaJIT's FFI, JIT, and native representations. It is for LuaJIT hosts whose runtime facilities are available but whose parser does not meet Nupp's default floor. `lua51` may be selected on the build table, inherited by its targets, or overridden by one target: ```lua build = { dialect = "lua51", targets = { portable = { entries = { "lib.main" } }, native = { entries = { "app.main" }, dialect = "luajit" } } } ``` `nupp build --dialect lua51` and `nupp check --dialect lua51` override the selected target for that invocation and also work with explicitly named source files. The resolved value appears in build and check JSON and in `nupp tasks`. It is part of the cache key, so artifacts and checks from different dialects cannot satisfy one another. The `lua51` checker requires a supported target representation for every reached construct. Service facades resolve their implementations when required. See [portable libraries](portability/libraries.md) for typed providers, target dependencies, and setup entry modules. A target's `dependencies` are names, declared once at the top level of the manifest and shared by every target that lists them: ```lua dependencies = { fast = { kind = "c", sources = { "native/fast.c" }, bindings = { header = "native/include/fast.h" } }, codec = { kind = "cargo", manifest = "native/codec/Cargo.toml", library = "codec" } } ``` Each kind has its own keys: see [C dependencies](#c-dependencies), [Rust dependencies](#rust-dependencies), [rock dependencies](#rock-dependencies), and [type dependencies](#type-dependencies). Dependency acquisition and usage are separate: target dependencies ship with the target, `compileDependencies` are visible only while compiling, and a dependency selected by `generators.*.using` is a host tool. See [Service Providers](service-providers.md) for generators, runtime service lookup, and the compatibility rule for ambient type dependencies. `nupp test` uses the default target and bundled runner. The optional `test` action names a different target to build first and command to run: ```lua test = { build = "app", argv = { "nupp", "test-runner" } } ``` See [testing.md](testing.md) for what that command is handed and how its results are reported. ### Resource destinations Resource globs preserve paths relative to the nearest include root. A resource table gives one source an explicit target-relative destination when its runtime lookup is relative to another module: ```lua resources = { { source = "src/public/schema.nupp", output = "app/data/schema.nupp" } } ``` ## Manifest validation The manifest is validated before builds, checks, tests, and task queries. Validation covers dense string arrays, required target inputs, supported dependency kinds, named target and dependency references, and dependency cycles. A target's `dialect` is `"luajit"`, `"luajit-compat"` or `"lua51"`. Configuration errors name the invalid field before any build work starts. Every table in the manifest takes a closed set of keys, and one that is not in it is refused by name, with the nearest spelling when there is one: ```text nupp: build.targets.site has no key "custmCss"; did you mean "customCss"? ``` The sets cover the top level, the build section, every target, a docs target's pages, `test`, `tasks`, `selfHost`, `fmt`, and each dependency. A dependency is checked against the keys its own kind reads, since what a C build takes and what a rock takes have almost nothing in common. Keys beginning with `_` are the build's own, folded in from a command's options; a manifest has no reason to write one. ::: deepdive A key nothing reads would otherwise take effect silently, which is the one way a configuration file can lie to the person who wrote it: `custmCss` rendered a site with the default theme and exited cleanly. Refusing the whole manifest costs one error message on a typo and removes a class of bug whose only symptom is that a setting appears not to work. ::: ## Listing targets `nupp tasks` lists the manifest's build targets, default or configured test action, self-host and fixpoint action, and any named `tasks` entries, and marks the default build target. `nupp tasks ` prints the effective target configuration, including manifest-level defaults such as `outDir`. Both forms accept `--format json`, or `--json`, for build-tool integration; text is the default. ```bash nupp tasks nupp tasks app --json nupp task docs-serve ``` A named task also runs with `nupp task `. See [tasks.md](project-tasks.md) for the manifest shape a task takes. ## Removing build output `nupp clean` removes the output paths of every configured target; `nupp clean --target ` limits removal to one target. `--dry-run` prints the paths without changing them. Clean rejects absolute paths, parent traversal, and the project root before removing anything. A multi-platform binary also accepts `nupp clean --target --platform `; `--platform all` and an omitted platform remove all platform outputs owned by that target. A platform option without a target is refused. ## Compiler-native features Compiler-provided native APIs do not appear in `dependencies`. Their resolved uses record effects while Nupp checks the target's complete source set, and the build stages the matching providers automatically. For example, [](nupp.io.path) spells and transforms paths in Nupp, while calls that reach its working-directory and canonicalization provider record `native.path` and select the generic filesystem feature in `build/lib/nupp_native_v2`; a target with no resolved native use does not build or retain the library at all. The global [`nupp` standard-library namespace](../runtime/data/standard-library.md) itself is always created by generated code. Nested members use the same exact resolution. `nupp.uuid.v4()` selects UUID support, while an alias such as `local uuid = nupp.uuid` followed by `uuid.v4()` selects the same feature without also selecting JSON or UTF-8. Files and filesystem-backed path operations use the Rust-native provider; whole-file transfers and processes share its Tokio executor and use bounded queues. HTTP uses Reqwest over Tokio and Rustls, URI uses Rust's `url` parser, and UUID uses the Rust-native provider. Built-in message digests are written in Nupp and stage no native artifact. Installed digest services may bring their own declared native dependencies. The Rust facilities share the versioned `build/lib/nupp_native_v2` sidecar. Generated or external C interop builds its own declared native dependencies; there is no unversioned compatibility provider beside the Rust provider. Each provider is built with the union of its selected features. Pure facilities such as buffers, checksums and `nupp.math` emit their Lua adapters but stage no native artifact. At `-O1` and above the build recomputes these effects from the post-folding tree, so a use found only in a constant-dead branch or loop is removed with that code. The registry also recognizes compiler-provided modules. `require("lpeg")` selects native LPeg 1.1, while `require("re")` selects the bundled official Lua frontend and implies LPeg. Every `nupp.peg` matcher also selects LPeg: Nupp may emit a faster kernel for a recognized static graph, but the typed matcher shell and general lowering share the same native feature. A local table named `nupp`, or a computed `require`, does not claim a compiler feature: only the resolved global path and literal module name do. ### Feature overrides Detection is the default, not a requirement to configure every target. A target may override one answer when it deliberately supplies or forbids a provider: `true` forces inclusion, `false` forces removal, and an absent name keeps the detected answer. ```lua nativeFeatures = { json = true, lpeg = true, uuid = true } ``` The forceable binary feature names are `json`, `lpeg`, `path`, `uri`, `uuid`, `files`, `process`, `workers`, and `http`. The registered module effects include `nupp.codec.json`, native `lpeg`, and the Lua `re` module that requires it. Bundled LuaRock modules are checked too, so Lunamark contributes LPeg even when application source does not require it directly. Forced removal is an expert escape hatch: if reachable code still requires that provider, the resulting program fails at runtime in the usual way. ### Platform builds A binary target may use `stub = "nupp"` to ask the source compiler to build its own host with exactly the resolved host features. A path-valued `stub` remains a prebuilt or third-party artifact and is never silently relinked. The same compiler-owned target can name catalog platforms: ```lua platforms = { "x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-pc-windows-msvc" } ``` Build one with `nupp build --target app --platform ` or all in manifest order with `--platform all`. A multi-platform default output is `///`; `platformOutputs` may map configured triples to custom raw paths. POSIX platforms also own a deterministic `.tar` which records mode `0755`. An explicit macOS result is unsigned and build JSON reports `distributionReady = false` with a signing notice. Sign it on macOS with `codesign --force --sign - ` for local execution. The release workflow uses Developer ID signing and notarization when its optional Apple credentials are configured; otherwise its macOS archive remains unsigned and says so. Platform selection sets `layoutTarget` for that build and separates its cache and completion state. The selected compiler-owned catalog stub satisfies every resolved feature that has a host implementation, so files and process do not stage a current-machine sidecar beside a foreign executable. Sidecar-only features such as path, URI, UUID and HTTP are refused until the catalog has a provider artifact for that platform. ### Standalone native binaries A compiler-owned binary can relink its host with the target's static native closure before stamping the payload: ```lua build = { kind = "binary", stub = "nupp", standalone = true, entries = { "app.main" }, dependencies = { "image" } } ``` `nupp build --target app --standalone` enables the same mode for one build. Nupp compiles source C dependencies as static archives, rewrites their generated bindings to the executable's process namespace, emits native AOT as a static archive, and force-loads the complete archive closure into the host. The result does not carry those libraries under `lib`. An explicitly shared C dependency is refused in this mode; use `linkage = "static"` or `"both"`. Native facilities which only have a sidecar implementation are refused for the same reason. A path-valued third-party `stub` cannot be relinked and therefore cannot select `standalone`. ### Static AOT components An embedded host that owns its final executable can consume a component's AOT code as an archive instead of carrying a shared-library sidecar: ```lua build = { kind = "component", entries = { "game.main" }, aot = "require", aotLinkage = "static", } ``` `aotLinkage` is `"shared"` by default. That route writes a shared AOT library beside the component and the generated binding opens it at runtime. `"static"` writes an AOT archive under `outDir/lib` for the embedding application's link; the generated binding instead resolves ordinary kernels from the process C namespace. It does not make Nupp produce or relink an executable. Choose `"shared"` for a file-based application or a component that must be updated independently of its host. Choose `"static"` only when the host owns the final link and deliberately has no dynamic-loader dependency. The host must force-link and retain the archive, export its AOT symbols to the embedded LuaJIT default namespace, and use the same target ABI as the component. Static archives are source-qualified because every linked component shares one C namespace. A component whose AOT entry constructs Lua tables or strings also carries a Lua C-module registrar. Its host registers that archive before it loads the component; see [Embedding Nupp](embedding.md#static-aot-components). Pure numeric and span kernels need no registrar. A static component cannot make a sidecar-only native provider available: its native dependencies must already be linked into, or otherwise supplied by, the host. A static build writes two more things beside the archive. `outDir/aot/archive.c` defines one exported probe, `ks_aot_archive_`, returning a number derived from the archive's contents, and every rewritten module checks it before anything else it declares. A name that does not resolve reports that the archive was not linked into the host; a value that disagrees reports that the archive which was linked is a different build of the component. Both are load errors naming the cause, in place of the `dlsym` failure an unretained archive otherwise produces on whichever kernel happened to be declared first. `outDir/aot/link.json` is the handoff for whoever performs that link. It names the component and target, the archive, the probe symbol and its expected value, every exported kernel and registrar symbol, the builder registrations the host owes, and the retain and export flags the target's linker takes, with `` standing for wherever the archive ends up: ```json { "schemaVersion": 1, "component": "gameScripts", "target": "aarch64-apple-darwin", "archive": "lib/libgameScripts_aot.a", "fingerprint": {"symbol": "ks_aot_archive_a91c2e...", "value": 54690558639838}, "retain": { "forceLoad": ["-Wl,-force_load,"], "export": ["-Wl,-export_dynamic"] } } ``` Each entry in `builders` names both a `key` and the `symbols` behind it, because they are not the same string: the host calls a tier-spelled registrar symbol and registers what it returns under the unsuffixed key, which is what the generated wrapper looks the table up under. A target whose VM uses a vendor static symbol registry instead of a linker option has no retain flags to give; `symbols` is then the list that registry must contain. `aot = "emit-c"` with static linkage writes the same C units, probe, and link manifest without compiling any of them. ### Target capability profiles A target profile says what a destination admits, as distinct from what its pointers are: whether it has a dynamic loader, a tracing JIT, working FFI callbacks, a VM that resolves default-namespace symbols out of the process image, and a toolchain that produces static AOT archives. Every publicly modelled triple has a built-in profile and answers yes to all of them, except `wasm32-unknown-emscripten`, which has no tracing JIT and no native archive. The profile is what static linkage is checked against. A build that selects `aotLinkage = "static"` for a target whose profile does not produce static archives, or whose VM does not resolve symbols out of the process image, is refused with that reason rather than producing an archive nothing can use. Three source constructs are refused the same way, as [NUPP2904](../../reference/diagnostics.md). `@jit` asserts that a tracing contract exists, so a target with no trace compiler rejects it instead of quietly rereading it as advice. A `cdef ... from "name"` asks the platform to load a library, so a target with no loader rejects it; the same C is reachable through the default namespace, which is what a static link puts there. An `ffi.cast` to a function type asks the VM for a callback trampoline, so a target that allocates none rejects it — ahead of the `unsafe` question, because `unsafe` says the author accepts what a callback costs rather than that the destination can make one, and unlike the `jit-callback` lint beside it this cannot be waved away with `@allow`. A target nothing describes refuses nothing. A vendor describes a private target in its compiler pack, so nothing about a confidential platform has to reach the public distribution catalog. `pack.json` takes an optional `profile`: ```json "profile": { "layoutModel": "aarch64-unknown-linux-gnu", "os": "linux", "capabilities": { "dynamicLoader": false, "tracingJit": false, "ffiCallbacks": false, "staticSymbolResolver": true, "staticAot": true }, "link": {"forceLoad": [], "export": []} } ``` Every capability must be stated: an omitted one is one nobody verified, and defaulting it to the permissive answer is the mistake profiles exist to prevent. `layoutModel` names an already modelled triple, so admitting a target does not also open the set of layout models. A descriptor wins over the built-in answer for the same triple, because a vendor port of a public triple is still that vendor's port. Current-platform source builds use the repository toolchain driver. Installed and cross-target builds use a compiler pack selected by host and target triple. Installed distributions discover packs under `lib/nupp/compiler-packs` beside their `bin` directory. `NUPP_COMPILER_PACK_DIR` overrides that location with a pack tree containing `//pack.json`; Nupp verifies the recorded size and SHA-256 of its compiler, archiver, and host linker before running them. An explicit `NUPP_NATIVE_CC` or dependency `cc` remains the expert override and the ambient compiler search remains the compatibility fallback when no pack directory is configured. Tagged Linux x86-64 and Windows x86-64 archives carry their matching native pack under `lib/nupp/compiler-packs`, and the release publishes the same tree as a separate pack archive for an existing installation. Nupp finds the bundled tree both after a conventional `bin`/`lib` installation and while the release archive is being run directly. Release CI poisons ambient compiler names and requires the installed pack to build and run one standalone target containing both generated C FFI and AOT code before either archive is published. The macOS arm64 release does not carry a compiler pack. Apple does not permit a release to redistribute the Xcode SDK that a complete pack would require, so a macOS standalone native source build currently uses locally installed Xcode command-line tools. Ordinary stamped binaries and target-indexed prebuilt static C artifacts do not acquire that source-build requirement. Cross-target packs are not yet release artifacts. `pack.json` has `schemaVersion = 1`, `host`, `target`, `version`, authenticated `cc` and `ar` tool records, optional `cxx` and `linkHost` records, and `compileFlags`/`linkFlags` arrays for its sysroot. Each tool record contains a pack-relative `path`, `sha256`, and `size`. `linkHost`, when present, accepts `FEATURES OUTPUT ARCHIVE... -- LINK_FLAG...`; it owns one retained Rust application-host archive containing the entry point, VM, exact-feature native provider, and platform SDK linkage. Platforms force-load that archive where safe; Windows selects its one-codegen-unit export surfaces normally after the application archives so unused Rust and system import objects stay unlinked. `{pack}` inside a compile or link flag expands to the selected pack directory, so a sysroot remains relocatable after the archive is installed elsewhere. ### Native artifacts Native artifacts are sidecars for modules targets and ordinary prebuilt stubs. Ship the target's `lib` directory with a binary unless its selected stub links the provider itself; a Lua payload cannot embed a shared library. A one-file `bundle` target rejects providers that need sidecars. Host-supplied modules such as LPeg remain ordinary `require` dependencies and must be available in the runtime that loads the bundle. See [distribution.md](../../reference/distribution.md#limits) for what a stamped binary can and cannot carry. ## Documentation targets A `kind = "docs"` target runs the parse-only documentation generator through the same `nupp build --target` interface: ```lua docs = { kind = "docs", sources = { "src" }, format = "both", -- site, markdown, or both outDir = "build/docs", title = "Project API", includePrivate = false, github = "https://github.com/example/project", logo = "images/project.svg", public = "docs/public", customCss = "docs/site.css", constructorPattern = "^new", pages = { { glob = "docs/**.md" } } } ``` The keys the target itself reads: | Key | Effect | | --- | --- | | `sources` | Roots the API reference is read from | | `format` | `site`, `markdown`, `json`, or `both` | | `outDir` | Where the rendered output is written | | `title` | The site's own title | | `name` | Brand name beside the logo, when it differs from `title` | | `description` | One line for the home page and the `llms.txt` index | | `github` | Repository link in the header | | `logo` | Image for the header brand, replacing the default mark | | `favicon` | Icon linked from every page | | `public` | Directory copied to the output root, for images and downloads | | `customCss` | Stylesheet appended after the default theme | | `lexers` | Directory of project Scintillua lexers, searched before the bundled ones | | `includePrivate` | Renders the declarations privacy rules hide | | `constructorPattern` | Lua pattern a constructor's last name segment matches | | `pages` | Handwritten pages: a `glob` over a tree, a `directory`, or one `source` at one `path` | | `diagnostics` | The generated diagnostic index, and the page it is appended to | | `stdlib` | The generated LuaJIT standard library page | | `dependencies` | Rocks to install before rendering, `lunamark` among them | The appended `customCss` overrides the documented `--nuppdoc-*` custom properties without changing other documentation targets. A page entry also takes `redirects` for the routes it used to answer at, and `layout = "home"` for a landing page, whose hero and feature showcase are written in the page's own Markdown. See [doc.md](../tooling/documentation.md) for doc comments, page syntax, privacy rules, and what each output format writes. ### Page trees A page entry may name a `glob` instead of a `path` and a `source`. It then stands for every Markdown file the pattern matches, each published where it sits: `docs/learn/projects/build.md` answers at `guides/build`, and an `index.md` names the directory holding it rather than a route ending in `index`. `base` is the directory routes are named from, defaulting to the fixed part the pattern opens with, and `exclude` drops files the tree holds and the site does not publish. ```lua { glob = "docs/**.md", exclude = { "docs/style.md" } } ``` What a path cannot say, the page says in a frontmatter block of `key: value` lines between `---` fences: | Field | Effect | | --- | --- | | `order` | Where the page sits in the navigation | | `title` | What navigation calls it, when its heading is not what to call it | | `redirects` | Routes it used to answer at, separated by commas | | `layout` | The layout it renders under, `home` being the one that differs | The sidebar follows every segment of a route, in the order their first page appears, so one `order` per page settles the order of sections and pages at each level. A page that names none follows the pages that do, in the order the directory lists them. A page that names no `title` and carries no heading is titled by the module it documents, which is what a module overview wants and how it stays correct when the module is renamed. A page whose route a `directory` entry already publishes is left to that entry, and so is the file the `diagnostics` index opens with. Sweeping a tree does not publish either one twice. ### Page directories A page entry may name a `directory` instead of a `source`. The entry then stands for every `.md` file under that directory, published at `path` followed by the file name without its extension, plus an index generated at `path` itself. A document is published by existing, so nothing has to be added to the manifest when one is written. ```lua { path = "neps", title = "NEPs", directory = "docs/neps" } ``` Each document may open with a frontmatter block of `key: value` lines between `---` fences. `title` names the document, falling back to its first heading and then to its file name. Every other field is rendered under the heading, so a `status:` line appears on the page and in the generated index without being written into the prose. A value may be quoted, and the quotes are dropped. A file name beginning with digits and a hyphen, such as `0001-process.md`, carries that number as the document's identity. The number is shown without its padding and prefixed with the entry's `title` made singular, so a collection titled `NEPs` titles its first document `NEP 1`. `index.md` is the collection's own page rather than a document in it. Its prose opens the index and the table of documents is generated below it. Links between documents are written as ordinary relative Markdown links and are resolved to routes like links in any other handwritten page. Only the index appears in the navigation; its documents are reached from it. A collection may therefore sit inside an existing section, so `path = "reference/neps"` puts one under Reference without filling that section's sidebar with every document it holds. ## Cache and failure behavior Build state is JSON in `outDir/.nupp-state.json`. Cache keys cover source content, configuration, compiler artifacts, native tool versions, flags, target settings, and dependency inputs. Generated files are rewritten only when their content changes. A missing or malformed state file causes a cold build. Warm builds reuse checked module records and generated Lua across processes. A source edit checks and generates that module; dependents are only invalidated when its exported interface fingerprint changes. Changes to project-wide type declarations invalidate the project index, while body-only edits preserve it. Deleting the state file, changing compiler or configuration inputs, or modifying an emitted artifact safely falls back to the required cold work. The optimization level is among those keys. `nupp build -O2` reaches the configuration before it is hashed, so changing it invalidates every artifact built with the old optimizer contract rather than leaving a project half compiled under each. Switching therefore costs a cold build, and cannot produce a mixture. `-O0` is the default and performs no rewrite; see the [performance guide](../performance/index.md#optimization-passes) for what the levels above it do. ### Compiler identity "Changing the compiler" means changing the part of it that computes the answer being reused, not changing any part of it. Module artifacts are keyed on what compiling a module reaches; parsed headers on the parser; formatting verdicts on the formatter; comptime type blueprints on the checker. Each is the digest of that module and everything it requires, read off the compiler's own tree, so a new command, a language-server change, or an edit to a diagnostic's prose leaves all four reusable. Anything that cannot be read that way, such as a compiler that is one bundled file or a `require` naming a computed module, falls back to the digest of the whole compiler, which invalidates more than it has to and never less. ### Shared content caches Two of these stores hold answers about content rather than about a project: a file's header and its formatting verdict are the same answers wherever the file is. `NUPP_CACHE_DIR` names one directory for them, which is what a run making many small projects wants, the test suite being one that makes a project per case, so the second project starts from what the first worked out. ```bash NUPP_CACHE_DIR=/tmp/nupp-cache nupp check ``` The build state is not moved by it: its records are keyed by module name, so two projects sharing them would read each other's modules. ### Compiled modules A `modules` build also leaves `/.bytecode`: each module it wrote, compiled once, so that starting the program parses none of them. For the compiler's own build that is a hundred and eighty files and about forty milliseconds off every command. Entries sit under a directory named for the LuaJIT that wrote them, because a checkout can be worked on by more than one -- one on the path and one the toolchain built -- and neither should be able to take the other's cache away. Within it an entry is named by the digest of the module it holds, and the `index` beside them maps a module name to a digest and records the directory they were written for. Nothing about a build depends on any of it: an index that is missing, damaged, or written for a tree that has since moved is ignored, and the modules are parsed the way they always were. What the index does not name is removed on the next build, so the directory holds one entry per module rather than one per edit. The recorded directory matters because a dump carries the chunk name it was given, and the compiler reads that name back off itself to find its declarations and its native library. Entries name the tree they were written for, so a copied build directory is ignored until a build rewrites it. `nupp clean` removes it with the rest of the build. ### Write ordering The checker and generator finish before module outputs are changed. Each file is written through a sibling temporary file, state is saved after the artifacts, and `.nupp-complete` is written last. `bin/nupp` reads that marker to decide whether the compiler in `build/` has seen every edit and needs rebuilding before the command it was asked for. It is not how the compiler to run is chosen. A build removes the marker before it writes anything, so for as long as a build takes there is a working compiler in `build/` and no marker beside it; `bin/nupp` runs the compiler that is there, and fetches the stage zero only when there is none. One build at a time writes a tree -- a build takes a lock for as long as it runs, and a command that has to read the compiler waits for it. ## Build progress and timing A build run from a terminal names the module it is working on, on one line it rewrites in place, and finishes with how long it took, where that time went, and which modules cost the most of it: ```text built compiler in 18.9s: 164 compiled, 0 reused check 16.1s generate 952ms slowest nupp.compiler.gen 1.9s nupp.mem.heap 699ms nupp.compiler.check.calls 664ms ``` Time is accounted as a timeline: one activity is current at any moment, so switching closes the one before it and the activities add up to the run rather than overlapping it. They are `scan`, which is deciding what can be reused, `check`, `generate`, `write`, `persist`, `dependencies`, `native`, `bundle`, and `other` for what is left over. A warm build that compiles nothing still reports `check`: deciding a module can be reused consults the exported call guarantees of the modules it depends on, and answering that is a check. Per-module numbers are exclusive. A module's check reaches its imports through the query graph, so the time those take is charged to them rather than to whichever module reached them first. Otherwise the slowest module would be whichever one the build happened to start with. Nothing is written unless standard error is a terminal, so a build driven by a script is as quiet as it has always been. `--progress=always` reports anyway, `-q` reports nothing, and `NUPP_PROGRESS` says the same thing with `always`, `never` or `auto` for the builds nothing passes a flag to, including the rebuild `bin/nupp` runs before every other command. `nupp build --json` carries the same numbers in a `timing` object rather than writing a report. ## Type dependencies `kind = "types"` supplies checker-only declarations. A type dependency is ambient for the whole project, rather than named by a build target: Nupp reads it while checking, but never executes it, adds it to `package.path`, or copies it to the output. ```lua love = { kind = "types", format = "luacats", source = { git = "https://github.com/LuaCATS/love2d.git", rev = "", }, path = "library", } ``` The first supported format is `luacats`. Its Lua files are parsed as annotated declarations, so a definition of a global such as `love` becomes available to Nupp source without a local adapter. The Git revision is mandatory and must be a full commit id. Nupp caches the checkout under `.nupp/deps/`, and `nupp check`, `nupp build` and the language server all resolve the pinned revision before checking, so an editor and a build agree about what the project declares. Future type providers use the same `kind = "types"` lifecycle and choose their reader through `format`, so a Teal importer does not need a separate dependency system. ## C dependencies `kind = "c"` supports local sources, `pkgConfig`, compiler and linker flags, include directories, exact-revision Git sources, and shared or static linkage: ```lua zstd = { kind = "c", source = { git = "https://github.com/facebook/zstd.git", rev = "" }, path = "lib", sources = { "*.c" }, bindings = { header = "zstd.h" } } ``` `pkgConfig` may be one package name or an array when a native library uses several installed APIs, such as `{ "libpng", "zlib" }`. The packages are resolved together so their compiler and linker flags reach the same build. Fetched Git trees live under `.nupp/deps` and require an explicit revision. C dependencies default to `linkage = "shared"` and emit a `.so`, `.dylib`, or `.dll` under `outDir/lib`. `linkage = "static"` emits `lib.a` instead, while `linkage = "both"` emits both from one set of compiled objects: ```lua image = { kind = "c", linkage = "both", sources = { "native/*.c" }, bindings = { header = "native/image.h" } } ``` The shared artifact remains the one a generated FFI binding loads. A static artifact is a linker input: another C dependency can name this dependency and will link the archive into its own shared library. A static archive becomes part of a binary only when that target selects `standalone`; ordinary binary targets retain the shared-library sidecar path. `out` renames the sole artifact for `shared` or `static`; with `both`, it names the shared artifact. `staticOut` independently renames the archive and is most useful with `both`. Both default paths are under `outDir/lib`. A configured header is passed through `import-c`, and the resulting Nupp module is placed under `outDir/generated` so it participates in normal module resolution. A static-only binding without a `bindings.library`, `load`, or `pkgConfig` override declares symbols in the process namespace for a host that links the archive; an ordinary Nupp-produced sidecar should use `shared` or `both` so the binding has a loadable library. `sources` and `headers` are path globs: `*` and `?` stay inside one component, while `**/` matches zero or more directories. `pkg-config` output honors shell quotes and backslash escapes, but is never expanded or executed by a shell. Static linkage asks `pkg-config --static` for the transitive flags that must travel with an archive. ### Target-indexed prebuilt C artifacts A library publisher can provide authenticated shared, static, or paired artifacts per target triple instead of asking an application build to compile the library again: ```lua image = { kind = "c", linkage = "both", bindings = { header = "include/image.h" }, artifacts = { ["aarch64-apple-darwin"] = { shared = {path = "prebuilt/macos/libimage.dylib", sha256 = "<64 hex>", size = 12345}, static = {path = "prebuilt/macos/libimage.a", sha256 = "<64 hex>", size = 23456} } } } ``` Selection uses the build's `layoutTarget`, or the modeled host triple for an ordinary host build. The file is read from the dependency root, authenticated, and staged under the same output name a source build would use. A standalone target selects `static`; an ordinary generated binding selects `shared`. Sources remain a fallback for triples without a matching artifact set. Prebuilt artifacts cannot request a generated macro/inline bridge, since that bridge must have been compiled into the published library already. ### Header-only C dependencies An API made entirely from `static inline` functions and function-like macros has no native symbol for LuaJIT to load. Opt its binding into a generated bridge; no empty `.c` source is required: ```lua image = { kind = "c", includeDirs = { "native" }, headers = { "native/**/*.h" }, cflags = { "-std=c11", "-Wall", "-Werror" }, cppflags = { "-DIMAGE_FAST=1" }, bindings = { header = "native/image.h", bridge = true, macros = { IMAGE_CLAMP = { parameters = { "int32", "int32", "int32" }, result = "int32" }, IMAGE_IGNORE = { parameters = { "int32" } } } } } ``` The binding keys have separate jobs: | Key | Effect | | --- | --- | | `header` | Header to preprocess and import; required for generated bindings | | `library` | Override the library name or path written into generated `cdef` declarations | | `out` | Override the generated Nupp module path | | `bridge` | Wrap eligible named `static inline` definitions from `header` | | `macros` | Wrap only the listed function-like macros using explicit signatures | Each macro recipe requires a dense `parameters` array. `result` is optional; omit it for a void wrapper. The accepted value forms are `boolean`, `float`, `number`, `integer`, `int8` through `int64`, and `uint8` through `uint64`. Recipes do not accept pointer forms, varargs, or arbitrary C declarator text. `bridge` controls inline discovery; a `macros` table can request macro wrappers independently. The header above is then consumed under the dependency name: ```nupp local image = require("image") local tripled = image.image_triple(14) local clamped = image.IMAGE_CLAMP(20, 2, 8) image.IMAGE_IGNORE(clamped) print(tripled) -- 42 ``` For the default `outDir`, a macOS build writes: ```text build/generated/image.nupp build/generated/image_bridge.c build/lib/libimage.dylib ``` Linux uses `libimage.so`; Windows uses the platform DLL name. The generated binding names that library `@lib/libimage.dylib`: a leading `@` is resolved against the module that loads it rather than handed to the platform loader, so a copied or moved output tree still finds it. A `kind = "bundle"`, `"binary"` or `"component"` target is one file someone carries somewhere, so the build puts a copy of the library beside the artifact, the way it already does for compiled `@aot` code. A `bindings.library` override, a `load` naming a library already installed, and a `pkgConfig` package are written through unchanged, since none of them are part of what the build ships. The generated translation unit includes the original header and exports only deterministic private wrapper symbols. It is compiled with the dependency's `cc`, `includeDirs`, `cflags`, `cppflags`, package flags, and linker inputs, then installed into the same shared library as any ordinary `sources`. A dependency containing only bridge wrappers still produces the library. The dependency cache includes the header, source and bridge bytes, macro recipes, compiler identity, flags, package flags, child dependencies, and manifest configuration. A changed header or recipe regenerates both the module and bridge. The binding's named header is tracked automatically; list its local include closure under `headers` as above so a transitive header edit also invalidates the native artifact. Disabling `bridge` and removing `macros` produces no bridge source, compiler invocation, or bridge-only library. Macro arity and type recipes are validated before a generated binding is installed. The C compilation then validates the original macro expansion and inline bodies under the selected flags. Either failure stops the dependency build; compiler-failed generated source remains inspectable, but no successful target may consume it. See [Calling C safely](../runtime/c-interop/index.md#header-only-functions) for a complete header, standalone bridge emission, inspection output, ownership refinements, and the supported boundary. ## Rust dependencies `kind = "cargo"` delegates package resolution and locking to Cargo. The provider builds a `cdylib` into an isolated target directory and copies the platform library into `outDir/lib`. `Cargo.lock` is enforced by default; `locked = false` is available for newly created projects, and `offline = true` passes Cargo's offline policy through. `target`, `profile`, and `features` are part of the dependency cache key. When `bindings.cbindgen` is enabled, the provider runs cbindgen in the crate directory before passing its header through `import-c`. `command` can override the `cbindgen` executable, for example when a project pins a wrapper around a particular cbindgen release. The header describes the ABI, not ownership. A Rust `Box` becomes a raw pointer in cbindgen output, so name its policy explicitly when the caller owns it. `returns` maps a constructor to its cleanup function and `takes` marks the parameter positions that cleanup consumes: ```lua bindings = { cbindgen = true, ownership = { returns = { codec_create = "codec_destroy" }, takes = { codec_destroy = { 1 } } } } ``` The generated binding represents `codec_create` as `affine(Codec*, codec_destroy)`. This changes checking and lexical cleanup, not the C ABI. An ownership mapping also asserts the returned pointer is non-null: that is correct for `Box`, but not for a nullable factory. See [ownership.md](../runtime/ownership/borrowing.md#c-interop) for what the checker then enforces at the boundary. The copy in `outDir/lib` is named the way a C dependency's library is, so a generated binding says `@lib/libtiny_rust.dylib` and a copied or moved output tree still finds it. A single-artifact target carries it beside the artifact for the same reason. ## Rock dependencies `kind = "luarocks"` installs a Lua library with LuaRocks. Nothing is built and nothing is generated: what the provider produces is a populated tree and the two search-path entries that reach it. ```lua dependencies = { -- From the LuaRocks server, at the version named here. lunamark = { kind = "luarocks", version = "0.6.0-1" }, -- From a rockspec in the project, for a library upstream does not publish. scintillua = { kind = "luarocks", rockspec = "rocks/scintillua-6.7-1.rockspec" }, -- From sources that ship with the project: `luarocks make`, no fetch. tinyrock = { kind = "luarocks", rock = "tinyrock", path = "vendor/tinyrock", rockspec = "vendor/tinyrock/tinyrock-1.0-1.rockspec" } } ``` A rock must be pinned by one of those three, a `version`, a `rockspec`, or a `path`, and a manifest that pins none of them is refused before any build work starts. Naming both a `version` and a `rockspec` that declares a different one is refused too. A rock does not list `dependencies` of its own: LuaRocks resolves what a rock needs, which is the reason to use it. `rockDependencies = false` passes `--deps-mode=none` when an upstream rockspec's dependency list is deliberately unsuitable. The target must name separately pinned rock dependencies before that rock; Nupp does not infer or replace what was disabled. This is an escape hatch for taking ownership of a dependency boundary, not an unpinned-install mode. | Field | Meaning | | --- | --- | | `rock` | The rock's name, when it differs from the dependency's | | `version` | The exact version to install | | `rockspec` | A rockspec in the project to install from | | `path` | A directory to build in place with `luarocks make` | | `bundle` | Globs naming what a bundle or binary carries | | `tree` | Where to install, `.rocks` by default | | `luaVersion` | The tree's Lua version, `5.1` by default | | `luaDir` | Where the Lua headers and libraries live | | `server` | An additional rocks server to fetch from | | `luarocks` | The LuaRocks executable, `luarocks` by default | Rocks install into `.rocks` in the project root, a tree the project owns rather than the one the user's account owns, so two checkouts can hold different versions of a library without either able to break the other's build by upgrading something. LuaJIT is Lua 5.1, and a C rock compiled against another 5.1 loads into a VM that cannot call it, which is what `luaVersion` pins. The headers a C rock compiles against are found from the running interpreter's own module path; `luaDir`, or the `NUPP_LUA_DIR` environment variable, names them instead. A pinned rock already installed at the version asked for is left alone, so a warm build reaches for nothing. A rock built from `path` is remade whenever its sources change, which is what the fingerprint is for. The tree is added to the search path of the build that installed it, so a target's own dependencies are loadable the moment they are installed, and `nupp doc` installs its renderer and renders with it in one command. `nupp test` puts the tested target's trees on `LUA_PATH` and `LUA_CPATH` for the test command, ahead of what is already there and without replacing it. Anything else that runs outside the build reads the tree the way LuaRocks trees are always read. An installed rock may also carry typed module declarations in its versioned `nupp/` directory. See [LuaRocks](integrations/luarocks.md) for authoring, packing, testing, and publishing that layout. ### Carrying a rock into a bundle A bundle and a binary are one file, and one file cannot bring a rock tree along. `bundle` names what goes in with it, as globs over the tree the rock installs into: ```lua lunamark = { kind = "luarocks", version = "0.6.0-1", bundle = { "lunamark.lua", "lunamark/**.lua", "cosmo.lua", "cosmo/**.lua", "re.lua" } } ``` Each selected file becomes a `package.preload` entry under the name `require` would have found it by in the tree, so `lunamark/writer/html.lua` becomes `lunamark.writer.html`, and a `foo/init.lua` becomes `foo`. The same `require` therefore resolves in a checkout, in a bundle, and in a stamped binary, and the program cannot tell which it is running in. Named rather than swept, because a rock tree also holds test scripts, command-line programs and documentation that nothing will ever ask for. A rock with no `bundle` is installed and not carried, which is the right answer for anything only the build itself uses. A rock's C libraries cannot ride in a payload, because a `.so` is not a Lua chunk, so a binary that needs one needs a stub linked against it. Nupp's own stub links the three its commands cannot run without; see [distribution.md](../../reference/distribution.md#limits) for that boundary. ## Self-hosting A self-hosted compiler cannot compile its own sources until some other compiler has, so `selfHost` says where that other one comes from. `selfHost.bootstrap` names a command, run from the project root, whose last line of output is the path of the stage-zero compiler; `selfHost.target` is the target that rebuilds the compiler, and `selfHost.bootstrapTarget` composes the bundle a new stage zero is published from. `nupp fixpoint` runs the command, then builds three times: stage zero builds stage one, stage one builds stage two, and stage two builds stage three. Stages two and three must be byte identical. Three stages rather than two, because the first one is not a claim about anything. Stage zero is a different compiler -- an earlier release, on purpose -- so stage one is these sources as that compiler emits them. Stage two is the first tree emitted by code the current sources describe, and stage three is stage two doing it again. Their being identical says the compiler is a fixed point of itself rather than of whatever happened to build it. The working compiler is updated only after they match. `nupp fixpoint --emit-stage0 PATH` also writes the stage-zero bundle the verified compiler composes, which is the artifact a release publishes for the next checkout to start from. Nothing reads it back: a stage zero is meant to differ from the compiler in the tree, and nothing compares the two. This repository's `selfHost.bootstrap` is `scripts/toolchain stage0`, which fetches the release pinned in `scripts/toolchain.pins` and verifies it against the digest committed beside it. The cost of that is a rule on these sources: they may only use language features the pinned release already understands, and a feature reaches them a release later. [NEP 28](https://github.com/nupp-lang/nupp/blob/main/docs/neps/0028-fetched-stage-zero.md) records why that trade was made. The build system's own implementation lives under the internal `nupp.compiler.build.*` namespace in `src/nupp/compiler/build/`: `project` owns orchestration, `hash` owns cache digests, and `process` owns argv-based subprocess execution. ## FAQ ### Why does the build compile a module nothing requires? Because the source set is what a project is written in, and a module left out of the build is a module nothing checked. See [Source sets](#source-sets) for what the alternative costs. ### Why did one edit rebuild the whole project? An edit to an exported type declaration invalidates the project index, where an edit to a function body invalidates one module. `nupp check --json` reports `timing.compiledModules` and `timing.slowest`, so a run says how much it redid rather than leaving that to be inferred from how long it took. ### Can a one-file bundle carry a native library? No. A bundle contains Lua chunks. It can require a host-supplied module such as LPeg, but cannot embed a shared library. A binary that uses native providers ships their sidecars or links them into its host. See [distribution.md](../../reference/distribution.md#limits) for the whole boundary. ::: seealso - [cli.md](../../reference/cli.md#build) for every flag `nupp build` takes - [tasks.md](project-tasks.md) for named tasks and the environment they run in - [distribution.md](../../reference/distribution.md) for how a stamped binary is put together :::