commit b81292122a781a186745cc54ba9135ae1b92e006
parent 86ef4a0326e9cda798fafd2a5a04900881e5b86b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 22 Jun 2026 14:59:37 -0700
Implement build workspaces and external repos
Diffstat:
17 files changed, 2633 insertions(+), 399 deletions(-)
diff --git a/doc/BUILD_COORDINATOR.md b/doc/BUILD_COORDINATOR.md
@@ -47,10 +47,12 @@ the resolution algorithm that drives it.
| Term | Meaning |
|------|---------|
-| **Target** `T` | A named unit of work, e.g. `//app:server`. A build definition maps each target name to one recipe; the target's local argv comes from the build request (empty if none). |
+| **Target** `T` | A named unit of work, e.g. `//app:server` or `@zlib//:lib`. A target is a canonical label `[@repo]//package:name`; the repo and package path select the workspace root and `BUILD.kit` file that resolve the local target name to one recipe. The target's local argv comes from the build request (empty if none). |
| **Recipe** | The executable the coordinator runs to produce a target's output. Identified by `recipe-id` = `BLAKE2b(recipe file bytes)`. |
| **Configuration** | Tunables a recipe reads (target triple, opt level, feature flags). Two scopes — *propagated* (a key→value map, `config-id`) and *local* (the argv vector, `argv-id`) — see [below](#configuration-model). Both content-addressed: `config-id`/`argv-id` = `BLAKE2b(canonical bytes)`. |
| **Output tree** | The directory a recipe produces, captured as a CAS tree (`tree-id`). The unit a build request returns. |
+| **Exports** | Optional package-config-style metadata a recipe publishes by writing a canonical `.kit/exports` file inside its output tree. Dependents query it through ordinary `need` edges, so no trace shape changes. |
+| **External repo** | A hash-pinned workspace archive named by the root workspace manifest and addressed in labels as `@name//pkg:target`. It is fetched by blob/package id, verified, unpacked into the external-repo cache, then resolved like any workspace root. |
| **Test target request** | A request to execute a target as a test (`kit build test ...`). It uses the same recipe protocol and dependency discovery as a build request, but returns a typed test result. |
| **Test result tree** | The directory a test recipe produces, plus coordinator-injected `stdout` and `stderr` files, captured as a CAS tree (`result-tree-id`). |
| **PASS-only test trace** | A test trace is a cacheable claim only when the recipe exits 0. Nonzero exits return FAIL results for inspection but write no reusable trace. |
@@ -66,44 +68,391 @@ the coordinator for a config value, the bytes of a source file, the expansion of
a glob, or a hash-pinned blob dependency, and each request is logged as a dependency
so a later rebuild knows precisely what to re-check.
-## Build definitions
+## Build packages and recipe resolution
A workspace is rooted by `KitBuildOptions.workspace_root` (`kit build --root`,
-default `.`) and has a build definition at `KitBuildOptions.build_def_path`
-(`kit build --def`, default `BUILD.kit`). The definition is strict canonical
-text:
+default `.`). Build definitions are package-local `BUILD.kit` files rooted at
+workspace-relative directories. A canonical in-workspace target label has the
+form `//package:name`:
+
+- `//:name` is in the root package and resolves through `<root>/BUILD.kit`.
+- `//app:server` is in package `app` and resolves through
+ `<root>/app/BUILD.kit`.
+- `//lib/math:vec` is in package `lib/math` and resolves through
+ `<root>/lib/math/BUILD.kit`.
+
+The CLI and helper commands may accept Bazel-style shorthand, but the resolver
+normalizes before cache lookup or protocol logging: `//pkg` means `//pkg:pkg`,
+`:name` means `//<current-package>:name`, and bare `name` is accepted only where
+the command grammar unambiguously expects a local target name.
+
+External targets add a repository prefix: `@zlib//:lib`,
+`@platform//include:headers`. Repository names are local to the workspace whose
+recipe issued the request; before a target enters trace bodies or target-record
+keys, the resolver expands it to a canonical repo instance selected by the live
+workspace manifest. The user-facing spelling remains `@name//pkg:target`, but
+resolution always knows which verified external workspace root that name denotes.
+
+Package paths use forward slashes, are relative to the workspace root, and reject
+empty components, `.`, `..`, backslashes, and drive-prefix syntax. A package
+exists only when its directory contains a `BUILD.kit` file; labels do not search
+ancestor directories. A nested `BUILD.kit` starts a nested package, so files below
+that directory belong to the nested package for discovery and generated-target
+listing purposes.
+
+Each `BUILD.kit` is strict canonical text. The v1 form remains the minimal exact
+mapping:
```
kit-build 1
-[target //app:server]
+[target server]
recipe recipes/app.sh
-[target //lib:core]
+[target core]
recipe recipes/lib.sh
```
-Target stanzas are sorted by target name and unique. Target names are non-empty
-tokens (no whitespace or newlines). Recipe paths are workspace-relative paths:
-absolute paths, `.` / `..` path elements, backslashes, and drive-prefix syntax
-are rejected. The definition supplies only the target-to-recipe mapping. It does
-not supply argv, default config, or dependency edges; those are request-time or
+Target stanzas are sorted by **local** target name and unique within the package.
+Local names are non-empty tokens (no whitespace, slash, colon, or newlines).
+Recipe paths are resolved relative to the package directory unless explicitly
+prefixed with `//`, in which case they are workspace-relative; absolute paths,
+`.` / `..` path elements, backslashes, and drive-prefix syntax are rejected.
+
+The package file supplies recipe-resolution metadata only. It does not supply
+argv, propagated config, or dependency edges; those are request-time or
dynamically discovered through the recipe protocol.
-The live definition is consulted when a target is resolved. The only
-definition-derived input recorded in traces is the resolved recipe's content id
-(`recipe-id`), not a hash of the whole definition file.
+### Target catalog
+
+The resolver should grow beyond explicit exact targets without changing the
+coordinator's cache or recipe protocol. A package-local catalog may contain:
+
+- **exact targets**, which name a local target directly;
+- **default recipes**, keyed by target type;
+- **rules**, which map local target-name patterns to target types and optional
+ recipe overrides;
+- **redo-style default recipe discovery**, where a target name's suffixes imply
+ fallback recipe filenames searched through an ordered set of directories;
+- **listing projections**, which enumerate discoverable generated targets for
+ `kit build list` and for match diagnostics.
+
+One possible canonical v2 shape:
+
+```
+kit-build 2
+
+[default c.object]
+recipe //recipes/c-object.sh
+
+[default c.library]
+recipe //recipes/c-library.sh
+
+[target core]
+type c.library
+
+[targets c.object]
+list src/*.c -> {stem}.o
+
+[rule c-object]
+match *.o
+type c.object
+list src/*.c -> {stem}.o
+
+[redo-defaults]
+enabled true
+search . do
+walk-parents true
+```
+
+Resolution of `//pkg:name` is deterministic:
+
+1. Load `<root>/pkg/BUILD.kit`.
+2. Prefer an exact `[target name]` stanza.
+3. Otherwise evaluate package-local `[rule ...]` matchers against `name`.
+4. Reject duplicate exact definitions or ambiguous same-priority rule matches.
+5. If no exact target or rule matched, try redo-style recipe-file discovery when
+ enabled for the package.
+6. Determine the target type, if the matching target/rule/default supplied one.
+7. Select the recipe: target recipe override, then rule recipe override, then
+ discovered redo-style recipe, then package/default recipe for the type, then
+ built-in default recipe for the type. If no recipe is found, resolution fails.
+
+Redo-style discovery is deterministic but not limited to the target package. It
+builds an ordered list of search roots, then probes a suffix list within each
+root. For target `//lib/math:foo.test.o`, with `search . do` and
+`walk-parents true`, search roots are:
+
+```
+lib/math/
+lib/math/do/
+lib/
+lib/do/
+./
+do/
+```
+
+Within each root, the local target name `foo.test.o` probes:
+
+```
+foo.test.o.do
+default.test.o.do
+default.o.do
+default.do
+```
+
+The first present executable file wins. `search` entries are workspace-relative
+or package-relative directories (`.` means the package/ancestor directory at the
+current walk step); they reject absolute paths, `.`, `..` path elements other
+than the single `.` search token, backslashes, and drive-prefix syntax.
+`walk-parents false` restricts the search to the target package's roots. The
+root package is always the last ancestor when parent walking is enabled.
+
+The selected recipe path is normalized to a workspace-relative path and still
+produces a normal `recipe-id` by hashing that file's bytes; the fact that it was
+selected by suffix discovery is not separately recorded in traces. Adding a
+more-specific default later changes only targets whose live resolution now
+selects different recipe bytes. A same-content replacement remains a cache
+no-op. Because first-hit wins, broad defaults should live high in the tree (or in
+a configured `do/` root) and package-specific overrides should live near the
+package that needs them.
+
+`kit build list //pkg/...` lists the package's exact targets plus generated
+targets from finite `list` projections. A rule without a finite listing
+projection, or a target resolved only by redo-style default discovery, may still
+resolve a direct request, but it is not discoverable unless a listing projection
+enumerates it. Listing descends into child packages by finding nested
+`BUILD.kit` files; a package's rules do not list targets inside nested packages,
+and redo defaults still need explicit listing projections to contribute
+discoverable targets.
+
+`BUILD.kit` is therefore still useful alongside `.do` files: it is the package's
+index, not necessarily the place where every recipe body lives. It can declare
+many targets compactly through grouped target-list stanzas and projections, while
+the recipes themselves live in shared `.do` files:
+
+```
+[targets c.object]
+list src/*.c -> {stem}.o
+
+[targets c.test]
+list test/*_test.c -> {stem}
+
+[rule c-object]
+match *.o
+type c.object
+```
+
+Grouped target stanzas are pure catalog entries. They expand to local target
+names for listing, and those expanded names behave like generated exact targets
+for resolution: exact target metadata, then any recipe override, then matching
+rule, redo-style discovered recipe, and type default. The projection must be
+finite and deterministic: it may use package-relative globs, sorted by path, with
+template variables derived only from the matched path (`{stem}`, `{base}`,
+`{dir}`, `{ext}`).
+
+For convenience, `kit build list --scan-do //pkg/...` may also enumerate targets
+implied by concrete `.do` filenames found in the redo search roots. This scan is
+best-effort discovery only: `foo.o.do` can list `//pkg:foo.o`, but
+`default.o.do` cannot know all possible `.o` names without a projection, so it is
+reported as a default provider rather than expanded into targets. The default
+plain `kit build list` should stay projection-based and deterministic; `--scan-do`
+is useful for humans auditing recipe coverage.
+
+Relative labels are client ergonomics only. The wire protocol and trace bodies
+store canonical labels. Recipe helper commands may expand `:local` relative to
+`$KIT_BUILD_PACKAGE`; `//other:target` means the current repo's `other` package;
+and `@repo//other:target` selects an external repo through the current repo's
+workspace manifest before issuing a `need` request.
+
+The live package definition is consulted when a target is resolved. The only
+catalog-derived input recorded in traces is the resolved recipe's content id
+(`recipe-id`), not a hash of the whole package file or workspace catalog.
+Changing a rule or exact target so the same canonical label selects different
+recipe bytes busts via `recipe-id`; editing unrelated targets, defaults, or
+listing metadata does not. Any target metadata that a default recipe uses to
+choose sources, flags, outputs, or dependencies must still be consumed through
+tracked inputs (`source`, `glob`, `config-get`, `need`, or a future equivalent);
+the resolver itself remains a client-side organization layer, not hidden cache
+identity.
+
+### Workspace manifest, defaults, and external repositories
+
+A workspace may also have a root `WORKSPACE.kit` manifest. `BUILD.kit` files
+describe packages; `WORKSPACE.kit` describes the workspace as a consumable unit,
+names external repositories, and carries workspace-wide build defaults. There is
+no separate build-config file in v1: settings that are stable enough to check in
+belong here, while per-invocation choices remain CLI `--config` values. The
+manifest is canonical text and optional for single-workspace builds:
+
+```
+kit-workspace 1
+name myapp
+version 0.1.0
+def-name BUILD.kit
+
+[config default]
+target x86_64-linux
+opt debug
+
+[config release]
+inherits default
+opt release
+
+[external zlib]
+format tar.gz
+archive <blob-id>
+url https://example.org/zlib-1.3.1.tar.gz
+strip-prefix zlib-1.3.1
+
+[external platform]
+format kpkg
+archive <blob-id>
+package <package-id>
+url https://cache.example/platform-2026.kpkg
+```
+
+`[config default]` supplies propagated configuration entries for every top-level
+request in that workspace. Named config profiles are optional overlays selected
+by the CLI (`--profile release`) or by a future package policy. Profile
+inheritance is explicit and acyclic; later entries in the selected profile
+override inherited entries. Final top-level propagated config is:
+
+```
+WORKSPACE.kit [config default]
+ ⊕ selected [config PROFILE] overlays
+ ⊕ CLI --config / --env entries
+```
+
+The resulting map is still serialized as the ordinary `config-id`; traces do not
+record which profile names produced it. Changing a workspace default therefore
+affects only targets whose recipes actually consume the changed keys, by the
+same config-observation rules as CLI config. `def-name` selects the package
+build-file basename for this workspace unless overridden by `--def-name`; it is
+not part of cache identity except through the recipe selected by live catalog
+resolution.
+
+External repository entries are hash-pinned. `archive` is the BLAKE2b blob id of
+the exact archive bytes; `url` rows are untrusted fetch hints and are not cache
+identity. For `format tar.gz`, the coordinator fetches the archive blob if
+needed, verifies the blob id, unpacks it with strict path validation, applies
+`strip-prefix` if present, and treats the resulting directory as a read-only
+workspace root. For `format kpkg`, the archive blob is a signed kit package; the
+coordinator verifies the package with the configured trust policy, checks the
+optional `package` id when present, and materializes its default output tree as
+the external workspace root.
+
+External workspaces are cached under the build store by content, e.g.
+`build/external/<repo-key>/`, where `<repo-key>` is derived from `(format,
+archive-id, package-id-or-empty, strip-prefix)`. Fetching or unpacking the same
+repo twice is idempotent. The unpacked tree is read-only to recipes; outputs
+still go to the recipe sandbox. Labels inside an external repo resolve against
+that repo's own `BUILD.kit` and optional `WORKSPACE.kit`. If an external recipe
+needs `@foo//...`, `@foo` is looked up in that external workspace's manifest,
+not in the root workspace, unless an explicit future repo-mapping feature says
+otherwise.
+
+Changing a root `WORKSPACE.kit` external entry is not recorded as a whole-file
+input. Instead, live resolution of `@name//pkg:target` selects a different
+verified workspace root when the pinned archive/package id changes. Existing
+traces then refresh through that root: recipe ids and source/glob leaves are
+recomputed against the new external workspace. Same-content repins remain cache
+no-ops; different content busts through the existing `recipe-id`, source, glob,
+and dep-output checks.
+
+The driver should provide repository-management commands that update
+`WORKSPACE.kit` without hand-editing hashes:
+
+```
+kit build repo add NAME URL [--format tar.gz|kpkg] [--strip-prefix DIR]
+kit build repo fetch NAME
+kit build repo list
+```
+
+`repo add` downloads `URL` through the host fetch path, stores the bytes in the
+CAS as a blob, records the blob id and URL in `WORKSPACE.kit`, and for `.kpkg`
+also records the verified `package-id`. `repo fetch` materializes the configured
+external workspace into the build-store external cache.
+
+To make a workspace consumable by another workspace, package the source
+workspace as a deterministic kit package whose default output tree is the
+workspace root. The package must include `WORKSPACE.kit`, package `BUILD.kit`
+files, recipes, source files, and any files needed by package listing/resolution;
+it must exclude the build store, transient sandboxes, and VCS metadata unless
+explicitly included. A workspace package is then referenced from another
+workspace's `WORKSPACE.kit` as an external repo, normally with `format kpkg`.
+This reuses the existing `kit pkg` trust model: content bytes are hash-verified,
+and the package manifest is trusted only when its signer is trusted.
+
+### Published target exports
+
+Targets may publish package-config-style metadata for dependents by placing a
+canonical exports file in their output tree:
+
+```
+$KIT_BUILD_OUT/.kit/exports
+```
+
+The file is strict canonical text. It maps variable names to ordered string
+vectors:
+
+```
+kit-build-exports 1
+[var cflags]
+-Iinclude
+-DKIT_USE_ZLIB=1
+[var libs]
+-Llib
+-lz
+```
+
+Variable names are sorted by byte order. Values inside one variable keep recipe
+order because flag order may be semantically meaningful. Values are raw text
+lines without NUL or newline bytes. Unknown sections, duplicate variable
+sections, or non-canonical ordering are malformed. Recipes may write this file
+directly or use a helper:
+
+```
+kit build export-set cflags -- -Iinclude -DKIT_USE_ZLIB=1
+kit build export-set libs -- -Llib -lz
+```
+
+Exports are part of the output tree, so they are already covered by `tree-id`.
+A dependent that wants exports first performs a normal `need` of the provider
+target, then reads `.kit/exports` from the materialized output path. The helper
+commands are convenience wrappers around that rule:
+
+```
+kit build export-get TARGET VAR
+kit build export-collect VAR TARGET...
+```
+
+`export-get` is equivalent to `need TARGET` followed by parsing the target's
+exports and printing the selected variable. `export-collect` repeats that for
+several targets in argument order. The dependency recorded in the trace is still
+only the ordinary target dep edge `(dep, dep-config-id, dep-argv-id,
+output-tree-id)`: if the provider's exports change, its output tree changes, so
+the dependent's shallow dep-output comparison fails and the dependent recipe
+runs again. There is no separate exports trace section, no ambient package-config
+database, and no hidden cache identity.
+
+This is intentionally conservative. If a provider changes an unrelated output
+file but preserves identical exports, dependents that queried exports still see
+the provider's `tree-id` change and may re-run. A later optimization could split
+exports into a named sub-tree or metadata id and record that id on dep edges, but
+v1 keeps the cache model simple and sound.
## Configuration model
Configuration has two scopes, distinguished by visibility:
- **Propagated configuration** flows down the whole subtree. The top-level
- request supplies it; a recipe reads a value with `config-get <key>` (logged as a
+ request supplies it, after applying `WORKSPACE.kit` defaults and any selected
+ config profile; a recipe reads a value with `config-get <key>` (logged as a
config dep); and a `need` may **overlay** it for the sub-build it triggers. A
target's *effective* propagated config = its inherited config with overlays
applied along the `need` path from the root. It is canonicalized to byte-stable
text and content-addressed: `config-id = BLAKE2b(canonical map)`, stored as a
CAS blob so any recorded `config-id` resolves back to the actual map.
- Driver spelling is `--config K=V`. For recipe environment pass-through,
+ Driver spelling is `--profile NAME` and `--config K=V`. For recipe environment pass-through,
`--config env.NAME` copies the current process's `$NAME` value into config key
`env.NAME`; `--env NAME` and `--env NAME=VALUE` are shorthand for
`--config env.NAME` and `--config env.NAME=VALUE`. The pass-through forms
@@ -111,7 +460,7 @@ Configuration has two scopes, distinguished by visibility:
- **Local configuration** is the target's **argv**, supplied entirely by the
build request (both the top-level request and a `need` carry an optional argv);
- when none is given it is the **empty** argv. The build definition carries no
+ when none is given it is the **empty** argv. The package catalog carries no
argv. It is visible *only* to that one target's recipe (delivered as the
process's actual argv) and is **not** propagated to deps. Treating argv as
config keeps the model uniform: everything a recipe sees is configuration, some
@@ -127,30 +476,30 @@ bundled with [shared traces](#shared-traces-trusted-as-signed-packages)). This i
what lets the shallow path **replay every `need` request exactly** — reconstruct
the dep's target name, its full effective config with overlays applied, and its
argv — even when driving from an imported shallow trace with no access to the
-original build definition or workspace. Source files and globs are the opposite
+original package catalog or workspace. Source files and globs are the opposite
case: kept by hash only, because they are *verified* (did this still hash the
same?), never replayed, and their bytes already live in the CAS.
The **resolution identity** of a build is therefore the triple
`(target-name, config-id, argv-id)`, where `config-id` is the effective
*propagated* config and `argv-id` is the effective *local* argv (the request's
-argv, or the empty argv when none is supplied — the build definition carries no
+argv, or the empty argv when none is supplied — the package catalog carries no
argv). The target name is just an identifier — argv is not encoded in it; it is
supplied by the request and then content-addressed. Two requests that differ in the propagated
config **or** the argv — `(T, C1, A)` and `(T, C2, A)`, or `(T, C, A1)` and
`(T, C, A2)` — are genuinely distinct builds; any number may be in flight at once.
`recipe-id` covers only the recipe *file bytes*. *Which* recipe a target maps to
-lives in the **build definition**, which the coordinator reads to resolve `T`.
-The only definition-derived input recorded for `T` is its **resolved recipe-id**
-(a per-node scalar), refreshed by recomputing it through the *live* definition —
-not a hash of the whole definition file. So repointing `T` to a different-content
-recipe busts it; repointing to a same-content recipe does not (it is the same
-build); and editing an *unrelated* target's stanza leaves `defn_find(T)`
-unchanged, so it does not bust `T`. The *mapping* is not an independent input —
-only the resolved recipe content is. (argv is *not* in the definition — it comes
-from the request, and a different argv is a different `argv-id`, hence a distinct
-build.)
+lives in the package catalog, which the coordinator reads to resolve `T`. The
+only catalog-derived input recorded for `T` is its **resolved recipe-id** (a
+per-node scalar), refreshed by recomputing it through the *live* catalog — not a
+hash of the whole package file or target listing. So repointing `T` to a
+different-content recipe busts it; repointing to a same-content recipe does not
+(it is the same build); and editing an *unrelated* target, rule, or listing entry
+leaves `catalog_resolve(T)`'s recipe unchanged, so it does not bust `T`. The
+*mapping* is not an independent input — only the resolved recipe content is.
+(argv is *not* in the catalog — it comes from the request, and a different argv
+is a different `argv-id`, hence a distinct build.)
### Why two trace kinds
@@ -239,9 +588,10 @@ src/*.c <glob-result-hash>
//lib:core <dep-config-id> <dep-argv-id> <dep-output-tree-id>
```
-The `recipe <recipe-id>` scalar is the only definition-derived input (recomputed
-through the live definition on refresh); the definition file itself is *not* a
-source leaf, so editing an unrelated target's stanza does not bust this trace.
+The `recipe <recipe-id>` scalar is the only catalog-derived input (recomputed
+through the live package catalog on refresh); the package file itself is *not* a
+source leaf, so editing an unrelated target, rule, or listing entry does not bust
+this trace.
**Deep trace** — the `(argv)` binding to an output, plus a single pointer to the
transitive input closure (held as a **deepset** DAG, below), *no* `[dep]` and
@@ -299,7 +649,7 @@ ancestor:
```
kit-build-deepset 1
target //app:server ; carried so refresh can recompute this node's recipe-id
-recipe <recipe-id> ; the node's recipe content-hash (the only definition-derived input)
+recipe <recipe-id> ; the node's recipe content-hash (the only catalog-derived input)
[config] ; this node's scope-projected propagated config observations
opt <present|unset/default...> <value-hash-or->
[source] ; this node's DIRECT source leaves, sorted by path
@@ -334,13 +684,13 @@ src/*.c <glob-result-hash>
source is recorded as `<path> -` (a lone `-`), so *creating* the file later
busts the trace — the source analogue of a consumed-while-unset config key.
- `recipe <recipe-id>` (on the shallow trace and on every deepset node) is the
- target's **only** definition-derived input: `BLAKE2b` of the recipe *file*
- bytes, refreshed by recomputing it through the *live* definition
- (`defn_find(target) → recipe path → hash`). That catches a recipe edit *or* a
- repoint to a different-content recipe at any depth, treats a repoint to a
- same-content recipe as unchanged, and — because it goes through `defn_find` —
- is unaffected by edits to *unrelated* stanzas. There is no whole-definition
- source leaf.
+ target's **only** catalog-derived input: `BLAKE2b` of the recipe *file* bytes,
+ refreshed by recomputing it through the *live* catalog
+ (`catalog_resolve(target) → recipe path → hash`). That catches a recipe edit
+ *or* a repoint to a different-content recipe at any depth, treats a repoint to
+ a same-content recipe as unchanged, and — because it goes through
+ `catalog_resolve` — is unaffected by edits to unrelated stanzas, rules, or
+ listing metadata. There is no whole-package-file source leaf.
- `deepset <deep-set-id>` is the root of the closure DAG. Phase 1 loads it and
its children from the CAS to refresh; if any deepset blob is absent (e.g.
GC'd), the deep trace is treated as absent and resolution falls through
@@ -626,7 +976,7 @@ Expected event shapes for the config-scope cases:
### Test caching
`kit build test` is first-class test execution in the coordinator, not a separate
-task runner. A test target uses the same build definition, recipe protocol,
+task runner. A test target uses the same package catalog, recipe protocol,
dependency discovery, config/argv identity, deepset refresh, materialization,
remotes, and verify mode as `kit build`; the difference is the result type and
record namespace.
@@ -654,9 +1004,10 @@ still resolves to the same output tree. Verify mode audits cached PASS claims by
re-running the test and comparing the fresh result tree with the cached
`result-tree-id`; a FAIL or different result tree reports a verify mismatch.
-Task-runner policy (test discovery, selection expressions, sharding, retries,
-report aggregation) is deliberately deferred. The v1 surface is one target
-request at a time: `kit build test ... TARGET [-- ARG...]`.
+Task-runner policy (test selection expressions, sharding, retries, report
+aggregation) is deliberately deferred. Target discovery for tests uses the same
+package catalog listing as build targets, but the v1 execution surface is one
+target request at a time: `kit build test ... TARGET [-- ARG...]`.
### Running a recipe
@@ -666,9 +1017,10 @@ run_recipe(T, cfg, argv, chain):
sandbox = build/tmp/run-<n>/ ; out = sandbox/out/
spawn recipe (process argv = argv, the request's argv or empty when none given) with a
CLEAN env (host inherits nothing): { KIT_BUILD_SOCK, KIT_BUILD_OUT=out,
- KIT_BUILD_TARGET=T, workspace root } + cfg's `env.*` keys as declared env vars
+ KIT_BUILD_TARGET=T, KIT_BUILD_REPO=<repo>, KIT_BUILD_PACKAGE=<package>,
+ current workspace root } + cfg's `env.*` keys as declared env vars
service the recipe's protocol requests, logging each as a dep (next section);
- config-get reads cfg; source returns the live workspace path + pinned blob-id;
+ config-get reads cfg; source returns the live current-workspace path + pinned blob-id;
need releases this recipe's job slot, recurses resolve(...) under cfg ⊕ overrides
and the need's own effective argv, then reacquires the slot (so a chain deeper
than `jobs` cannot deadlock)
@@ -773,25 +1125,32 @@ command.
`--env PATH` is the CLI shorthand for copying the driver's current `$PATH` into
config key `env.PATH`; `--env PATH=/bin` sets that key explicitly. If `$PATH`
is unset, `--env PATH` / `--config env.PATH` is rejected.
-- **`source`** hands back a path inside the **live workspace** (not a staged
- copy) plus the blob-id the read is pinned to; an absent path is reported and
- recorded as an *absent* leaf (creating it later busts the trace). Returning the
- live path makes workspace immutability for the build's duration load-bearing
- for soundness — see [Determinism](#determinism-and-hermeticity).
+- **`source`** hands back a path inside the target's **live workspace root** (the
+ main workspace, or the fetched read-only external repo for an `@repo` target;
+ not a staged copy) plus the blob-id the read is pinned to. An absent path is
+ reported and recorded as an *absent* leaf (creating it later busts the trace).
+ Returning the live path makes workspace immutability for the build's duration
+ load-bearing for soundness — see [Determinism](#determinism-and-hermeticity).
+ The wire request uses workspace-relative paths; helper commands may accept
+ package-relative paths and expand them through `$KIT_BUILD_PACKAGE` before
+ sending the request.
- **`glob`** records the whole match set's content via `glob-result-hash`; the
recipe then reads the returned paths without further declaration. Matches are
**streamed** across as many frames as needed, so an arbitrarily large match set
- is not bounded by a single frame.
+ is not bounded by a single frame. As with `source`, helpers may expand
+ package-relative patterns before issuing the canonical workspace-relative
+ request.
- **`fetch`** retrieves an externally stored blob by expected content id, trying
URL hints in order and installing only bytes that hash to that id. The logged
dependency is the blob id itself. URLs are not cache identity: changing mirrors
or URL order does not make a different build as long as the same blob id is
requested.
-- **`need`** is the dynamic-dependency primitive. The optional `k=v` pairs
- **overlay** propagated config for that sub-build; `--env NAME` /
+- **`need`** is the dynamic-dependency primitive. The target may be in the same
+ workspace (`//pkg:name`) or an external repo (`@repo//pkg:name`). The optional
+ `k=v` pairs **overlay** propagated config for that sub-build; `--env NAME` /
`--env NAME=value` are accepted as env-prefixed config overlays; an optional
- argv vector sets the dep's **local argv** (omitted ⇒ the empty argv; local argv never
- propagates). The coordinator resolves `(dep, cfg ⊕ overrides, argv)`
+ argv vector sets the dep's **local argv** (omitted ⇒ the empty argv; local argv
+ never propagates). The coordinator resolves `(dep, cfg ⊕ overrides, argv)`
recursively, returns its output `tree-id` and a readable path, and records the
edge with the dep's `config-id` and `argv-id` so the shallow path can re-resolve
identically. `need` is also where cycles are caught.
@@ -825,16 +1184,19 @@ logged as an ordinary source dependency.
Build execution:
```
-kit build [--store DIR] [--root DIR] [--def FILE]
- [--config K=V|env.NAME]... [--env NAME[=V]]...
+kit build [--store DIR] [--root DIR] [--def-name FILE]
+ [--profile NAME] [--config K=V|env.NAME]... [--env NAME[=V]]...
[--verify] [--stats] [--trace] TARGET [-- ARG...]
+kit build list [--scan-do] [PATTERN]
+kit build repo {add|fetch|list} ...
+kit build workspace package [--format kpkg|tar.gz] [-s SECKEY] -o OUT
```
Test execution:
```
-kit build test [--store DIR] [--root DIR] [--def FILE]
- [--config K=V|env.NAME]... [--env NAME[=V]]...
+kit build test [--store DIR] [--root DIR] [--def-name FILE]
+ [--profile NAME] [--config K=V|env.NAME]... [--env NAME[=V]]...
[--verify] [--stats] [--trace] TARGET [-- ARG...]
```
@@ -845,7 +1207,10 @@ errors also exit nonzero and print diagnostics on stderr. `--stats` prints
cumulative coordinator counters, including `test_runs`, `test_cache_hits`, and
`test_failures`, to stderr. `--trace` prints target-level resolution decisions
to stderr, using the [diagnostic resolution trace](#diagnostic-resolution-trace)
-event names.
+event names. `--def-name` selects the package build-file basename, overriding
+`WORKSPACE.kit`'s `def-name`; when neither is present it defaults to
+`BUILD.kit`. It must be a single filename, not a path, so package lookup remains
+`<workspace>/<package>/<def-name>`.
Recipe-side shell helper commands are available as `kit build <verb> ...` inside
a running recipe (`$KIT_BUILD_SOCK` set), or explicitly outside a recipe as
@@ -857,6 +1222,9 @@ kit build source [--optional] [--format path|id|id-path] PATH
kit build fetch [--format path|id|id-path] BLOB URL...
kit build depfile [--lines] FILE
kit build glob PATTERN
+kit build export-set VAR -- VALUE...
+kit build export-get TARGET VAR
+kit build export-collect VAR TARGET...
kit build need [--format path|id|id-path]
[--config K=V|env.NAME]... [--env NAME[=V]]...
TARGET [-- ARG...]
@@ -1007,7 +1375,7 @@ deterministically.
futures for in-flight target dedup. Hosts without a scheduler run sequentially;
the current `kit build` hosted path uses that sequential mode.
- **No-cache policy.** The model describes when a target should always run, but
- the current build definition format has no target-level no-cache flag.
+ the current package catalog format has no target-level no-cache flag.
- **GC.** Build-store garbage collection is not implemented. The intended sweep
roots at live target records, follows trace bodies to referenced output
trees/blobs, config maps, argv vectors, and deepset closure blobs, then removes
diff --git a/driver/cmd/build_coord.c b/driver/cmd/build_coord.c
@@ -28,6 +28,7 @@ typedef struct BuildCli {
const char* store;
const char* root;
const char* def;
+ const char* profile;
const char* target;
const char* config_default;
KitBuildKV config[BUILD_MAX_CONFIG];
@@ -50,10 +51,10 @@ void driver_help_build(void) {
"kit build - content-addressed build coordinator\n"
"\n"
"USAGE\n"
- " kit build [--store DIR] [--root DIR] [--def FILE]\n"
+ " kit build [--store DIR] [--root DIR] [--def FILE] [--profile NAME]\n"
" [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
" [--verify] [--stats] TARGET [-- ARG...]\n"
- " kit build test [--store DIR] [--root DIR] [--def FILE]\n"
+ " kit build test [--store DIR] [--root DIR] [--def FILE] [--profile NAME]\n"
" [--config K=V|env.NAME]... [--env NAME[=V]]...\n"
" [--verify] [--stats] TARGET [-- ARG...]\n"
" kit build config-get [--default VALUE] KEY # inside a build recipe\n"
@@ -76,7 +77,8 @@ void driver_help_build(void) {
"OPTIONS\n"
" --store DIR Build store root (default: $KIT cache/build)\n"
" --root DIR Workspace root (default: .)\n"
- " --def FILE Build definition path (default: BUILD.kit)\n"
+ " --def FILE Package build-file basename (default: WORKSPACE def-name or BUILD.kit)\n"
+ " --profile NAME Apply WORKSPACE.kit [config NAME] after [config default]\n"
" --config K=V Seed propagated configuration\n"
" --config env.N Seed env.N from current $N\n"
" --env N[=V] Shorthand for --config env.N[=V]\n"
@@ -604,7 +606,6 @@ static int build_parse_args(BuildCli* cli, int argc, char** argv, int first_arg,
int i;
memset(cli, 0, sizeof *cli);
cli->root = ".";
- cli->def = BUILD_DEFAULT_DEF;
cli->test_mode = test_mode;
for (i = first_arg; i < argc; ++i) {
const char* a = argv[i];
@@ -614,6 +615,8 @@ static int build_parse_args(BuildCli* cli, int argc, char** argv, int first_arg,
cli->root = argv[++i];
} else if (driver_streq(a, "--def") && i + 1 < argc) {
cli->def = argv[++i];
+ } else if (driver_streq(a, "--profile") && i + 1 < argc) {
+ cli->profile = argv[++i];
} else if (driver_streq(a, "--config") && i + 1 < argc) {
if (build_parse_config(cli, argv[++i]) != 0) {
driver_errf(BUILD_TOOL, "bad --config, expected K=V or env.NAME");
@@ -773,7 +776,8 @@ int driver_build(int argc, char** argv) {
memset(&opts, 0, sizeof opts);
opts.workspace_root = kit_slice_cstr(abs_root);
- opts.build_def_path = kit_slice_cstr(cli.def);
+ opts.build_def_path = cli.def ? kit_slice_cstr(cli.def) : KIT_SLICE_NULL;
+ opts.profile = cli.profile ? kit_slice_cstr(cli.profile) : KIT_SLICE_NULL;
opts.jobs = 1;
opts.verify = cli.verify;
if (cli.trace) opts.trace = build_trace_stderr;
diff --git a/include/kit/build_coord.h b/include/kit/build_coord.h
@@ -44,6 +44,7 @@
#define KIT_BUILD_ENV_SOCK "KIT_BUILD_SOCK"
#define KIT_BUILD_ENV_OUT "KIT_BUILD_OUT"
#define KIT_BUILD_ENV_TARGET "KIT_BUILD_TARGET"
+#define KIT_BUILD_ENV_PACKAGE "KIT_BUILD_PACKAGE"
/* Propagated-config keys under this prefix declare the recipe's pass-through
* environment: a key "env.PATH" sets $PATH (to its config value) in the recipe's
@@ -254,7 +255,8 @@ typedef void (*KitBuildTraceFn)(void* user, KitSlice line);
typedef struct KitBuildOptions {
KitSlice workspace_root; /* recipe cwd and source root */
- KitSlice build_def_path; /* build-definition file (tracked as a source) */
+ KitSlice build_def_path; /* package build-file basename (default BUILD.kit) */
+ KitSlice profile; /* optional WORKSPACE.kit [config NAME] overlay */
int jobs; /* max concurrent recipes; <=0 => host default */
int verify; /* verify mode: re-run cache hits and compare */
KitBuildTraceFn trace; /* optional diagnostic resolution trace sink */
diff --git a/src/api/build_coord.c b/src/api/build_coord.c
@@ -27,20 +27,21 @@ KitStatus kit_build(KitBuildCoordinator* c, const KitBuildRequest* req,
BuildConfig cfg;
BuildArgv argv;
BuildResolved r;
- size_t i;
+ char target[BUILD_TARGET_MAX];
if (!c || !req || !out) return KIT_INVALID;
+ if (build_coord_canonical_target(c, req->target, KIT_SLICE_NULL,
+ KIT_SLICE_NULL, target) != BUILD_OK)
+ return KIT_INVALID;
build_config_init(&cfg, cfg_entries, sizeof cfg_entries / sizeof cfg_entries[0]);
- for (i = 0; i < req->nconfig; ++i) {
- if (build_config_set(&cfg, req->config[i].key, req->config[i].value) !=
- BUILD_OK)
- return KIT_INVALID;
- }
+ if (build_coord_top_config(c, req->config, req->nconfig, &cfg) != BUILD_OK)
+ return KIT_INVALID;
build_argv_init(&argv, argv_entries,
sizeof argv_entries / sizeof argv_entries[0]);
if (build_argv_set(&argv, req->argv, req->argc) != BUILD_OK)
return KIT_INVALID;
memset(&r, 0, sizeof r);
- if (build_resolve(c, req->target, &cfg, &argv, NULL, &r) != BUILD_OK)
+ if (build_resolve(c, kit_slice_cstr(target), &cfg, &argv, NULL, &r) !=
+ BUILD_OK)
return KIT_ERR;
memcpy(out->output_tree, r.output_tree, KIT_BUILD_HASH_LEN);
memcpy(out->path, r.path, sizeof out->path);
@@ -54,22 +55,23 @@ KitStatus kit_build_test(KitBuildCoordinator* c, const KitBuildRequest* req,
BuildConfig cfg;
BuildArgv argv;
BuildTestResolved r;
- size_t i;
+ char target[BUILD_TARGET_MAX];
if (!c || !req || !out) return KIT_INVALID;
if (!c->host.exec || !c->host.exec->spawn_capture) return KIT_UNSUPPORTED;
+ if (build_coord_canonical_target(c, req->target, KIT_SLICE_NULL,
+ KIT_SLICE_NULL, target) != BUILD_OK)
+ return KIT_INVALID;
build_config_init(&cfg, cfg_entries,
sizeof cfg_entries / sizeof cfg_entries[0]);
- for (i = 0; i < req->nconfig; ++i) {
- if (build_config_set(&cfg, req->config[i].key, req->config[i].value) !=
- BUILD_OK)
- return KIT_INVALID;
- }
+ if (build_coord_top_config(c, req->config, req->nconfig, &cfg) != BUILD_OK)
+ return KIT_INVALID;
build_argv_init(&argv, argv_entries,
sizeof argv_entries / sizeof argv_entries[0]);
if (build_argv_set(&argv, req->argv, req->argc) != BUILD_OK)
return KIT_INVALID;
memset(&r, 0, sizeof r);
- if (build_test_resolve(c, req->target, &cfg, &argv, NULL, &r) != BUILD_OK)
+ if (build_test_resolve(c, kit_slice_cstr(target), &cfg, &argv, NULL, &r) !=
+ BUILD_OK)
return KIT_ERR;
out->status = r.status;
out->exit_code = r.exit_code;
diff --git a/src/build/coord.c b/src/build/coord.c
@@ -1,6 +1,10 @@
#include "coord.h"
#include "bundle.h"
+#include "../dist/tar.h"
+
+#include <kit/compress.h>
+#include <kit/package.h>
#include <stdarg.h>
#include <stdio.h>
@@ -27,6 +31,7 @@ struct BuildPulledSet {
};
struct BuildSourceMemo {
+ char root[BUILD_PATH_MAX];
char path[BUILD_PATH_MAX];
uint8_t blob[BUILD_HASH_LEN];
int present;
@@ -34,6 +39,7 @@ struct BuildSourceMemo {
};
struct BuildGlobMemo {
+ char root[BUILD_PATH_MAX];
char pattern[BUILD_PATTERN_MAX];
uint8_t result_hash[BUILD_HASH_LEN];
BuildPathBlob* entries;
@@ -53,8 +59,17 @@ typedef struct BuildDeepSetMemo {
struct BuildDeepSetMemo* next;
} BuildDeepSetMemo;
+struct BuildExternalWorkspace {
+ char name[BUILD_KEY_MAX];
+ char root[BUILD_PATH_MAX];
+ char def_name[BUILD_PATH_MAX];
+ BuildWorkspace workspace;
+ struct BuildExternalWorkspace* next;
+};
+
typedef struct BuildGlobExpand {
KitBuildCoordinator* c;
+ const char* root;
KitSlice pattern;
BuildPathBlob* entries;
size_t n;
@@ -62,6 +77,16 @@ typedef struct BuildGlobExpand {
int failed;
} BuildGlobExpand;
+static int build_coord_source_hash_root(KitBuildCoordinator* c,
+ const char* root, KitSlice path,
+ uint8_t out_blob[BUILD_HASH_LEN],
+ int* present);
+static int build_coord_glob_root(KitBuildCoordinator* c, const char* root,
+ KitSlice pattern,
+ uint8_t out_result_hash[BUILD_HASH_LEN],
+ BuildCoordGlobFn cb, void* cb_user);
+static void release_file_data(KitBuildCoordinator* c, KitFileData* fd);
+
static int path_set(char* out, size_t cap, KitSlice s) {
if (!out || cap == 0u || !s.s || s.len + 1u > cap) return BUILD_ERR;
memcpy(out, s.s, s.len);
@@ -215,8 +240,8 @@ static int glob_walk_cb(void* user, const char* source_path,
(void)executable;
if (!g || !tree_path) return 1;
if (!glob_match_path(g->pattern, tree_path)) return 0;
- if (build_coord_source_hash(g->c, kit_slice_cstr(tree_path), blob,
- &present) != BUILD_OK ||
+ if (build_coord_source_hash_root(g->c, g->root, kit_slice_cstr(tree_path),
+ blob, &present) != BUILD_OK ||
!present ||
glob_entries_push(g, tree_path, blob) != BUILD_OK) {
g->failed = 1;
@@ -225,25 +250,356 @@ static int glob_walk_cb(void* user, const char* source_path,
return 0;
}
-static size_t count_targets(const uint8_t* data, size_t len) {
- size_t i, n = 0;
- static const char marker[] = "[target ";
- for (i = 0; i + sizeof marker - 1u <= len; ++i) {
- if ((i == 0u || data[i - 1u] == '\n') &&
- memcmp(data + i, marker, sizeof marker - 1u) == 0)
- ++n;
+static int read_workspace_manifest(KitBuildCoordinator* c, const char* root,
+ BuildWorkspace* out, int* present) {
+ char path[BUILD_PATH_MAX];
+ KitFileData fd;
+ char err[160];
+ if (!c || !root || !out || !present) return BUILD_ERR;
+ *present = 0;
+ build_workspace_init(out);
+ if (path_join2(path, sizeof path, root, "WORKSPACE.kit") != BUILD_OK)
+ return BUILD_ERR;
+ memset(&fd, 0, sizeof fd);
+ if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, path,
+ &fd) != KIT_OK)
+ return BUILD_OK;
+ *present = 1;
+ if (build_workspace_parse(fd.data, fd.size, out, err, sizeof err) !=
+ BUILD_OK) {
+ build_diagf(c->ctx, "build: %s: %s", path, err);
+ release_file_data(c, &fd);
+ return BUILD_ERR;
+ }
+ release_file_data(c, &fd);
+ return BUILD_OK;
+}
+
+static int external_key(const BuildWorkspaceExternal* ext, char out[BUILD_HEX_LEN]) {
+ KitBlobInfo bi;
+ char buf[BUILD_PATH_MAX + 160];
+ char archive[BUILD_HEX_LEN], package[BUILD_HEX_LEN];
+ const char* fmt = "unknown";
+ if (!ext || !out) return BUILD_ERR;
+ if (ext->format == BUILD_WS_EXT_TREE) fmt = "tree";
+ if (ext->format == BUILD_WS_EXT_KPKG) fmt = "kpkg";
+ if (ext->format == BUILD_WS_EXT_TARGZ) fmt = "tar.gz";
+ kit_hex_encode(archive, ext->archive, BUILD_HASH_LEN);
+ if (ext->has_package)
+ kit_hex_encode(package, ext->package, BUILD_HASH_LEN);
+ else
+ package[0] = '\0';
+ snprintf(buf, sizeof buf, "%s %s %s %s", fmt, archive, package,
+ ext->strip_prefix);
+ kit_blob_info(&bi, (const uint8_t*)buf, strlen(buf));
+ kit_hex_encode(out, bi.id, BUILD_HASH_LEN);
+ return BUILD_OK;
+}
+
+static int path_parent_dir(const char* path, char* out, size_t cap) {
+ size_t n, i;
+ if (!path || !out || cap == 0u) return BUILD_ERR;
+ n = strlen(path);
+ while (n && (path[n - 1u] == '/' || path[n - 1u] == '\\')) --n;
+ for (i = n; i > 0u; --i) {
+ if (path[i - 1u] == '/' || path[i - 1u] == '\\') {
+ if (i > cap) return BUILD_ERR;
+ memcpy(out, path, i - 1u);
+ out[i - 1u] = '\0';
+ return BUILD_OK;
+ }
+ }
+ out[0] = '\0';
+ return BUILD_OK;
+}
+
+static int external_write_file(KitBuildCoordinator* c, const char* root,
+ const char* rel, const uint8_t* data,
+ size_t len, int executable) {
+ char path[BUILD_PATH_MAX];
+ char parent[BUILD_PATH_MAX];
+ KitWriter* w = NULL;
+ KitStatus st;
+ if (!c || !root || !rel || (!data && len)) return BUILD_ERR;
+ if (path_join2(path, sizeof path, root, rel) != BUILD_OK ||
+ path_parent_dir(path, parent, sizeof parent) != BUILD_OK)
+ return BUILD_ERR;
+ if (parent[0] && c->host.cas_host->mkdir_p &&
+ c->host.cas_host->mkdir_p(c->host.cas_host->user, parent) != 0)
+ return BUILD_ERR;
+ if (!c->host.cas_host->file_io ||
+ c->host.cas_host->file_io->open_writer(c->host.cas_host->file_io->user,
+ path, &w) != KIT_OK ||
+ !w)
+ return BUILD_ERR;
+ st = len ? kit_writer_write(w, data, len) : KIT_OK;
+ if (st == KIT_OK) st = kit_writer_status(w);
+ kit_writer_close(w);
+ if (st != KIT_OK) return BUILD_ERR;
+ if (executable && c->host.cas_host->mark_executable &&
+ c->host.cas_host->mark_executable(c->host.cas_host->user, path) != 0)
+ return BUILD_ERR;
+ return BUILD_OK;
+}
+
+static int external_tar_relpath(const BuildWorkspaceExternal* ext,
+ const DistTarEntry* e,
+ char out[BUILD_PATH_MAX], int* skip) {
+ const char* name;
+ const char* rel;
+ size_t prefix_len;
+ if (!ext || !e || !out || !skip) return BUILD_ERR;
+ *skip = 0;
+ name = e->name;
+ if (!name[0]) return BUILD_ERR;
+ if (ext->strip_prefix[0]) {
+ prefix_len = strlen(ext->strip_prefix);
+ if (strncmp(name, ext->strip_prefix, prefix_len) != 0)
+ return BUILD_ERR;
+ if (name[prefix_len] == '\0') {
+ *skip = 1;
+ return BUILD_OK;
+ }
+ if (name[prefix_len] != '/') return BUILD_ERR;
+ rel = name + prefix_len + 1u;
+ } else {
+ rel = name;
+ }
+ if (!rel[0]) {
+ *skip = 1;
+ return BUILD_OK;
+ }
+ if (strlen(rel) >= BUILD_PATH_MAX || !rel_path_safe(kit_slice_cstr(rel)))
+ return BUILD_ERR;
+ snprintf(out, BUILD_PATH_MAX, "%s", rel);
+ return BUILD_OK;
+}
+
+static int materialize_targz_external(KitBuildCoordinator* c,
+ const BuildWorkspaceExternal* ext,
+ const char* root) {
+ KitSlice urls[8];
+ KitFileData fd;
+ KitWriter* tar_w = NULL;
+ const uint8_t* tar_bytes;
+ size_t tar_len;
+ DistTarEntry entries[DIST_MAX_FILES];
+ size_t nentries = 0;
+ char blob_path[BUILD_PATH_MAX];
+ size_t i;
+ int ok = BUILD_ERR;
+ if (!c || !ext || !root) return BUILD_ERR;
+ for (i = 0; i < ext->n_urls; ++i) urls[i] = kit_slice_cstr(ext->urls[i]);
+ if (build_coord_fetch_blob(c, ext->archive, urls, ext->n_urls, blob_path,
+ sizeof blob_path) != BUILD_OK)
+ return BUILD_ERR;
+ memset(&fd, 0, sizeof fd);
+ if (kit_cas_get_blob(c->cas, ext->archive, &fd) != KIT_OK) return BUILD_ERR;
+ if (kit_writer_mem(c->ctx->heap, &tar_w) != KIT_OK || !tar_w)
+ goto out;
+ if (kit_decompress(c->ctx, KIT_COMPRESS_GZIP, fd.data, fd.size, tar_w) !=
+ KIT_OK ||
+ kit_writer_status(tar_w) != KIT_OK)
+ goto out;
+ tar_bytes = kit_writer_mem_bytes(tar_w, &tar_len);
+ if (dist_tar_iter(tar_bytes, tar_len, entries,
+ sizeof entries / sizeof entries[0], &nentries) != DIST_OK)
+ goto out;
+ for (i = 0; i < nentries; ++i) {
+ char rel[BUILD_PATH_MAX];
+ int skip = 0;
+ if (entries[i].type == '5') continue;
+ if (entries[i].type != '\0' && entries[i].type != '0') goto out;
+ if (external_tar_relpath(ext, &entries[i], rel, &skip) != BUILD_OK)
+ goto out;
+ if (skip) continue;
+ if (external_write_file(c, root, rel, entries[i].data, entries[i].size,
+ (entries[i].mode & 0111u) != 0u) != BUILD_OK)
+ goto out;
+ }
+ ok = BUILD_OK;
+out:
+ if (tar_w) kit_writer_close(tar_w);
+ kit_cas_release(c->cas, &fd);
+ return ok;
+}
+
+static int trusted_pkgid(const char* trusted, uint8_t out[BUILD_HASH_LEN],
+ int* present) {
+ const char* p;
+ char hex[BUILD_HEX_LEN];
+ if (!trusted || !out || !present) return BUILD_ERR;
+ *present = 0;
+ p = strstr(trusted, "pkgid=");
+ if (!p) return BUILD_OK;
+ p += 6u;
+ if (strlen(p) < BUILD_HEX_LEN - 1u) return BUILD_ERR;
+ memcpy(hex, p, BUILD_HEX_LEN - 1u);
+ hex[BUILD_HEX_LEN - 1u] = '\0';
+ if (kit_hex_decode(out, hex, BUILD_HASH_LEN) != KIT_OK) return BUILD_ERR;
+ *present = 1;
+ return BUILD_OK;
+}
+
+static int ensure_external_workspace(KitBuildCoordinator* c,
+ const BuildWorkspace* parent_ws,
+ KitSlice repo, KitSlice instance,
+ BuildExternalWorkspace** out) {
+ BuildExternalWorkspace* ew;
+ const BuildWorkspaceExternal* ext;
+ char key[BUILD_HEX_LEN];
+ char parent[BUILD_PATH_MAX];
+ char root[BUILD_PATH_MAX];
+ int present = 0;
+ if (!c || !parent_ws || !out || !repo.s || repo.len == 0u ||
+ !instance.s || instance.len == 0u || instance.len >= BUILD_KEY_MAX)
+ return BUILD_ERR;
+ for (ew = c->externals; ew; ew = ew->next) {
+ if (strlen(ew->name) == instance.len &&
+ memcmp(ew->name, instance.s, instance.len) == 0) {
+ *out = ew;
+ return BUILD_OK;
+ }
+ }
+ ext = build_workspace_external_find(parent_ws, repo);
+ if (!ext) {
+ build_diagf(c->ctx, "build: unknown external repo @%.*s",
+ KIT_SLICE_ARG(repo));
+ return BUILD_ERR;
+ }
+ if (external_key(ext, key) != BUILD_OK ||
+ path_join2(parent, sizeof parent, c->store.root, "external") !=
+ BUILD_OK ||
+ path_join2(root, sizeof root, parent, key) != BUILD_OK)
+ return BUILD_ERR;
+ if (c->host.cas_host->mkdir_p &&
+ c->host.cas_host->mkdir_p(c->host.cas_host->user, parent) != 0)
+ return BUILD_ERR;
+ if (ext->format == BUILD_WS_EXT_TREE) {
+ if (kit_cas_verify_tree(c->cas, ext->archive) != KIT_OK)
+ return BUILD_ERR;
+ if (c->host.cas_host->mkdir_p &&
+ c->host.cas_host->mkdir_p(c->host.cas_host->user, root) != 0)
+ return BUILD_ERR;
+ if (kit_cas_materialize_tree(c->cas, ext->archive, root) != KIT_OK)
+ return BUILD_ERR;
+ } else if (ext->format == BUILD_WS_EXT_KPKG) {
+ KitFileData fd;
+ KitPkgVerifyOptions opts;
+ KitPkgVerifyResult result;
+ KitSlice urls[8];
+ size_t i;
+ char blob_path[BUILD_PATH_MAX];
+ for (i = 0; i < ext->n_urls; ++i) urls[i] = kit_slice_cstr(ext->urls[i]);
+ if (build_coord_fetch_blob(c, ext->archive, urls, ext->n_urls, blob_path,
+ sizeof blob_path) != BUILD_OK)
+ return BUILD_ERR;
+ memset(&fd, 0, sizeof fd);
+ if (kit_cas_get_blob(c->cas, ext->archive, &fd) != KIT_OK) return BUILD_ERR;
+ memset(&opts, 0, sizeof opts);
+ memset(&result, 0, sizeof result);
+ opts.pkg_data = fd.data;
+ opts.pkg_len = fd.size;
+ opts.format = KIT_PKG_FORMAT_KPKG;
+ opts.unpack_dir = root;
+ opts.tofu = 1;
+ if (kit_pkg_verify(c->ctx, c->host.cas_host, &opts, &result) != KIT_OK) {
+ kit_cas_release(c->cas, &fd);
+ return BUILD_ERR;
+ }
+ if (ext->has_package) {
+ uint8_t got[BUILD_HASH_LEN];
+ int has_pkgid = 0;
+ if (trusted_pkgid(result.trusted, got, &has_pkgid) != BUILD_OK ||
+ !has_pkgid || !build_id_eq(got, ext->package)) {
+ kit_cas_release(c->cas, &fd);
+ return BUILD_ERR;
+ }
+ }
+ kit_cas_release(c->cas, &fd);
+ } else if (ext->format == BUILD_WS_EXT_TARGZ) {
+ if (c->host.cas_host->mkdir_p &&
+ c->host.cas_host->mkdir_p(c->host.cas_host->user, root) != 0)
+ return BUILD_ERR;
+ if (materialize_targz_external(c, ext, root) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ return BUILD_ERR;
+ }
+ ew = (BuildExternalWorkspace*)c->ctx->heap->alloc(
+ c->ctx->heap, sizeof *ew, _Alignof(BuildExternalWorkspace));
+ if (!ew) return BUILD_ERR;
+ memset(ew, 0, sizeof *ew);
+ memcpy(ew->name, instance.s, instance.len);
+ ew->name[instance.len] = '\0';
+ snprintf(ew->root, sizeof ew->root, "%s", root);
+ if (read_workspace_manifest(c, root, &ew->workspace, &present) != BUILD_OK) {
+ c->ctx->heap->free(c->ctx->heap, ew, sizeof *ew);
+ return BUILD_ERR;
}
- return n;
+ snprintf(ew->def_name, sizeof ew->def_name, "%s",
+ ew->workspace.def_name[0] ? ew->workspace.def_name : "BUILD.kit");
+ ew->next = c->externals;
+ c->externals = ew;
+ *out = ew;
+ return BUILD_OK;
}
-static void release_defn(KitBuildCoordinator* c) {
- const KitFileIO* fio;
- if (!c || !c->defn_bytes.data) return;
- fio = c->host.cas_host ? c->host.cas_host->file_io : NULL;
- if (fio && fio->release) fio->release(fio->user, &c->defn_bytes);
- c->defn_bytes.data = NULL;
- c->defn_bytes.size = 0;
- c->defn_bytes.token = NULL;
+static int target_workspace(KitBuildCoordinator* c, KitSlice target,
+ char repo[BUILD_KEY_MAX], const char** root_out,
+ const char** def_name_out,
+ const BuildWorkspace** ws_out) {
+ char package[BUILD_PATH_MAX];
+ char local[BUILD_TARGET_MAX];
+ if (!c || !root_out || !def_name_out || !ws_out) return BUILD_ERR;
+ if (build_target_split_repo(target, repo, package, local) != BUILD_OK)
+ return BUILD_ERR;
+ if (repo && repo[0]) {
+ const BuildWorkspace* cur_ws = &c->root_workspace;
+ const char* cur_root = c->workspace_root;
+ const char* cur_def_name = c->build_def_name;
+ char instance[BUILD_KEY_MAX];
+ size_t instance_len = 0;
+ size_t pos = 0;
+ size_t repo_len = strlen(repo);
+ while (pos < repo_len) {
+ BuildExternalWorkspace* ew = NULL;
+ KitSlice name;
+ KitSlice inst;
+ size_t start = pos;
+ while (pos < repo_len && repo[pos] != '+') ++pos;
+ if (pos == start) return BUILD_ERR;
+ if (instance_len) {
+ if (instance_len + 1u >= sizeof instance) return BUILD_ERR;
+ instance[instance_len++] = '+';
+ }
+ if (instance_len + (pos - start) + 1u > sizeof instance)
+ return BUILD_ERR;
+ memcpy(instance + instance_len, repo + start, pos - start);
+ instance_len += pos - start;
+ instance[instance_len] = '\0';
+ name.s = repo + start;
+ name.len = pos - start;
+ inst = kit_slice_cstr(instance);
+ if (ensure_external_workspace(c, cur_ws, name, inst, &ew) != BUILD_OK)
+ return BUILD_ERR;
+ cur_ws = &ew->workspace;
+ cur_root = ew->root;
+ cur_def_name = ew->def_name;
+ if (pos < repo_len && repo[pos] == '+') {
+ ++pos;
+ if (pos == repo_len) return BUILD_ERR;
+ }
+ }
+ *root_out = cur_root;
+ *def_name_out = cur_def_name;
+ *ws_out = cur_ws;
+ } else {
+ *root_out = c->workspace_root;
+ *def_name_out = c->build_def_name;
+ *ws_out = &c->root_workspace;
+ }
+ return BUILD_OK;
}
static void free_deepsets(KitBuildCoordinator* c) {
@@ -300,6 +656,18 @@ static void free_pulled(KitBuildCoordinator* c) {
c->pulled = NULL;
}
+static void free_external_workspaces(KitBuildCoordinator* c) {
+ BuildExternalWorkspace* ew;
+ if (!c || !c->ctx || !c->ctx->heap) return;
+ ew = c->externals;
+ while (ew) {
+ BuildExternalWorkspace* next = ew->next;
+ c->ctx->heap->free(c->ctx->heap, ew, sizeof *ew);
+ ew = next;
+ }
+ c->externals = NULL;
+}
+
static void free_sources(KitBuildCoordinator* c) {
BuildSourceMemo* n;
if (!c || !c->ctx || !c->ctx->heap) return;
@@ -366,10 +734,7 @@ KitStatus build_coord_open(const KitContext* ctx, const KitBuildHost* host,
KitBuildCoordinator** out) {
KitBuildCoordinator* c;
KitHeap* h;
- KitFileData fd;
- size_t ntargets;
- char defn_path[BUILD_PATH_MAX];
- char err[160];
+ int have_workspace = 0;
KitStatus st;
if (!ctx || !ctx->heap || !host || !host->cas_host ||
@@ -395,71 +760,40 @@ KitStatus build_coord_open(const KitContext* ctx, const KitBuildHost* host,
h->free(h, c, sizeof *c);
return KIT_INVALID;
}
-
- st = kit_cas_open(c->ctx, host->cas_host, c->cas_root, &c->cas);
- if (st != KIT_OK) {
+ build_workspace_init(&c->root_workspace);
+ if (read_workspace_manifest(c, c->workspace_root, &c->root_workspace,
+ &have_workspace) != BUILD_OK) {
h->free(h, c, sizeof *c);
- return st;
- }
- if (build_store_open(c->ctx, c->cas, host->store_io, host->cas_host,
- store_root, &c->store) != BUILD_OK) {
- kit_cas_close(c->cas);
- h->free(h, c, sizeof *c);
- return KIT_IO;
+ return KIT_MALFORMED;
}
if (!opts->build_def_path.s || opts->build_def_path.len == 0u) {
- build_diagf(c->ctx, "build: missing build definition path");
- kit_cas_close(c->cas);
- h->free(h, c, sizeof *c);
- return KIT_INVALID;
- }
- if (rel_path_safe(opts->build_def_path)) {
- if (path_join2(defn_path, sizeof defn_path, c->workspace_root,
- opts->build_def_path.s) != BUILD_OK) {
- kit_cas_close(c->cas);
+ if (path_set(c->build_def_name, sizeof c->build_def_name,
+ kit_slice_cstr(c->root_workspace.def_name[0]
+ ? c->root_workspace.def_name
+ : "BUILD.kit")) != BUILD_OK) {
h->free(h, c, sizeof *c);
return KIT_INVALID;
}
- } else if (path_set(defn_path, sizeof defn_path, opts->build_def_path) !=
- BUILD_OK) {
- kit_cas_close(c->cas);
+ } else if (!rel_path_safe(opts->build_def_path) ||
+ memchr(opts->build_def_path.s, '/', opts->build_def_path.len) ||
+ path_set(c->build_def_name, sizeof c->build_def_name,
+ opts->build_def_path) != BUILD_OK) {
+ build_diagf(&c->ctx_storage, "build: bad build definition name");
h->free(h, c, sizeof *c);
return KIT_INVALID;
}
- fd.data = NULL;
- fd.size = 0;
- fd.token = NULL;
- if (host->cas_host->file_io->read_all(host->cas_host->file_io->user,
- defn_path, &fd) != KIT_OK) {
- build_diagf(c->ctx, "build: failed to read definition: %s", defn_path);
- kit_cas_close(c->cas);
+
+ st = kit_cas_open(c->ctx, host->cas_host, c->cas_root, &c->cas);
+ if (st != KIT_OK) {
h->free(h, c, sizeof *c);
- return KIT_IO;
- }
- c->defn_bytes = fd;
- ntargets = count_targets(fd.data, fd.size);
- c->defn.targets = NULL;
- c->defn.cap_targets = ntargets;
- if (ntargets) {
- c->defn.targets = (BuildTargetDefn*)h->alloc(
- h, ntargets * sizeof *c->defn.targets, _Alignof(BuildTargetDefn));
- if (!c->defn.targets) {
- release_defn(c);
- kit_cas_close(c->cas);
- h->free(h, c, sizeof *c);
- return KIT_NOMEM;
- }
+ return st;
}
- if (build_defn_parse(fd.data, fd.size, &c->defn, err, sizeof err) !=
- BUILD_OK) {
- build_diagf(c->ctx, "build: %s", err);
- if (c->defn.targets)
- h->free(h, c->defn.targets, ntargets * sizeof *c->defn.targets);
- release_defn(c);
+ if (build_store_open(c->ctx, c->cas, host->store_io, host->cas_host,
+ store_root, &c->store) != BUILD_OK) {
kit_cas_close(c->cas);
h->free(h, c, sizeof *c);
- return KIT_MALFORMED;
+ return KIT_IO;
}
*out = c;
return KIT_OK;
@@ -471,29 +805,50 @@ void build_coord_close(KitBuildCoordinator* c) {
h = c->ctx->heap;
free_targets(c);
free_pulled(c);
+ free_external_workspaces(c);
free_sources(c);
free_globs(c);
free_deepsets(c);
- release_defn(c);
- if (c->defn.targets)
- h->free(h, c->defn.targets,
- c->defn.cap_targets * sizeof *c->defn.targets);
kit_cas_close(c->cas);
h->free(h, c, sizeof *c);
}
int build_coord_source_hash(KitBuildCoordinator* c, KitSlice path,
uint8_t out_blob[BUILD_HASH_LEN], int* present) {
+ return build_coord_source_hash_root(c, c ? c->workspace_root : NULL, path,
+ out_blob, present);
+}
+
+int build_coord_source_hash_target(KitBuildCoordinator* c, KitSlice target,
+ KitSlice path,
+ uint8_t out_blob[BUILD_HASH_LEN],
+ int* present) {
+ char repo[BUILD_KEY_MAX];
+ const char* root;
+ const char* def_name;
+ const BuildWorkspace* ws;
+ (void)def_name;
+ (void)ws;
+ if (target_workspace(c, target, repo, &root, &def_name, &ws) != BUILD_OK)
+ return BUILD_ERR;
+ return build_coord_source_hash_root(c, root, path, out_blob, present);
+}
+
+static int build_coord_source_hash_root(KitBuildCoordinator* c, const char* root,
+ KitSlice path,
+ uint8_t out_blob[BUILD_HASH_LEN],
+ int* present) {
char full[BUILD_PATH_MAX];
char rel[BUILD_PATH_MAX];
KitFileData fd;
KitBlobInfo info;
BuildSourceMemo* memo;
BuildSourceMemo* fresh = NULL;
- if (!c || !out_blob || !present || !rel_path_safe(path)) return BUILD_ERR;
+ if (!c || !root || !out_blob || !present || !rel_path_safe(path))
+ return BUILD_ERR;
if (slice_copy(rel, sizeof rel, path) != BUILD_OK) return BUILD_ERR;
for (memo = c->sources; memo; memo = memo->next) {
- if (strcmp(memo->path, rel) == 0) {
+ if (strcmp(memo->root, root) == 0 && strcmp(memo->path, rel) == 0) {
memcpy(out_blob, memo->blob, BUILD_HASH_LEN);
*present = memo->present;
return BUILD_OK;
@@ -503,9 +858,10 @@ int build_coord_source_hash(KitBuildCoordinator* c, KitSlice path,
_Alignof(BuildSourceMemo));
if (!fresh) return BUILD_ERR;
memset(fresh, 0, sizeof *fresh);
+ snprintf(fresh->root, sizeof fresh->root, "%s", root);
snprintf(fresh->path, sizeof fresh->path, "%s", rel);
*present = 0;
- if (path_join2(full, sizeof full, c->workspace_root, rel) != BUILD_OK)
+ if (path_join2(full, sizeof full, root, rel) != BUILD_OK)
goto err;
fd.data = NULL;
fd.size = 0;
@@ -548,8 +904,7 @@ int build_coord_fetch_blob(KitBuildCoordinator* c,
char dest[BUILD_PATH_MAX];
size_t i;
int installed = 0;
- if (!c || !expected_blob || !urls || nurls == 0u || !path_out ||
- path_cap == 0u)
+ if (!c || !expected_blob || !path_out || path_cap == 0u)
return BUILD_ERR;
path_out[0] = '\0';
if (kit_cas_has_blob(c->cas, expected_blob) == KIT_OK)
@@ -557,6 +912,7 @@ int build_coord_fetch_blob(KitBuildCoordinator* c,
KIT_OK
? BUILD_OK
: BUILD_ERR;
+ if (!urls || nurls == 0u) return BUILD_ERR;
if (!c->host.fetch || !c->host.fetch->fetch_url || !c->host.store_io ||
!c->host.store_io->make_temp_dir || !c->host.store_io->remove)
return BUILD_ERR;
@@ -600,17 +956,25 @@ int build_coord_fetch_blob(KitBuildCoordinator* c,
int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern,
uint8_t out_result_hash[BUILD_HASH_LEN],
BuildCoordGlobFn cb, void* cb_user) {
+ return build_coord_glob_root(c, c ? c->workspace_root : NULL, pattern,
+ out_result_hash, cb, cb_user);
+}
+
+static int build_coord_glob_root(KitBuildCoordinator* c, const char* root,
+ KitSlice pattern,
+ uint8_t out_result_hash[BUILD_HASH_LEN],
+ BuildCoordGlobFn cb, void* cb_user) {
BuildGlobExpand g;
size_t i, out_n = 0;
char pat[BUILD_PATTERN_MAX];
BuildGlobMemo* memo;
BuildGlobMemo* fresh = NULL;
- if (!c || !out_result_hash || !glob_pattern_safe(pattern) ||
+ if (!c || !root || !out_result_hash || !glob_pattern_safe(pattern) ||
!c->host.cas_host || !c->host.cas_host->walk_regular_files)
return BUILD_ERR;
if (slice_copy(pat, sizeof pat, pattern) != BUILD_OK) return BUILD_ERR;
for (memo = c->globs; memo; memo = memo->next) {
- if (strcmp(memo->pattern, pat) == 0) {
+ if (strcmp(memo->root, root) == 0 && strcmp(memo->pattern, pat) == 0) {
memcpy(out_result_hash, memo->result_hash, BUILD_HASH_LEN);
if (cb) {
for (i = 0; i < memo->n_entries; ++i) {
@@ -622,9 +986,10 @@ int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern,
}
memset(&g, 0, sizeof g);
g.c = c;
+ g.root = root;
g.pattern = pattern;
if (c->host.cas_host->walk_regular_files(c->host.cas_host->user,
- c->workspace_root, glob_walk_cb,
+ root, glob_walk_cb,
&g) != 0 ||
g.failed)
goto err;
@@ -645,6 +1010,7 @@ int build_coord_glob(KitBuildCoordinator* c, KitSlice pattern,
_Alignof(BuildGlobMemo));
if (!fresh) goto err;
memset(fresh, 0, sizeof *fresh);
+ snprintf(fresh->root, sizeof fresh->root, "%s", root);
snprintf(fresh->pattern, sizeof fresh->pattern, "%s", pat);
memcpy(fresh->result_hash, out_result_hash, BUILD_HASH_LEN);
if (out_n) {
@@ -678,6 +1044,21 @@ err:
return BUILD_ERR;
}
+int build_coord_glob_target(KitBuildCoordinator* c, KitSlice target,
+ KitSlice pattern,
+ uint8_t out_result_hash[BUILD_HASH_LEN],
+ BuildCoordGlobFn cb, void* cb_user) {
+ char repo[BUILD_KEY_MAX];
+ const char* root;
+ const char* def_name;
+ const BuildWorkspace* ws;
+ (void)def_name;
+ (void)ws;
+ if (target_workspace(c, target, repo, &root, &def_name, &ws) != BUILD_OK)
+ return BUILD_ERR;
+ return build_coord_glob_root(c, root, pattern, out_result_hash, cb, cb_user);
+}
+
int build_coord_config_by_id(KitBuildCoordinator* c,
const uint8_t config_id[BUILD_HASH_LEN],
BuildConfig* out) {
@@ -708,23 +1089,321 @@ int build_coord_argv_by_id(KitBuildCoordinator* c,
return r;
}
+int build_coord_top_config(KitBuildCoordinator* c, const KitBuildKV* overrides,
+ size_t noverrides, BuildConfig* out) {
+ char err[160];
+ if (!c || !out) return BUILD_ERR;
+ if (build_workspace_config_apply(&c->root_workspace, c->opts.profile,
+ overrides, noverrides, out, err,
+ sizeof err) != BUILD_OK) {
+ build_diagf(c->ctx, "build: %s", err);
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+int build_coord_canonical_target(KitBuildCoordinator* c, KitSlice label,
+ KitSlice current_repo,
+ KitSlice current_package,
+ char out[BUILD_TARGET_MAX]) {
+ char err[128];
+ if (!c || !out) return BUILD_ERR;
+ if (build_target_canonicalize(label, current_repo, current_package, out, err,
+ sizeof err) != BUILD_OK) {
+ build_diagf(c->ctx, "build: %s: %.*s", err, KIT_SLICE_ARG(label));
+ return BUILD_ERR;
+ }
+ return BUILD_OK;
+}
+
+static int package_def_path(const char* root, const char* def_name,
+ const char* package, char out[BUILD_PATH_MAX]) {
+ char rel[BUILD_PATH_MAX];
+ if (!root || !def_name || !package || !out) return BUILD_ERR;
+ if (package[0]) {
+ if (path_join2(rel, sizeof rel, package, def_name) != BUILD_OK)
+ return BUILD_ERR;
+ } else {
+ if (path_set(rel, sizeof rel, kit_slice_cstr(def_name)) !=
+ BUILD_OK)
+ return BUILD_ERR;
+ }
+ return path_join2(out, BUILD_PATH_MAX, root, rel);
+}
+
+static void release_file_data(KitBuildCoordinator* c, KitFileData* fd) {
+ if (!c || !fd) return;
+ if (fd->data && c->host.cas_host && c->host.cas_host->file_io &&
+ c->host.cas_host->file_io->release)
+ c->host.cas_host->file_io->release(c->host.cas_host->file_io->user, fd);
+ fd->data = NULL;
+ fd->size = 0;
+ fd->token = NULL;
+}
+
+static int read_package_defn(KitBuildCoordinator* c, const char* root,
+ const char* def_name, const char* package,
+ BuildDefn* defn, KitFileData* fd, char* err,
+ size_t errcap) {
+ char path[BUILD_PATH_MAX];
+ BuildTargetDefn* targets = NULL;
+ BuildDefaultDefn* defaults = NULL;
+ BuildRuleDefn* rules = NULL;
+ KitHeap* h;
+ enum { MAX_TARGETS = 256, MAX_DEFAULTS = 64, MAX_RULES = 128 };
+ if (!c || !defn || !fd) return BUILD_ERR;
+ h = c->ctx->heap;
+ memset(defn, 0, sizeof *defn);
+ memset(fd, 0, sizeof *fd);
+ targets = (BuildTargetDefn*)h->alloc(h, MAX_TARGETS * sizeof *targets,
+ _Alignof(BuildTargetDefn));
+ defaults = (BuildDefaultDefn*)h->alloc(h, MAX_DEFAULTS * sizeof *defaults,
+ _Alignof(BuildDefaultDefn));
+ rules = (BuildRuleDefn*)h->alloc(h, MAX_RULES * sizeof *rules,
+ _Alignof(BuildRuleDefn));
+ if (!targets || !defaults || !rules) goto err;
+ if (package_def_path(root, def_name, package, path) != BUILD_OK) goto err;
+ if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, path,
+ fd) != KIT_OK) {
+ if (err && errcap) snprintf(err, errcap, "failed to read package %s", path);
+ goto err;
+ }
+ defn->targets = targets;
+ defn->cap_targets = MAX_TARGETS;
+ defn->defaults = defaults;
+ defn->cap_defaults = MAX_DEFAULTS;
+ defn->rules = rules;
+ defn->cap_rules = MAX_RULES;
+ if (build_defn_parse(fd->data, fd->size, defn, err, errcap) != BUILD_OK)
+ goto err;
+ return BUILD_OK;
+
+err:
+ release_file_data(c, fd);
+ if (rules) h->free(h, rules, MAX_RULES * sizeof *rules);
+ if (defaults) h->free(h, defaults, MAX_DEFAULTS * sizeof *defaults);
+ if (targets) h->free(h, targets, MAX_TARGETS * sizeof *targets);
+ memset(defn, 0, sizeof *defn);
+ return BUILD_ERR;
+}
+
+static void release_package_defn(KitBuildCoordinator* c, BuildDefn* defn,
+ KitFileData* fd) {
+ KitHeap* h;
+ if (!c || !defn) return;
+ h = c->ctx->heap;
+ release_file_data(c, fd);
+ if (defn->rules)
+ h->free(h, defn->rules, defn->cap_rules * sizeof *defn->rules);
+ if (defn->defaults)
+ h->free(h, defn->defaults,
+ defn->cap_defaults * sizeof *defn->defaults);
+ if (defn->targets)
+ h->free(h, defn->targets, defn->cap_targets * sizeof *defn->targets);
+ memset(defn, 0, sizeof *defn);
+}
+
+static int file_present(KitBuildCoordinator* c, const char* root,
+ const char* rel) {
+ char full[BUILD_PATH_MAX];
+ KitFileData fd;
+ if (!c || !root || !rel) return 0;
+ memset(&fd, 0, sizeof fd);
+ if (path_join2(full, sizeof full, root, rel) != BUILD_OK)
+ return 0;
+ if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, full,
+ &fd) != KIT_OK)
+ return 0;
+ release_file_data(c, &fd);
+ return 1;
+}
+
+static int join_rel2(char* out, size_t cap, const char* a, const char* b) {
+ if (!a || !a[0]) return path_set(out, cap, kit_slice_cstr(b));
+ return path_join2(out, cap, a, b);
+}
+
+static int redo_candidate_at(KitBuildCoordinator* c, const char* workspace_root,
+ const char* root, const char* local,
+ char out[BUILD_PATH_MAX]) {
+ char cand[BUILD_PATH_MAX];
+ char leaf[BUILD_PATH_MAX];
+ size_t i;
+ if (snprintf(leaf, sizeof leaf, "%s.do", local) >= (int)sizeof leaf)
+ return BUILD_ERR;
+ if (join_rel2(cand, sizeof cand, root, leaf) == BUILD_OK &&
+ file_present(c, workspace_root, cand)) {
+ snprintf(out, BUILD_PATH_MAX, "%s", cand);
+ return BUILD_OK;
+ }
+ for (i = 0; local[i]; ++i) {
+ if (local[i] != '.') continue;
+ if (snprintf(leaf, sizeof leaf, "default%s.do", local + i) >=
+ (int)sizeof leaf)
+ return BUILD_ERR;
+ if (join_rel2(cand, sizeof cand, root, leaf) == BUILD_OK &&
+ file_present(c, workspace_root, cand)) {
+ snprintf(out, BUILD_PATH_MAX, "%s", cand);
+ return BUILD_OK;
+ }
+ }
+ if (join_rel2(cand, sizeof cand, root, "default.do") == BUILD_OK &&
+ file_present(c, workspace_root, cand)) {
+ snprintf(out, BUILD_PATH_MAX, "%s", cand);
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static int redo_try_search(KitBuildCoordinator* c, const char* workspace_root,
+ const char* ancestor, const char* search,
+ const char* local, char out[BUILD_PATH_MAX]) {
+ char root[BUILD_PATH_MAX];
+ if (!search || strcmp(search, ".") == 0) {
+ snprintf(root, sizeof root, "%s", ancestor ? ancestor : "");
+ } else if (!ancestor || !ancestor[0]) {
+ if (path_set(root, sizeof root, kit_slice_cstr(search)) != BUILD_OK)
+ return BUILD_ERR;
+ } else if (path_join2(root, sizeof root, ancestor, search) != BUILD_OK) {
+ return BUILD_ERR;
+ }
+ return redo_candidate_at(c, workspace_root, root, local, out);
+}
+
+static int parent_package(char pkg[BUILD_PATH_MAX]) {
+ size_t n;
+ if (!pkg || !pkg[0]) return 0;
+ n = strlen(pkg);
+ while (n > 0u && pkg[n - 1u] != '/') --n;
+ if (n == 0u) {
+ pkg[0] = '\0';
+ } else {
+ pkg[n - 1u] = '\0';
+ }
+ return 1;
+}
+
+static int resolve_redo(KitBuildCoordinator* c, const char* workspace_root,
+ const BuildDefn* defn, const char* package,
+ const char* local, char out[BUILD_PATH_MAX]) {
+ char ancestor[BUILD_PATH_MAX];
+ size_t i;
+ if (!defn || !defn->redo.enabled) return BUILD_ERR;
+ snprintf(ancestor, sizeof ancestor, "%s", package ? package : "");
+ for (;;) {
+ for (i = 0; i < defn->redo.n_search; ++i) {
+ if (redo_try_search(c, workspace_root, ancestor, defn->redo.search[i],
+ local, out) == BUILD_OK)
+ return BUILD_OK;
+ }
+ if (!defn->redo.walk_parents || !parent_package(ancestor)) break;
+ }
+ return BUILD_ERR;
+}
+
+int build_coord_resolve_recipe(KitBuildCoordinator* c, KitSlice canonical_target,
+ BuildRecipeResolution* out) {
+ BuildDefn defn;
+ KitFileData fd;
+ char repo[BUILD_KEY_MAX];
+ char package[BUILD_PATH_MAX];
+ char local[BUILD_TARGET_MAX];
+ char err[160];
+ const char* workspace_root;
+ const char* def_name;
+ const BuildWorkspace* ws;
+ const BuildTargetDefn* target;
+ const BuildRuleDefn* rule = NULL;
+ const BuildDefaultDefn* def = NULL;
+ const char* type = NULL;
+ const char* recipe = NULL;
+ char redo_recipe[BUILD_PATH_MAX];
+ int ambiguous = 0;
+ int recipe_workspace_rel = 0;
+ int ok = BUILD_ERR;
+
+ if (!c || !out ||
+ build_target_split_repo(canonical_target, repo, package, local) !=
+ BUILD_OK)
+ return BUILD_ERR;
+ if (target_workspace(c, canonical_target, repo, &workspace_root, &def_name,
+ &ws) != BUILD_OK)
+ return BUILD_ERR;
+ (void)ws;
+ memset(out, 0, sizeof *out);
+ if (read_package_defn(c, workspace_root, def_name, package, &defn, &fd, err,
+ sizeof err) != BUILD_OK) {
+ build_diagf(c->ctx, "build: %s", err);
+ return BUILD_ERR;
+ }
+
+ target = build_defn_find(&defn, kit_slice_cstr(local));
+ if (target) {
+ if (target->has_recipe) recipe = target->recipe_path;
+ if (target->has_type) type = target->type;
+ } else {
+ rule = build_defn_rule_match(&defn, kit_slice_cstr(local), &ambiguous);
+ if (ambiguous) {
+ build_diagf(c->ctx, "build: ambiguous rules for %.*s",
+ KIT_SLICE_ARG(canonical_target));
+ goto out_release;
+ }
+ if (rule) {
+ if (rule->has_recipe) recipe = rule->recipe_path;
+ if (rule->has_type) type = rule->type;
+ }
+ }
+ if (!recipe &&
+ resolve_redo(c, workspace_root, &defn, package, local, redo_recipe) ==
+ BUILD_OK) {
+ recipe = redo_recipe;
+ recipe_workspace_rel = 1;
+ }
+ if (!recipe && type) {
+ def = build_defn_default_find(&defn, kit_slice_cstr(type));
+ if (def) recipe = def->recipe_path;
+ }
+ if (!recipe) {
+ build_diagf(c->ctx, "build: unknown target %.*s",
+ KIT_SLICE_ARG(canonical_target));
+ goto out_release;
+ }
+ if ((recipe_workspace_rel
+ ? path_set(out->recipe_relpath, sizeof out->recipe_relpath,
+ kit_slice_cstr(recipe))
+ : build_recipe_path_resolve(kit_slice_cstr(package),
+ kit_slice_cstr(recipe),
+ out->recipe_relpath)) != BUILD_OK ||
+ path_join2(out->recipe_abspath, sizeof out->recipe_abspath,
+ workspace_root, out->recipe_relpath) != BUILD_OK)
+ goto out_release;
+ snprintf(out->canonical_target, sizeof out->canonical_target, "%.*s",
+ KIT_SLICE_ARG(canonical_target));
+ snprintf(out->repo, sizeof out->repo, "%s", repo);
+ snprintf(out->package, sizeof out->package, "%s", package);
+ snprintf(out->local_name, sizeof out->local_name, "%s", local);
+ snprintf(out->workspace_root, sizeof out->workspace_root, "%s",
+ workspace_root);
+ ok = BUILD_OK;
+
+out_release:
+ release_package_defn(c, &defn, &fd);
+ return ok;
+}
+
int build_coord_recipe_id(KitBuildCoordinator* c, KitSlice target,
uint8_t out[BUILD_HASH_LEN]) {
- const BuildTargetDefn* t;
- char full[BUILD_PATH_MAX];
+ BuildRecipeResolution r;
KitFileData fd;
KitBlobInfo info;
if (!c || !out) return BUILD_ERR;
- t = build_defn_find(&c->defn, target);
- if (!t) return BUILD_ERR;
- if (path_join2(full, sizeof full, c->workspace_root, t->recipe_path) !=
- BUILD_OK)
+ if (build_coord_resolve_recipe(c, target, &r) != BUILD_OK)
return BUILD_ERR;
fd.data = NULL;
fd.size = 0;
fd.token = NULL;
- if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user, full,
- &fd) != KIT_OK)
+ if (c->host.cas_host->file_io->read_all(c->host.cas_host->file_io->user,
+ r.recipe_abspath, &fd) != KIT_OK)
return BUILD_ERR;
if (kit_cas_add_blob(c->cas, fd.data, fd.size, &info) != KIT_OK) {
if (c->host.cas_host->file_io->release)
diff --git a/src/build/coord.h b/src/build/coord.h
@@ -12,6 +12,7 @@
#include "defn.h"
#include "store.h"
#include "trace.h"
+#include "workspace.h"
/*
* The coordinator context and its process-lifetime in-memory state. All state
@@ -91,6 +92,7 @@ typedef struct BuildPulledSet
typedef struct BuildTargetTable
BuildTargetTable; /* (name,cfg-id,argv-id) -> future */
typedef struct BuildTargetFuture BuildTargetFuture;
+typedef struct BuildExternalWorkspace BuildExternalWorkspace;
struct KitBuildCoordinator {
KitContext ctx_storage; /* private copy so the handle outlives caller's ctx */
@@ -100,10 +102,11 @@ struct KitBuildCoordinator {
char workspace_root[BUILD_PATH_MAX];
char store_root[BUILD_PATH_MAX];
char cas_root[BUILD_PATH_MAX];
+ char build_def_name[BUILD_PATH_MAX];
+ BuildWorkspace root_workspace;
+ BuildExternalWorkspace* externals;
KitCas* cas; /* the shared content store */
BuildStore store; /* paths + record RMW + tree cache over build/ */
- BuildDefn defn; /* parsed build definition (a tracked source) */
- KitFileData defn_bytes; /* the definition file bytes (borrowed/owned by io) */
int jobs_limit; /* effective parallelism (1 when host->sched NULL) */
/* Process-lifetime memos. */
BuildSourceMemo* sources;
@@ -135,9 +138,10 @@ typedef enum BuildStatField {
void build_coord_stat_bump(KitBuildCoordinator*, BuildStatField);
void build_coord_tracef(KitBuildCoordinator*, const char* fmt, ...);
-/* Open/close. open loads + parses the build definition, opens the CAS and
- * store, allocates the memos, and sizes the jobs semaphore (clamped to 1 when
- * the host provides no sched). */
+/* Open/close. open records the workspace/package build-file name, opens the CAS
+ * and store, allocates the memos, and sizes the jobs semaphore (clamped to 1
+ * when the host provides no sched). Package BUILD.kit files are loaded on
+ * demand by target resolution. */
KitStatus build_coord_open(const KitContext*, const KitBuildHost*,
KitSlice store_root, const KitBuildOptions*,
KitBuildCoordinator** out);
@@ -149,6 +153,10 @@ void build_coord_close(KitBuildCoordinator*);
* (absence is a legitimate, cacheable observation). Memoized per process. */
int build_coord_source_hash(KitBuildCoordinator*, KitSlice path,
uint8_t out_blob[BUILD_HASH_LEN], int* present);
+int build_coord_source_hash_target(KitBuildCoordinator*, KitSlice target,
+ KitSlice path,
+ uint8_t out_blob[BUILD_HASH_LEN],
+ int* present);
/* Fetch a pinned blob into the CAS if absent, trying URL hints in order. Returns
* the verified local CAS blob path in path_out. The URL list is untrusted and
@@ -166,6 +174,10 @@ typedef int (*BuildCoordGlobFn)(void* user, const char* path,
int build_coord_glob(KitBuildCoordinator*, KitSlice pattern,
uint8_t out_result_hash[BUILD_HASH_LEN],
BuildCoordGlobFn cb, void* cb_user);
+int build_coord_glob_target(KitBuildCoordinator*, KitSlice target,
+ KitSlice pattern,
+ uint8_t out_result_hash[BUILD_HASH_LEN],
+ BuildCoordGlobFn cb, void* cb_user);
/* Recover a propagated config map / an argv vector by its id (loads + parses
* the serialized CAS blob, memoized) — the replay path for a recorded `need`,
@@ -177,6 +189,30 @@ int build_coord_argv_by_id(KitBuildCoordinator*,
const uint8_t argv_id[BUILD_HASH_LEN],
BuildArgv* out);
+int build_coord_top_config(KitBuildCoordinator*, const KitBuildKV* overrides,
+ size_t noverrides, BuildConfig* out);
+
+typedef struct BuildRecipeResolution {
+ char canonical_target[BUILD_TARGET_MAX];
+ char repo[BUILD_KEY_MAX];
+ char package[BUILD_PATH_MAX];
+ char local_name[BUILD_TARGET_MAX];
+ char workspace_root[BUILD_PATH_MAX];
+ char recipe_relpath[BUILD_PATH_MAX];
+ char recipe_abspath[BUILD_PATH_MAX];
+} BuildRecipeResolution;
+
+/* Canonicalize a label against a package. */
+int build_coord_canonical_target(KitBuildCoordinator*, KitSlice label,
+ KitSlice current_repo,
+ KitSlice current_package,
+ char out[BUILD_TARGET_MAX]);
+
+/* Resolve a canonical target through its live package BUILD.kit and redo
+ * defaults. The returned recipe path is workspace-relative and absolute. */
+int build_coord_resolve_recipe(KitBuildCoordinator*, KitSlice canonical_target,
+ BuildRecipeResolution* out);
+
/* recipe-id = BLAKE2b(recipe file bytes), from the target's definition stanza.
*/
int build_coord_recipe_id(KitBuildCoordinator*, KitSlice target,
diff --git a/src/build/defn.c b/src/build/defn.c
@@ -8,9 +8,14 @@ static int set_err(char* err, size_t cap, const char* msg) {
return BUILD_ERR;
}
-static int slice_valid_token(KitSlice s, size_t cap) {
+static int slice_eq_cstr(KitSlice s, const char* c) {
+ size_t n = c ? strlen(c) : 0u;
+ return s.len == n && (n == 0u || memcmp(s.s, c, n) == 0);
+}
+
+static int token_valid(KitSlice s, size_t cap) {
size_t i;
- if (s.len == 0 || s.len >= cap || !s.s) return 0;
+ if (s.len == 0u || s.len >= cap || !s.s) return 0;
for (i = 0; i < s.len; ++i) {
unsigned char c = (unsigned char)s.s[i];
if (c == '\0' || c == '\n' || c == '\r' || c == ' ' || c == '\t')
@@ -19,15 +24,54 @@ static int slice_valid_token(KitSlice s, size_t cap) {
return 1;
}
-static int rel_path_valid(KitSlice s) {
+static int local_name_valid(KitSlice s) {
+ size_t i;
+ if (!token_valid(s, BUILD_TARGET_MAX)) return 0;
+ for (i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
+ }
+ return 1;
+}
+
+static int type_valid(KitSlice s) {
+ size_t i;
+ if (!token_valid(s, BUILD_KEY_MAX)) return 0;
+ for (i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
+ }
+ return 1;
+}
+
+static int package_valid(KitSlice s) {
+ size_t i, start = 0;
+ if (!s.s && s.len != 0u) return 0;
+ if (s.len >= BUILD_PATH_MAX) return 0;
+ if (s.len == 0u) return 1;
+ if (s.s[0] == '/') return 0;
+ for (i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '/') {
+ size_t n = i - start;
+ if (n == 0u) return 0;
+ if (n == 1u && s.s[start] == '.') return 0;
+ if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
+ start = i + 1u;
+ } else if (s.s[i] == '\0' || s.s[i] == '\\' || s.s[i] == ':') {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int rel_path_valid(KitSlice s, int allow_single_dot) {
size_t i;
size_t start = 0;
- if (!slice_valid_token(s, BUILD_PATH_MAX)) return 0;
+ if (!token_valid(s, BUILD_PATH_MAX)) return 0;
+ if (allow_single_dot && s.len == 1u && s.s[0] == '.') return 1;
if (s.s[0] == '/') return 0;
for (i = 0; i <= s.len; ++i) {
if (i == s.len || s.s[i] == '/') {
size_t n = i - start;
- if (n == 0) return 0;
+ if (n == 0u) return 0;
if (n == 1u && s.s[start] == '.') return 0;
if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
start = i + 1u;
@@ -38,6 +82,21 @@ static int rel_path_valid(KitSlice s) {
return 1;
}
+static int recipe_path_valid(KitSlice s) {
+ if (s.len > 2u && s.s[0] == '/' && s.s[1] == '/')
+ return rel_path_valid((KitSlice){.s = s.s + 2u, .len = s.len - 2u}, 0);
+ return rel_path_valid(s, 0);
+}
+
+static int pattern_valid(KitSlice s) {
+ size_t i;
+ if (!token_valid(s, BUILD_TARGET_MAX)) return 0;
+ for (i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/' || s.s[i] == ':' || s.s[i] == '\\') return 0;
+ }
+ return 1;
+}
+
static int next_line(const uint8_t* data, size_t len, size_t* pos,
KitSlice* line) {
size_t start = *pos;
@@ -67,62 +126,268 @@ static int cstr_slice_cmp(const char* a, KitSlice b) {
}
}
+static int parse_bool(KitSlice s, int* out) {
+ if (slice_eq_cstr(s, "true")) {
+ *out = 1;
+ return BUILD_OK;
+ }
+ if (slice_eq_cstr(s, "false")) {
+ *out = 0;
+ return BUILD_OK;
+ }
+ return BUILD_ERR;
+}
+
+static int split_word(KitSlice line, KitSlice* key, KitSlice* rest) {
+ size_t i = 0;
+ while (i < line.len && line.s[i] != ' ' && line.s[i] != '\t') ++i;
+ key->s = line.s;
+ key->len = i;
+ while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
+ rest->s = line.s + i;
+ rest->len = line.len - i;
+ return key->len != 0u ? BUILD_OK : BUILD_ERR;
+}
+
+static int parse_header(KitSlice line, KitSlice* kind, KitSlice* arg) {
+ size_t i;
+ if (line.len < 3u || line.s[0] != '[' || line.s[line.len - 1u] != ']')
+ return BUILD_ERR;
+ line.s++;
+ line.len -= 2u;
+ for (i = 0; i < line.len && line.s[i] != ' ' && line.s[i] != '\t'; ++i) {}
+ kind->s = line.s;
+ kind->len = i;
+ while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
+ arg->s = line.s + i;
+ arg->len = line.len - i;
+ return kind->len != 0u ? BUILD_OK : BUILD_ERR;
+}
+
+static int line_is_header(KitSlice line) {
+ return line.len >= 2u && line.s[0] == '[' && line.s[line.len - 1u] == ']';
+}
+
+static int glob_match(const char* pat, size_t pn, const char* text, size_t tn) {
+ size_t pi = 0, ti = 0, star = (size_t)-1, mark = 0;
+ while (ti < tn) {
+ if (pi < pn && pat[pi] == '*') {
+ star = pi++;
+ mark = ti;
+ } else if (pi < pn && (pat[pi] == '?' || pat[pi] == text[ti])) {
+ ++pi;
+ ++ti;
+ } else if (star != (size_t)-1) {
+ pi = star + 1u;
+ ti = ++mark;
+ } else {
+ return 0;
+ }
+ }
+ while (pi < pn && pat[pi] == '*') ++pi;
+ return pi == pn;
+}
+
int build_defn_parse(const uint8_t* data, size_t len, BuildDefn* out, char* err,
size_t errcap) {
size_t pos = 0;
KitSlice line;
+ enum { ST_NONE, ST_TARGET, ST_DEFAULT, ST_RULE, ST_REDO } stanza = ST_NONE;
+ BuildTargetDefn* cur_target = NULL;
+ BuildDefaultDefn* cur_default = NULL;
+ BuildRuleDefn* cur_rule = NULL;
if (!data || !out) return set_err(err, errcap, "missing build definition");
- if (out->cap_targets && !out->targets)
- return set_err(err, errcap, "missing target storage");
+ if ((out->cap_targets && !out->targets) ||
+ (out->cap_defaults && !out->defaults) ||
+ (out->cap_rules && !out->rules))
+ return set_err(err, errcap, "missing build definition storage");
out->bytes = data;
out->len = len;
+ out->version = 0;
out->n_targets = 0;
+ out->n_defaults = 0;
+ out->n_rules = 0;
+ memset(&out->redo, 0, sizeof out->redo);
+ out->redo.walk_parents = 1;
- if (next_line(data, len, &pos, &line) != BUILD_OK ||
- !kit_slice_eq_cstr(line, BUILD_DEFN_MAGIC))
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "bad build definition magic/version");
+ if (kit_slice_eq_cstr(line, "kit-build 1")) {
+ out->version = 1;
+ } else if (kit_slice_eq_cstr(line, "kit-build 2")) {
+ out->version = 2;
+ } else {
return set_err(err, errcap, "bad build definition magic/version");
+ }
while (pos < len) {
- KitSlice name, recipe;
- BuildTargetDefn* t;
-
+ KitSlice key, rest, kind, arg;
if (next_line(data, len, &pos, &line) != BUILD_OK)
- return set_err(err, errcap, "unterminated target stanza");
- if (line.len < sizeof("[target ]") || line.s[0] != '[' ||
- memcmp(line.s, "[target ", sizeof("[target ") - 1u) != 0 ||
- line.s[line.len - 1u] != ']')
- return set_err(err, errcap, "expected target stanza");
- name.s = line.s + sizeof("[target ") - 1u;
- name.len = line.len - (sizeof("[target ") - 1u) - 1u;
- if (!slice_valid_token(name, BUILD_TARGET_MAX))
- return set_err(err, errcap, "invalid target name");
- if (out->n_targets > 0 &&
- cstr_slice_cmp(out->targets[out->n_targets - 1u].name, name) >= 0)
- return set_err(err, errcap, "non-canonical target ordering");
- if (out->n_targets >= out->cap_targets)
- return set_err(err, errcap, "too many targets");
+ return set_err(err, errcap, "unterminated stanza");
+ if (line.len == 0u) continue;
- if (next_line(data, len, &pos, &line) != BUILD_OK)
- return set_err(err, errcap, "missing recipe line");
- if (line.len < sizeof("recipe ") ||
- memcmp(line.s, "recipe ", sizeof("recipe ") - 1u) != 0)
- return set_err(err, errcap, "expected recipe line");
- recipe.s = line.s + sizeof("recipe ") - 1u;
- recipe.len = line.len - (sizeof("recipe ") - 1u);
- if (!rel_path_valid(recipe))
- return set_err(err, errcap, "invalid recipe path");
-
- t = &out->targets[out->n_targets++];
- copy_slice(t->name, sizeof t->name, name);
- copy_slice(t->recipe_path, sizeof t->recipe_path, recipe);
+ if (line_is_header(line)) {
+ if (parse_header(line, &kind, &arg) != BUILD_OK)
+ return set_err(err, errcap, "bad stanza header");
+ cur_target = NULL;
+ cur_default = NULL;
+ cur_rule = NULL;
+ if (slice_eq_cstr(kind, "target")) {
+ if (arg.len == 0u) return set_err(err, errcap, "missing target name");
+ if (!local_name_valid(arg))
+ return set_err(err, errcap, "invalid target name");
+ if (out->n_targets > 0 &&
+ cstr_slice_cmp(out->targets[out->n_targets - 1u].name, arg) >= 0)
+ return set_err(err, errcap, "non-canonical target ordering");
+ if (out->n_targets >= out->cap_targets)
+ return set_err(err, errcap, "too many targets");
+ cur_target = &out->targets[out->n_targets++];
+ memset(cur_target, 0, sizeof *cur_target);
+ copy_slice(cur_target->name, sizeof cur_target->name, arg);
+ stanza = ST_TARGET;
+ } else if (slice_eq_cstr(kind, "default")) {
+ if (out->version < 2)
+ return set_err(err, errcap, "default requires kit-build 2");
+ if (arg.len == 0u) return set_err(err, errcap, "missing default type");
+ if (!type_valid(arg)) return set_err(err, errcap, "invalid default type");
+ if (out->n_defaults > 0 &&
+ cstr_slice_cmp(out->defaults[out->n_defaults - 1u].type, arg) >= 0)
+ return set_err(err, errcap, "non-canonical default ordering");
+ if (out->n_defaults >= out->cap_defaults)
+ return set_err(err, errcap, "too many defaults");
+ cur_default = &out->defaults[out->n_defaults++];
+ memset(cur_default, 0, sizeof *cur_default);
+ copy_slice(cur_default->type, sizeof cur_default->type, arg);
+ stanza = ST_DEFAULT;
+ } else if (slice_eq_cstr(kind, "rule")) {
+ if (out->version < 2)
+ return set_err(err, errcap, "rule requires kit-build 2");
+ if (arg.len == 0u) return set_err(err, errcap, "missing rule name");
+ if (!type_valid(arg)) return set_err(err, errcap, "invalid rule name");
+ if (out->n_rules > 0 &&
+ cstr_slice_cmp(out->rules[out->n_rules - 1u].name, arg) >= 0)
+ return set_err(err, errcap, "non-canonical rule ordering");
+ if (out->n_rules >= out->cap_rules)
+ return set_err(err, errcap, "too many rules");
+ cur_rule = &out->rules[out->n_rules++];
+ memset(cur_rule, 0, sizeof *cur_rule);
+ copy_slice(cur_rule->name, sizeof cur_rule->name, arg);
+ stanza = ST_RULE;
+ } else if (slice_eq_cstr(kind, "redo-defaults")) {
+ if (out->version < 2)
+ return set_err(err, errcap, "redo-defaults requires kit-build 2");
+ if (arg.len != 0u && !slice_eq_cstr(arg, "settings"))
+ return set_err(err, errcap, "bad redo-defaults header");
+ stanza = ST_REDO;
+ } else {
+ return set_err(err, errcap, "unknown stanza");
+ }
+ continue;
+ }
+
+ if (split_word(line, &key, &rest) != BUILD_OK)
+ return set_err(err, errcap, "bad stanza line");
+ if (stanza == ST_TARGET) {
+ if (!cur_target) return set_err(err, errcap, "internal target state");
+ if (slice_eq_cstr(key, "recipe")) {
+ if (cur_target->has_recipe || !recipe_path_valid(rest))
+ return set_err(err, errcap, "invalid recipe path");
+ copy_slice(cur_target->recipe_path, sizeof cur_target->recipe_path,
+ rest);
+ cur_target->has_recipe = 1;
+ } else if (slice_eq_cstr(key, "type")) {
+ if (cur_target->has_type || !type_valid(rest))
+ return set_err(err, errcap, "invalid target type");
+ copy_slice(cur_target->type, sizeof cur_target->type, rest);
+ cur_target->has_type = 1;
+ } else {
+ return set_err(err, errcap, "unknown target field");
+ }
+ } else if (stanza == ST_DEFAULT) {
+ if (!cur_default) return set_err(err, errcap, "internal default state");
+ if (!slice_eq_cstr(key, "recipe") || cur_default->recipe_path[0] ||
+ !recipe_path_valid(rest))
+ return set_err(err, errcap, "invalid default field");
+ copy_slice(cur_default->recipe_path, sizeof cur_default->recipe_path,
+ rest);
+ } else if (stanza == ST_RULE) {
+ if (!cur_rule) return set_err(err, errcap, "internal rule state");
+ if (slice_eq_cstr(key, "match")) {
+ if (cur_rule->match[0] || !pattern_valid(rest))
+ return set_err(err, errcap, "invalid rule match");
+ copy_slice(cur_rule->match, sizeof cur_rule->match, rest);
+ } else if (slice_eq_cstr(key, "recipe")) {
+ if (cur_rule->has_recipe || !recipe_path_valid(rest))
+ return set_err(err, errcap, "invalid rule recipe");
+ copy_slice(cur_rule->recipe_path, sizeof cur_rule->recipe_path, rest);
+ cur_rule->has_recipe = 1;
+ } else if (slice_eq_cstr(key, "type")) {
+ if (cur_rule->has_type || !type_valid(rest))
+ return set_err(err, errcap, "invalid rule type");
+ copy_slice(cur_rule->type, sizeof cur_rule->type, rest);
+ cur_rule->has_type = 1;
+ } else {
+ return set_err(err, errcap, "unknown rule field");
+ }
+ } else if (stanza == ST_REDO) {
+ if (slice_eq_cstr(key, "enabled")) {
+ if (parse_bool(rest, &out->redo.enabled) != BUILD_OK)
+ return set_err(err, errcap, "invalid redo enabled");
+ } else if (slice_eq_cstr(key, "walk-parents")) {
+ if (parse_bool(rest, &out->redo.walk_parents) != BUILD_OK)
+ return set_err(err, errcap, "invalid redo walk-parents");
+ } else if (slice_eq_cstr(key, "search")) {
+ KitSlice word;
+ size_t start = 0, i;
+ for (i = 0; i <= rest.len; ++i) {
+ if (i == rest.len || rest.s[i] == ' ' || rest.s[i] == '\t') {
+ if (i == start) {
+ start = i + 1u;
+ continue;
+ }
+ word.s = rest.s + start;
+ word.len = i - start;
+ if (!rel_path_valid(word, 1) ||
+ out->redo.n_search >=
+ sizeof out->redo.search / sizeof out->redo.search[0])
+ return set_err(err, errcap, "invalid redo search");
+ copy_slice(out->redo.search[out->redo.n_search],
+ sizeof out->redo.search[out->redo.n_search], word);
+ ++out->redo.n_search;
+ start = i + 1u;
+ }
+ }
+ } else {
+ return set_err(err, errcap, "unknown redo-defaults field");
+ }
+ } else {
+ return set_err(err, errcap, "field outside stanza");
+ }
+ }
+
+ for (pos = 0; pos < out->n_targets; ++pos) {
+ if (!out->targets[pos].has_recipe && !out->targets[pos].has_type)
+ return set_err(err, errcap, "target has no recipe or type");
+ }
+ for (pos = 0; pos < out->n_defaults; ++pos) {
+ if (!out->defaults[pos].recipe_path[0])
+ return set_err(err, errcap, "default has no recipe");
+ }
+ for (pos = 0; pos < out->n_rules; ++pos) {
+ if (!out->rules[pos].match[0])
+ return set_err(err, errcap, "rule has no match");
+ }
+ if (out->redo.enabled && out->redo.n_search == 0u) {
+ snprintf(out->redo.search[out->redo.n_search++],
+ sizeof out->redo.search[0], ".");
}
return BUILD_OK;
}
const BuildTargetDefn* build_defn_find(const BuildDefn* defn, KitSlice name) {
size_t lo, hi;
- if (!defn || !slice_valid_token(name, BUILD_TARGET_MAX)) return NULL;
+ if (!defn || !local_name_valid(name)) return NULL;
lo = 0;
hi = defn->n_targets;
while (lo < hi) {
@@ -136,3 +401,220 @@ const BuildTargetDefn* build_defn_find(const BuildDefn* defn, KitSlice name) {
}
return NULL;
}
+
+const BuildDefaultDefn* build_defn_default_find(const BuildDefn* defn,
+ KitSlice type) {
+ size_t lo, hi;
+ if (!defn || !type_valid(type)) return NULL;
+ lo = 0;
+ hi = defn->n_defaults;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2u;
+ int cmp = cstr_slice_cmp(defn->defaults[mid].type, type);
+ if (cmp == 0) return &defn->defaults[mid];
+ if (cmp < 0)
+ lo = mid + 1u;
+ else
+ hi = mid;
+ }
+ return NULL;
+}
+
+const BuildRuleDefn* build_defn_rule_match(const BuildDefn* defn,
+ KitSlice name, int* ambiguous) {
+ const BuildRuleDefn* match = NULL;
+ size_t i;
+ if (ambiguous) *ambiguous = 0;
+ if (!defn || !local_name_valid(name)) return NULL;
+ for (i = 0; i < defn->n_rules; ++i) {
+ const BuildRuleDefn* r = &defn->rules[i];
+ if (!glob_match(r->match, strlen(r->match), name.s, name.len)) continue;
+ if (match) {
+ if (ambiguous) *ambiguous = 1;
+ return NULL;
+ }
+ match = r;
+ }
+ return match;
+}
+
+static int last_component(KitSlice s, KitSlice* out) {
+ size_t i = s.len;
+ if (s.len == 0u) return BUILD_ERR;
+ while (i > 0u && s.s[i - 1u] != '/') --i;
+ out->s = s.s + i;
+ out->len = s.len - i;
+ return local_name_valid(*out) ? BUILD_OK : BUILD_ERR;
+}
+
+int build_target_canonicalize(KitSlice label, KitSlice current_repo,
+ KitSlice current_package,
+ char out[BUILD_TARGET_MAX], char* err,
+ size_t errcap) {
+ KitSlice repo = current_repo;
+ KitSlice pkg, local;
+ char repo_buf[BUILD_KEY_MAX];
+ size_t colon = (size_t)-1, i;
+ if (!out || !label.s || label.len == 0u || !package_valid(current_package))
+ return set_err(err, errcap, "invalid target label");
+ if (repo.len && !token_valid(repo, BUILD_KEY_MAX))
+ return set_err(err, errcap, "invalid target repo");
+
+ if (label.s[0] == '@') {
+ KitSlice explicit_repo;
+ size_t slash = 1u;
+ while (slash < label.len && label.s[slash] != '/') ++slash;
+ if (slash <= 1u || slash + 1u >= label.len || label.s[slash + 1u] != '/')
+ return set_err(err, errcap, "bad target label");
+ explicit_repo.s = label.s + 1u;
+ explicit_repo.len = slash - 1u;
+ if (!token_valid(explicit_repo, BUILD_KEY_MAX))
+ return set_err(err, errcap, "bad target repo");
+ if (current_repo.len) {
+ if (current_repo.len + explicit_repo.len + 2u > sizeof repo_buf)
+ return set_err(err, errcap, "target repo too long");
+ memcpy(repo_buf, current_repo.s, current_repo.len);
+ repo_buf[current_repo.len] = '+';
+ memcpy(repo_buf + current_repo.len + 1u, explicit_repo.s,
+ explicit_repo.len);
+ repo_buf[current_repo.len + 1u + explicit_repo.len] = '\0';
+ repo = kit_slice_cstr(repo_buf);
+ } else {
+ repo = explicit_repo;
+ }
+ label.s += slash;
+ label.len -= slash;
+ }
+
+ if (label.len >= 2u && label.s[0] == '/' && label.s[1] == '/') {
+ KitSlice body = {.s = label.s + 2u, .len = label.len - 2u};
+ for (i = 0; i < body.len; ++i) {
+ if (body.s[i] == ':') {
+ if (colon != (size_t)-1) return set_err(err, errcap, "bad target label");
+ colon = i;
+ }
+ }
+ if (colon == (size_t)-1) {
+ pkg = body;
+ if (!package_valid(pkg) || last_component(pkg, &local) != BUILD_OK)
+ return set_err(err, errcap, "bad target label");
+ } else {
+ pkg.s = body.s;
+ pkg.len = colon;
+ local.s = body.s + colon + 1u;
+ local.len = body.len - colon - 1u;
+ if (!package_valid(pkg) || !local_name_valid(local))
+ return set_err(err, errcap, "bad target label");
+ }
+ } else if (label.s[0] == ':') {
+ pkg = current_package;
+ local.s = label.s + 1u;
+ local.len = label.len - 1u;
+ if (!local_name_valid(local))
+ return set_err(err, errcap, "bad target label");
+ } else {
+ pkg = current_package;
+ local = label;
+ if (!local_name_valid(local))
+ return set_err(err, errcap, "bad target label");
+ }
+
+ if (repo.len) {
+ if (repo.len + pkg.len + local.len + 5u > BUILD_TARGET_MAX)
+ return set_err(err, errcap, "target label too long");
+ out[0] = '@';
+ memcpy(out + 1u, repo.s, repo.len);
+ out[1u + repo.len] = '/';
+ out[2u + repo.len] = '/';
+ if (pkg.len) memcpy(out + 3u + repo.len, pkg.s, pkg.len);
+ out[3u + repo.len + pkg.len] = ':';
+ memcpy(out + 4u + repo.len + pkg.len, local.s, local.len);
+ out[4u + repo.len + pkg.len + local.len] = '\0';
+ } else if (pkg.len == 0u) {
+ if (local.len + 4u > BUILD_TARGET_MAX)
+ return set_err(err, errcap, "target label too long");
+ out[0] = '/';
+ out[1] = '/';
+ out[2] = ':';
+ memcpy(out + 3u, local.s, local.len);
+ out[3u + local.len] = '\0';
+ } else {
+ if (pkg.len + local.len + 4u > BUILD_TARGET_MAX)
+ return set_err(err, errcap, "target label too long");
+ out[0] = '/';
+ out[1] = '/';
+ memcpy(out + 2u, pkg.s, pkg.len);
+ out[2u + pkg.len] = ':';
+ memcpy(out + 3u + pkg.len, local.s, local.len);
+ out[3u + pkg.len + local.len] = '\0';
+ }
+ return BUILD_OK;
+}
+
+int build_target_split_repo(KitSlice canonical, char repo[BUILD_KEY_MAX],
+ char package[BUILD_PATH_MAX],
+ char local[BUILD_TARGET_MAX]) {
+ KitSlice pkg, name;
+ size_t i, colon = (size_t)-1, body_start = 0;
+ if (!canonical.s || canonical.len < 4u)
+ return BUILD_ERR;
+ if (repo) repo[0] = '\0';
+ if (canonical.s[0] == '@') {
+ size_t slash = 1u;
+ while (slash < canonical.len && canonical.s[slash] != '/') ++slash;
+ if (slash <= 1u || slash + 1u >= canonical.len ||
+ canonical.s[slash + 1u] != '/')
+ return BUILD_ERR;
+ if (repo) {
+ KitSlice r = {.s = canonical.s + 1u, .len = slash - 1u};
+ if (!token_valid(r, BUILD_KEY_MAX)) return BUILD_ERR;
+ copy_slice(repo, BUILD_KEY_MAX, r);
+ }
+ body_start = slash;
+ }
+ if (canonical.len < body_start + 4u || canonical.s[body_start] != '/' ||
+ canonical.s[body_start + 1u] != '/')
+ return BUILD_ERR;
+ for (i = body_start + 2u; i < canonical.len; ++i) {
+ if (canonical.s[i] == ':') {
+ colon = i;
+ break;
+ }
+ }
+ if (colon == (size_t)-1) return BUILD_ERR;
+ pkg.s = canonical.s + body_start + 2u;
+ pkg.len = colon - body_start - 2u;
+ name.s = canonical.s + colon + 1u;
+ name.len = canonical.len - colon - 1u;
+ if (!package_valid(pkg) || !local_name_valid(name)) return BUILD_ERR;
+ copy_slice(package, BUILD_PATH_MAX, pkg);
+ copy_slice(local, BUILD_TARGET_MAX, name);
+ return BUILD_OK;
+}
+
+int build_target_split(KitSlice canonical, char package[BUILD_PATH_MAX],
+ char local[BUILD_TARGET_MAX]) {
+ return build_target_split_repo(canonical, NULL, package, local);
+}
+
+int build_recipe_path_resolve(KitSlice package, KitSlice recipe,
+ char out[BUILD_PATH_MAX]) {
+ if (!out || !package_valid(package) || !recipe_path_valid(recipe))
+ return BUILD_ERR;
+ if (recipe.len > 2u && recipe.s[0] == '/' && recipe.s[1] == '/') {
+ if (recipe.len - 2u + 1u > BUILD_PATH_MAX) return BUILD_ERR;
+ memcpy(out, recipe.s + 2u, recipe.len - 2u);
+ out[recipe.len - 2u] = '\0';
+ return BUILD_OK;
+ }
+ if (package.len == 0u) {
+ copy_slice(out, BUILD_PATH_MAX, recipe);
+ return BUILD_OK;
+ }
+ if (package.len + 1u + recipe.len + 1u > BUILD_PATH_MAX) return BUILD_ERR;
+ memcpy(out, package.s, package.len);
+ out[package.len] = '/';
+ memcpy(out + package.len + 1u, recipe.s, recipe.len);
+ out[package.len + 1u + recipe.len] = '\0';
+ return BUILD_OK;
+}
diff --git a/src/build/defn.h b/src/build/defn.h
@@ -8,39 +8,60 @@
#include "build.h"
/*
- * The build definition: the file that maps each target name to one recipe. The
- * coordinator reads it to resolve a target. The ONLY definition-derived input of
- * a target is its resolved recipe-id (BLAKE2b of the recipe FILE bytes), carried
- * as a per-node scalar in the deepset and refreshed by recomputing it through
- * the LIVE definition (build_coord_recipe_id). That is content-only and correct:
- * repointing a target to a different-content recipe changes its recipe-id (busts);
- * repointing to a same-content recipe does not (same build); and — crucially —
- * editing an UNRELATED stanza does not change defn_find(T)'s result, so it does
- * NOT bust T. There is therefore NO whole-definition source leaf (an earlier
- * design that would have busted every target on any stanza edit); the `bytes`
- * below are retained only for the parse, not hashed as a dependency.
+ * Package-local build definitions. A workspace is rooted externally by
+ * KitBuildOptions.workspace_root; each package directory has its own BUILD.kit
+ * (or opts->build_def_path basename). The file is a catalog: exact targets,
+ * defaults, simple rules, and redo-style default discovery settings. The ONLY
+ * catalog-derived input of a target is still its resolved recipe-id (BLAKE2b of
+ * the recipe FILE bytes), carried as a per-node scalar in the deepset and
+ * refreshed by recomputing it through the LIVE catalog. The whole BUILD.kit file
+ * is not hashed as a dependency.
*
* The definition does NOT carry argv: local argv is supplied entirely by the
* build request (empty when none is given) and forms the argv-id dimension of
* the resolution key — see KitBuildRequest. Pure parse + lookup, no I/O.
*
- * Format (v1, provisional) — BUILD_DEFN_MAGIC version line then one stanza per
- * target:
+ * Format v1 accepts exact target stanzas. v2 adds target type/default/rule and
+ * redo-defaults stanzas. Target names in package files are local names:
*
* kit-build 1
- * [target //app:server]
+ * [target server]
* recipe recipes/cc.sh
- * [target //lib:core]
+ * [target core]
* recipe recipes/cc.sh
* ...
*/
/* One resolved target stanza: a name and the recipe it maps to. */
typedef struct BuildTargetDefn {
- char name[BUILD_TARGET_MAX];
- char recipe_path[BUILD_PATH_MAX]; /* workspace-relative recipe file */
+ char name[BUILD_TARGET_MAX]; /* local target name */
+ char recipe_path[BUILD_PATH_MAX]; /* package-relative or //workspace path */
+ char type[BUILD_KEY_MAX];
+ int has_recipe;
+ int has_type;
} BuildTargetDefn;
+typedef struct BuildDefaultDefn {
+ char type[BUILD_KEY_MAX];
+ char recipe_path[BUILD_PATH_MAX]; /* package-relative or //workspace path */
+} BuildDefaultDefn;
+
+typedef struct BuildRuleDefn {
+ char name[BUILD_KEY_MAX];
+ char match[BUILD_TARGET_MAX];
+ char recipe_path[BUILD_PATH_MAX]; /* package-relative or //workspace path */
+ char type[BUILD_KEY_MAX];
+ int has_recipe;
+ int has_type;
+} BuildRuleDefn;
+
+typedef struct BuildRedoDefaults {
+ int enabled;
+ int walk_parents;
+ char search[8][BUILD_PATH_MAX]; /* "." means current package/ancestor dir */
+ size_t n_search;
+} BuildRedoDefaults;
+
/* The parsed definition: an index of stanzas over caller-provided storage, plus
* the raw bytes (borrowed) kept alive for the definition's lifetime. The
* definition file as a whole is not a source dependency; only each target's
@@ -48,9 +69,17 @@ typedef struct BuildTargetDefn {
typedef struct BuildDefn {
const uint8_t* bytes; /* the whole definition file, borrowed */
size_t len;
+ int version;
BuildTargetDefn* targets;
size_t n_targets;
size_t cap_targets;
+ BuildDefaultDefn* defaults;
+ size_t n_defaults;
+ size_t cap_defaults;
+ BuildRuleDefn* rules;
+ size_t n_rules;
+ size_t cap_rules;
+ BuildRedoDefaults redo;
} BuildDefn;
/* Parse a definition file into `out` over caller storage. Non-canonical input
@@ -58,7 +87,26 @@ typedef struct BuildDefn {
int build_defn_parse(const uint8_t* data, size_t len, BuildDefn* out, char* err,
size_t errcap);
-/* Find a target stanza by name; returns NULL if absent. */
+/* Find a package-local target stanza by local name; returns NULL if absent. */
const BuildTargetDefn* build_defn_find(const BuildDefn*, KitSlice name);
+const BuildDefaultDefn* build_defn_default_find(const BuildDefn*,
+ KitSlice type);
+const BuildRuleDefn* build_defn_rule_match(const BuildDefn*, KitSlice name,
+ int* ambiguous);
+
+/* Label helpers. Canonical labels are [@repo]//package:name.
+ * `current_repo`/`current_package` are used for relative labels. */
+int build_target_canonicalize(KitSlice label, KitSlice current_repo,
+ KitSlice current_package,
+ char out[BUILD_TARGET_MAX], char* err,
+ size_t errcap);
+int build_target_split_repo(KitSlice canonical, char repo[BUILD_KEY_MAX],
+ char package[BUILD_PATH_MAX],
+ char local[BUILD_TARGET_MAX]);
+int build_target_split(KitSlice canonical, char package[BUILD_PATH_MAX],
+ char local[BUILD_TARGET_MAX]);
+int build_recipe_path_resolve(KitSlice package, KitSlice recipe,
+ char out[BUILD_PATH_MAX]);
+
#endif
diff --git a/src/build/resolve.c b/src/build/resolve.c
@@ -319,8 +319,9 @@ static int shallow_direct_match(KitBuildCoordinator* c,
for (i = 0; i < st->n_sources; ++i) {
uint8_t blob[BUILD_HASH_LEN];
int present = 0;
- if (build_coord_source_hash(c, kit_slice_cstr(st->sources[i].path), blob,
- &present) != BUILD_OK)
+ if (build_coord_source_hash_target(c, kit_slice_cstr(st->target),
+ kit_slice_cstr(st->sources[i].path),
+ blob, &present) != BUILD_OK)
return 0;
if (st->sources[i].absent) {
if (present) return 0;
@@ -330,8 +331,9 @@ static int shallow_direct_match(KitBuildCoordinator* c,
}
for (i = 0; i < st->n_globs; ++i) {
uint8_t hash[BUILD_HASH_LEN];
- if (build_coord_glob(c, kit_slice_cstr(st->globs[i].pattern), hash, NULL,
- NULL) != BUILD_OK)
+ if (build_coord_glob_target(c, kit_slice_cstr(st->target),
+ kit_slice_cstr(st->globs[i].pattern), hash,
+ NULL, NULL) != BUILD_OK)
return 0;
if (!build_id_eq(hash, st->globs[i].result_hash)) return 0;
}
@@ -1108,8 +1110,9 @@ static int build_leafset_refresh_inner(KitBuildCoordinator* c,
for (i = 0; i < leafset->n_sources; ++i) {
uint8_t blob[BUILD_HASH_LEN];
int present = 0;
- if (build_coord_source_hash(c, kit_slice_cstr(leafset->sources[i].path),
- blob, &present) != BUILD_OK)
+ if (build_coord_source_hash_target(c, target,
+ kit_slice_cstr(leafset->sources[i].path),
+ blob, &present) != BUILD_OK)
return BUILD_ERR;
if (leafset->sources[i].absent) {
if (present) goto done;
@@ -1119,8 +1122,9 @@ static int build_leafset_refresh_inner(KitBuildCoordinator* c,
}
for (i = 0; i < leafset->n_globs; ++i) {
uint8_t hash[BUILD_HASH_LEN];
- if (build_coord_glob(c, kit_slice_cstr(leafset->globs[i].pattern), hash,
- NULL, NULL) != BUILD_OK)
+ if (build_coord_glob_target(c, target,
+ kit_slice_cstr(leafset->globs[i].pattern),
+ hash, NULL, NULL) != BUILD_OK)
return BUILD_ERR;
if (!build_id_eq(hash, leafset->globs[i].result_hash)) goto done;
}
diff --git a/src/build/runner.c b/src/build/runner.c
@@ -366,16 +366,15 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
const BuildConfig* cfg, const BuildArgv* argv,
const BuildChainFrame* chain,
BuildResolved* out, int record_traces) {
- const BuildTargetDefn* defn;
+ BuildRecipeResolution recipe;
KitBuildListener* listener = NULL;
KitBuildConn* conn = NULL;
KitBuildProc* proc = NULL;
char endpoint[BUILD_PATH_MAX];
char sandbox[BUILD_PATH_MAX];
char out_dir[BUILD_PATH_MAX];
- char recipe_path[BUILD_PATH_MAX];
KitSlice proc_argv[129];
- KitBuildKV env[131];
+ KitBuildKV env[132];
char env_keys[128][BUILD_KEY_MAX];
size_t argc = 0, nenv = 0, i;
int exit_code = 1;
@@ -394,14 +393,8 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
endpoint[0] = '\0';
sandbox[0] = '\0';
out_dir[0] = '\0';
- recipe_path[0] = '\0';
- defn = build_defn_find(&c->defn, target);
- if (!defn) {
- build_diagf(c->ctx, "build: unknown target %.*s", KIT_SLICE_ARG(target));
- return BUILD_ERR;
- }
- if (path_join2(recipe_path, sizeof recipe_path, c->workspace_root,
- defn->recipe_path) != BUILD_OK)
+ memset(&recipe, 0, sizeof recipe);
+ if (build_coord_resolve_recipe(c, target, &recipe) != BUILD_OK)
return BUILD_ERR;
if (c->host.transport->listen(c->host.transport->user, endpoint,
sizeof endpoint, &listener) != 0 ||
@@ -428,7 +421,7 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
log.cap_pending = sizeof pending / sizeof pending[0];
log.next_token = 1;
- proc_argv[argc++] = kit_slice_cstr(recipe_path);
+ proc_argv[argc++] = kit_slice_cstr(recipe.recipe_abspath);
for (i = 0; i < argv->n && argc < sizeof proc_argv / sizeof proc_argv[0];
++i)
proc_argv[argc++] = kit_slice_cstr(argv->args[i]);
@@ -440,6 +433,8 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
env[nenv++].value = kit_slice_cstr(out_dir);
env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_TARGET);
env[nenv++].value = target;
+ env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_PACKAGE);
+ env[nenv++].value = kit_slice_cstr(recipe.package);
for (i = 0; i < cfg->n && nenv < sizeof env / sizeof env[0]; ++i) {
const char* key = cfg->entries[i].key;
size_t prefix_len = sizeof(KIT_BUILD_ENV_PREFIX) - 1u;
@@ -447,15 +442,15 @@ static int build_run_recipe_impl(KitBuildCoordinator* c, KitSlice target,
if (append_config_observation(&log, cfg, kit_slice_cstr(key),
KIT_SLICE_NULL, 0) != BUILD_OK)
goto out_cleanup;
- snprintf(env_keys[nenv - 3u], sizeof env_keys[nenv - 3u], "%s",
+ snprintf(env_keys[nenv - 4u], sizeof env_keys[nenv - 4u], "%s",
key + prefix_len);
- env[nenv].key = kit_slice_cstr(env_keys[nenv - 3u]);
+ env[nenv].key = kit_slice_cstr(env_keys[nenv - 4u]);
env[nenv].value = kit_slice_cstr(cfg->entries[i].value);
++nenv;
}
if (c->host.exec->spawn(c->host.exec->user, proc_argv, argc, env, nenv,
- kit_slice_cstr(c->workspace_root), &proc) != 0 ||
+ kit_slice_cstr(recipe.workspace_root), &proc) != 0 ||
!proc)
goto out_cleanup;
build_coord_stat_bump(c, BUILD_STAT_RECIPE_RUN);
@@ -495,7 +490,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
const BuildChainFrame* chain,
BuildTestResolved* out,
int record_traces) {
- const BuildTargetDefn* defn;
+ BuildRecipeResolution recipe;
KitBuildListener* listener = NULL;
KitBuildConn* conn = NULL;
KitBuildProc* proc = NULL;
@@ -504,9 +499,8 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
char out_dir[BUILD_PATH_MAX];
char stdout_path[BUILD_PATH_MAX];
char stderr_path[BUILD_PATH_MAX];
- char recipe_path[BUILD_PATH_MAX];
KitSlice proc_argv[129];
- KitBuildKV env[131];
+ KitBuildKV env[132];
char env_keys[128][BUILD_KEY_MAX];
size_t argc = 0, nenv = 0, i;
int exit_code = 1;
@@ -528,14 +522,8 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
out_dir[0] = '\0';
stdout_path[0] = '\0';
stderr_path[0] = '\0';
- recipe_path[0] = '\0';
- defn = build_defn_find(&c->defn, target);
- if (!defn) {
- build_diagf(c->ctx, "build test: unknown target %.*s", KIT_SLICE_ARG(target));
- return BUILD_ERR;
- }
- if (path_join2(recipe_path, sizeof recipe_path, c->workspace_root,
- defn->recipe_path) != BUILD_OK)
+ memset(&recipe, 0, sizeof recipe);
+ if (build_coord_resolve_recipe(c, target, &recipe) != BUILD_OK)
return BUILD_ERR;
if (c->host.transport->listen(c->host.transport->user, endpoint,
sizeof endpoint, &listener) != 0 ||
@@ -567,7 +555,7 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
log.cap_pending = sizeof pending / sizeof pending[0];
log.next_token = 1;
- proc_argv[argc++] = kit_slice_cstr(recipe_path);
+ proc_argv[argc++] = kit_slice_cstr(recipe.recipe_abspath);
for (i = 0; i < argv->n && argc < sizeof proc_argv / sizeof proc_argv[0];
++i)
proc_argv[argc++] = kit_slice_cstr(argv->args[i]);
@@ -579,6 +567,8 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
env[nenv++].value = kit_slice_cstr(out_dir);
env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_TARGET);
env[nenv++].value = target;
+ env[nenv].key = KIT_SLICE_LIT(KIT_BUILD_ENV_PACKAGE);
+ env[nenv++].value = kit_slice_cstr(recipe.package);
for (i = 0; i < cfg->n && nenv < sizeof env / sizeof env[0]; ++i) {
const char* key = cfg->entries[i].key;
size_t prefix_len = sizeof(KIT_BUILD_ENV_PREFIX) - 1u;
@@ -586,16 +576,16 @@ static int build_run_test_recipe_impl(KitBuildCoordinator* c, KitSlice target,
if (append_config_observation(&log, cfg, kit_slice_cstr(key),
KIT_SLICE_NULL, 0) != BUILD_OK)
goto out_cleanup;
- snprintf(env_keys[nenv - 3u], sizeof env_keys[nenv - 3u], "%s",
+ snprintf(env_keys[nenv - 4u], sizeof env_keys[nenv - 4u], "%s",
key + prefix_len);
- env[nenv].key = kit_slice_cstr(env_keys[nenv - 3u]);
+ env[nenv].key = kit_slice_cstr(env_keys[nenv - 4u]);
env[nenv].value = kit_slice_cstr(cfg->entries[i].value);
++nenv;
}
if (c->host.exec->spawn_capture(
c->host.exec->user, proc_argv, argc, env, nenv,
- kit_slice_cstr(c->workspace_root), kit_slice_cstr(stdout_path),
+ kit_slice_cstr(recipe.workspace_root), kit_slice_cstr(stdout_path),
kit_slice_cstr(stderr_path), &proc) != 0 ||
!proc)
goto out_cleanup;
@@ -664,8 +654,13 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
const BuildChainFrame* chain, BuildDepLog* log) {
uint8_t frame[BUILD_FRAME_MAX];
size_t n = 0;
- (void)target;
+ char current_repo[BUILD_KEY_MAX];
+ char current_package[BUILD_PATH_MAX];
+ char current_local[BUILD_TARGET_MAX];
if (!c || !conn || !cfg || !log) return BUILD_ERR;
+ if (build_target_split_repo(target, current_repo, current_package,
+ current_local) != BUILD_OK)
+ return BUILD_ERR;
for (;;) {
BuildReq req;
KitBuildKV overrides[64];
@@ -704,13 +699,17 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
uint8_t blob[BUILD_HASH_LEN];
int present = 0;
char full[BUILD_PATH_MAX];
- if (build_coord_source_hash(c, req.arg, blob, &present) != BUILD_OK ||
+ BuildRecipeResolution recipe;
+ memset(&recipe, 0, sizeof recipe);
+ if (build_coord_source_hash_target(c, target, req.arg, blob, &present) !=
+ BUILD_OK ||
append_source(log, req.arg, blob, present) != BUILD_OK) {
resp_error(&resp, KIT_ERR, "source failed");
} else if (!present) {
resp.status = BUILD_RESP_ABSENT;
- } else if (path_join2(full, sizeof full, c->workspace_root, req.arg.s) !=
- BUILD_OK) {
+ } else if (build_coord_resolve_recipe(c, target, &recipe) != BUILD_OK ||
+ path_join2(full, sizeof full, recipe.workspace_root,
+ req.arg.s) != BUILD_OK) {
resp_error(&resp, KIT_ERR, "source path failed");
} else {
memcpy(resp.id, blob, BUILD_HASH_LEN);
@@ -737,8 +736,8 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
stream.c = c;
stream.conn = conn;
stream.req = req;
- if (build_coord_glob(c, req.arg, result_hash, glob_stream_cb,
- &stream) != BUILD_OK ||
+ if (build_coord_glob_target(c, target, req.arg, result_hash,
+ glob_stream_cb, &stream) != BUILD_OK ||
stream.failed ||
append_glob(log, req.arg, result_hash) != BUILD_OK) {
resp_error(&resp, KIT_ERR, "glob failed");
@@ -755,6 +754,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
BuildResolved r;
uint8_t overlay_id[BUILD_HASH_LEN], cfg_id[BUILD_HASH_LEN],
argv_id[BUILD_HASH_LEN];
+ char dep_target[BUILD_TARGET_MAX];
build_config_init(&dep_cfg, cfg_entries,
sizeof cfg_entries / sizeof cfg_entries[0]);
build_config_init(&overlay_cfg, overlay_entries,
@@ -762,7 +762,10 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
build_config_init(&empty_cfg, NULL, 0);
build_argv_init(&dep_argv, argv_entries,
sizeof argv_entries / sizeof argv_entries[0]);
- if (build_config_overlay(cfg, req.overrides, req.noverrides, &dep_cfg) !=
+ if (build_coord_canonical_target(c, req.arg, kit_slice_cstr(current_repo),
+ kit_slice_cstr(current_package),
+ dep_target) != BUILD_OK ||
+ build_config_overlay(cfg, req.overrides, req.noverrides, &dep_cfg) !=
BUILD_OK ||
build_config_overlay(&empty_cfg, req.overrides, req.noverrides,
&overlay_cfg) != BUILD_OK ||
@@ -770,9 +773,11 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
build_argv_set(&dep_argv, req.argv, req.argc) != BUILD_OK ||
build_config_id(c->ctx->heap, &dep_cfg, cfg_id) != BUILD_OK ||
build_argv_id(c->ctx->heap, &dep_argv, argv_id) != BUILD_OK ||
- build_resolve(c, req.arg, &dep_cfg, &dep_argv, chain, &r) !=
+ build_resolve(c, kit_slice_cstr(dep_target), &dep_cfg, &dep_argv,
+ chain, &r) !=
BUILD_OK ||
- append_dep(log, req.arg, overlay_id, cfg_id, argv_id, &r) !=
+ append_dep(log, kit_slice_cstr(dep_target), overlay_id, cfg_id,
+ argv_id, &r) !=
BUILD_OK) {
resp_error(&resp, KIT_ERR, "need failed");
} else {
@@ -789,6 +794,7 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
BuildPendingNeed* p;
uint8_t overlay_id[BUILD_HASH_LEN], cfg_id[BUILD_HASH_LEN],
argv_id[BUILD_HASH_LEN];
+ char dep_target[BUILD_TARGET_MAX];
char err[128];
build_config_init(&dep_cfg, cfg_entries,
sizeof cfg_entries / sizeof cfg_entries[0]);
@@ -798,6 +804,9 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
build_argv_init(&dep_argv, argv_entries,
sizeof argv_entries / sizeof argv_entries[0]);
if (log->n_pending >= log->cap_pending ||
+ build_coord_canonical_target(c, req.arg, kit_slice_cstr(current_repo),
+ kit_slice_cstr(current_package),
+ dep_target) != BUILD_OK ||
build_config_overlay(cfg, req.overrides, req.noverrides, &dep_cfg) !=
BUILD_OK ||
build_config_overlay(&empty_cfg, req.overrides, req.noverrides,
@@ -806,14 +815,14 @@ int build_runner_service(KitBuildCoordinator* c, KitBuildConn* conn,
build_argv_set(&dep_argv, req.argv, req.argc) != BUILD_OK ||
build_config_id(c->ctx->heap, &dep_cfg, cfg_id) != BUILD_OK ||
build_argv_id(c->ctx->heap, &dep_argv, argv_id) != BUILD_OK ||
- build_dispatch(c, req.arg, &dep_cfg, &dep_argv, chain, &f, err,
- sizeof err) != BUILD_OK) {
+ build_dispatch(c, kit_slice_cstr(dep_target), &dep_cfg, &dep_argv,
+ chain, &f, err, sizeof err) != BUILD_OK) {
resp_error(&resp, KIT_ERR, "need-submit failed");
} else {
p = &log->pending[log->n_pending++];
memset(p, 0, sizeof *p);
p->token = log->next_token++;
- target_copy(req.arg, p->dep);
+ target_copy(kit_slice_cstr(dep_target), p->dep);
memcpy(p->overlay_id, overlay_id, BUILD_HASH_LEN);
memcpy(p->config_id, cfg_id, BUILD_HASH_LEN);
memcpy(p->argv_id, argv_id, BUILD_HASH_LEN);
diff --git a/src/build/workspace.c b/src/build/workspace.c
@@ -0,0 +1,310 @@
+#include "workspace.h"
+
+#include <stdio.h>
+#include <string.h>
+
+static int set_err(char* err, size_t cap, const char* msg) {
+ if (err && cap) snprintf(err, cap, "%s", msg);
+ return BUILD_ERR;
+}
+
+static int slice_eq_cstr(KitSlice s, const char* c) {
+ size_t n = c ? strlen(c) : 0u;
+ return s.len == n && (n == 0u || memcmp(s.s, c, n) == 0);
+}
+
+static int token_valid(KitSlice s, size_t cap) {
+ size_t i;
+ if (!s.s || s.len == 0u || s.len >= cap) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r' || c == ' ' || c == '\t' ||
+ c == '/' || c == ':' || c == '\\')
+ return 0;
+ }
+ return 1;
+}
+
+static int value_valid(KitSlice s, size_t cap) {
+ size_t i;
+ if (!s.s || s.len >= cap) return 0;
+ for (i = 0; i < s.len; ++i) {
+ unsigned char c = (unsigned char)s.s[i];
+ if (c == '\0' || c == '\n' || c == '\r') return 0;
+ }
+ return 1;
+}
+
+static int rel_path_valid(KitSlice s) {
+ size_t i, start = 0;
+ if (!s.s || s.len == 0u || s.len >= BUILD_PATH_MAX) return 0;
+ if (s.s[0] == '/') return 0;
+ for (i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '/') {
+ size_t n = i - start;
+ if (n == 0u) return 0;
+ if (n == 1u && s.s[start] == '.') return 0;
+ if (n == 2u && s.s[start] == '.' && s.s[start + 1u] == '.') return 0;
+ start = i + 1u;
+ } else if (s.s[i] == '\\' || s.s[i] == ':') {
+ return 0;
+ }
+ }
+ return 1;
+}
+
+static int next_line(const uint8_t* data, size_t len, size_t* pos,
+ KitSlice* line) {
+ size_t start = *pos;
+ size_t end = start;
+ while (end < len && data[end] != '\n') ++end;
+ if (end == len) return BUILD_ERR;
+ line->data = data + start;
+ line->len = end - start;
+ *pos = end + 1u;
+ return BUILD_OK;
+}
+
+static void copy_slice(char* dst, size_t cap, KitSlice s) {
+ (void)cap;
+ if (s.len) memcpy(dst, s.s, s.len);
+ dst[s.len] = '\0';
+}
+
+static int split_word(KitSlice line, KitSlice* key, KitSlice* rest) {
+ size_t i = 0;
+ while (i < line.len && line.s[i] != ' ' && line.s[i] != '\t') ++i;
+ key->s = line.s;
+ key->len = i;
+ while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
+ rest->s = line.s + i;
+ rest->len = line.len - i;
+ return key->len != 0u ? BUILD_OK : BUILD_ERR;
+}
+
+static int parse_header(KitSlice line, KitSlice* kind, KitSlice* arg) {
+ size_t i;
+ if (line.len < 3u || line.s[0] != '[' || line.s[line.len - 1u] != ']')
+ return BUILD_ERR;
+ line.s++;
+ line.len -= 2u;
+ for (i = 0; i < line.len && line.s[i] != ' ' && line.s[i] != '\t'; ++i) {}
+ kind->s = line.s;
+ kind->len = i;
+ while (i < line.len && (line.s[i] == ' ' || line.s[i] == '\t')) ++i;
+ arg->s = line.s + i;
+ arg->len = line.len - i;
+ return kind->len != 0u ? BUILD_OK : BUILD_ERR;
+}
+
+static int line_is_header(KitSlice line) {
+ return line.len >= 2u && line.s[0] == '[' && line.s[line.len - 1u] == ']';
+}
+
+void build_workspace_init(BuildWorkspace* ws) {
+ size_t i;
+ if (!ws) return;
+ memset(ws, 0, sizeof *ws);
+ snprintf(ws->def_name, sizeof ws->def_name, "BUILD.kit");
+ for (i = 0; i < sizeof ws->profiles / sizeof ws->profiles[0]; ++i)
+ build_config_init(&ws->profiles[i].cfg, ws->profiles[i].entries,
+ sizeof ws->profiles[i].entries /
+ sizeof ws->profiles[i].entries[0]);
+}
+
+static int parse_hex_id(KitSlice s, uint8_t out[BUILD_HASH_LEN]) {
+ char hex[BUILD_HEX_LEN];
+ if (s.len + 1u != sizeof hex) return BUILD_ERR;
+ memcpy(hex, s.s, s.len);
+ hex[s.len] = '\0';
+ return kit_hex_decode(out, hex, BUILD_HASH_LEN) == KIT_OK ? BUILD_OK
+ : BUILD_ERR;
+}
+
+int build_workspace_parse(const uint8_t* data, size_t len, BuildWorkspace* out,
+ char* err, size_t errcap) {
+ size_t pos = 0;
+ KitSlice line;
+ enum { ST_TOP, ST_CONFIG, ST_EXTERNAL } stanza = ST_TOP;
+ BuildWorkspaceConfigProfile* cur_cfg = NULL;
+ BuildWorkspaceExternal* cur_ext = NULL;
+ if (!data || !out) return set_err(err, errcap, "missing workspace");
+ build_workspace_init(out);
+ if (next_line(data, len, &pos, &line) != BUILD_OK ||
+ !kit_slice_eq_cstr(line, "kit-workspace 1"))
+ return set_err(err, errcap, "bad workspace magic/version");
+ while (pos < len) {
+ KitSlice key, rest, kind, arg;
+ if (next_line(data, len, &pos, &line) != BUILD_OK)
+ return set_err(err, errcap, "unterminated workspace line");
+ if (line.len == 0u) continue;
+ if (line_is_header(line)) {
+ if (parse_header(line, &kind, &arg) != BUILD_OK)
+ return set_err(err, errcap, "bad workspace stanza");
+ cur_cfg = NULL;
+ cur_ext = NULL;
+ if (slice_eq_cstr(kind, "config")) {
+ if (!token_valid(arg, BUILD_KEY_MAX) ||
+ out->n_profiles >= sizeof out->profiles / sizeof out->profiles[0])
+ return set_err(err, errcap, "bad config profile");
+ cur_cfg = &out->profiles[out->n_profiles++];
+ build_config_init(&cur_cfg->cfg, cur_cfg->entries,
+ sizeof cur_cfg->entries / sizeof cur_cfg->entries[0]);
+ copy_slice(cur_cfg->name, sizeof cur_cfg->name, arg);
+ stanza = ST_CONFIG;
+ } else if (slice_eq_cstr(kind, "external")) {
+ if (!token_valid(arg, BUILD_KEY_MAX) ||
+ out->n_externals >=
+ sizeof out->externals / sizeof out->externals[0])
+ return set_err(err, errcap, "bad external repo");
+ cur_ext = &out->externals[out->n_externals++];
+ memset(cur_ext, 0, sizeof *cur_ext);
+ copy_slice(cur_ext->name, sizeof cur_ext->name, arg);
+ stanza = ST_EXTERNAL;
+ } else {
+ return set_err(err, errcap, "unknown workspace stanza");
+ }
+ continue;
+ }
+ if (split_word(line, &key, &rest) != BUILD_OK)
+ return set_err(err, errcap, "bad workspace line");
+ if (stanza == ST_TOP) {
+ if (slice_eq_cstr(key, "name")) {
+ if (!token_valid(rest, BUILD_KEY_MAX))
+ return set_err(err, errcap, "bad workspace name");
+ copy_slice(out->name, sizeof out->name, rest);
+ } else if (slice_eq_cstr(key, "version")) {
+ if (!value_valid(rest, BUILD_VAL_MAX))
+ return set_err(err, errcap, "bad workspace version");
+ copy_slice(out->version, sizeof out->version, rest);
+ } else if (slice_eq_cstr(key, "def-name")) {
+ if (!rel_path_valid(rest) || memchr(rest.s, '/', rest.len))
+ return set_err(err, errcap, "bad workspace def-name");
+ copy_slice(out->def_name, sizeof out->def_name, rest);
+ } else {
+ return set_err(err, errcap, "unknown workspace field");
+ }
+ } else if (stanza == ST_CONFIG) {
+ if (!cur_cfg) return set_err(err, errcap, "internal config state");
+ if (slice_eq_cstr(key, "inherits")) {
+ if (cur_cfg->inherits[0] || !token_valid(rest, BUILD_KEY_MAX))
+ return set_err(err, errcap, "bad config inheritance");
+ copy_slice(cur_cfg->inherits, sizeof cur_cfg->inherits, rest);
+ } else if (build_config_set(&cur_cfg->cfg, key, rest) != BUILD_OK) {
+ return set_err(err, errcap, "bad config entry");
+ }
+ } else if (stanza == ST_EXTERNAL) {
+ if (!cur_ext) return set_err(err, errcap, "internal external state");
+ if (slice_eq_cstr(key, "format")) {
+ if (slice_eq_cstr(rest, "tree"))
+ cur_ext->format = BUILD_WS_EXT_TREE;
+ else if (slice_eq_cstr(rest, "kpkg"))
+ cur_ext->format = BUILD_WS_EXT_KPKG;
+ else if (slice_eq_cstr(rest, "tar.gz"))
+ cur_ext->format = BUILD_WS_EXT_TARGZ;
+ else
+ return set_err(err, errcap, "bad external format");
+ } else if (slice_eq_cstr(key, "archive")) {
+ if (cur_ext->has_archive ||
+ parse_hex_id(rest, cur_ext->archive) != BUILD_OK)
+ return set_err(err, errcap, "bad external archive");
+ cur_ext->has_archive = 1;
+ } else if (slice_eq_cstr(key, "package")) {
+ if (cur_ext->has_package ||
+ parse_hex_id(rest, cur_ext->package) != BUILD_OK)
+ return set_err(err, errcap, "bad external package");
+ cur_ext->has_package = 1;
+ } else if (slice_eq_cstr(key, "url")) {
+ if (!value_valid(rest, BUILD_URL_MAX) ||
+ cur_ext->n_urls >= sizeof cur_ext->urls / sizeof cur_ext->urls[0])
+ return set_err(err, errcap, "bad external url");
+ copy_slice(cur_ext->urls[cur_ext->n_urls],
+ sizeof cur_ext->urls[cur_ext->n_urls], rest);
+ ++cur_ext->n_urls;
+ } else if (slice_eq_cstr(key, "strip-prefix")) {
+ if (!rel_path_valid(rest))
+ return set_err(err, errcap, "bad external strip-prefix");
+ copy_slice(cur_ext->strip_prefix, sizeof cur_ext->strip_prefix, rest);
+ } else {
+ return set_err(err, errcap, "unknown external field");
+ }
+ }
+ }
+ for (pos = 0; pos < out->n_externals; ++pos) {
+ if (out->externals[pos].format == BUILD_WS_EXT_NONE ||
+ !out->externals[pos].has_archive)
+ return set_err(err, errcap, "incomplete external repo");
+ }
+ return BUILD_OK;
+}
+
+const BuildWorkspaceConfigProfile* build_workspace_profile_find(
+ const BuildWorkspace* ws, KitSlice name) {
+ size_t i;
+ if (!ws || !token_valid(name, BUILD_KEY_MAX)) return NULL;
+ for (i = 0; i < ws->n_profiles; ++i) {
+ if (strlen(ws->profiles[i].name) == name.len &&
+ memcmp(ws->profiles[i].name, name.s, name.len) == 0)
+ return &ws->profiles[i];
+ }
+ return NULL;
+}
+
+const BuildWorkspaceExternal* build_workspace_external_find(
+ const BuildWorkspace* ws, KitSlice name) {
+ size_t i;
+ if (!ws || !token_valid(name, BUILD_KEY_MAX)) return NULL;
+ for (i = 0; i < ws->n_externals; ++i) {
+ if (strlen(ws->externals[i].name) == name.len &&
+ memcmp(ws->externals[i].name, name.s, name.len) == 0)
+ return &ws->externals[i];
+ }
+ return NULL;
+}
+
+static int apply_profile(const BuildWorkspace* ws,
+ const BuildWorkspaceConfigProfile* p,
+ BuildConfig* out, int depth, char* err,
+ size_t errcap) {
+ size_t i;
+ if (!p) return BUILD_OK;
+ if (depth > 16) return set_err(err, errcap, "config inheritance cycle");
+ if (p->inherits[0]) {
+ const BuildWorkspaceConfigProfile* parent =
+ build_workspace_profile_find(ws, kit_slice_cstr(p->inherits));
+ if (!parent) return set_err(err, errcap, "missing inherited config");
+ if (apply_profile(ws, parent, out, depth + 1, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ }
+ for (i = 0; i < p->cfg.n; ++i) {
+ if (build_config_set(out, kit_slice_cstr(p->cfg.entries[i].key),
+ kit_slice_cstr(p->cfg.entries[i].value)) != BUILD_OK)
+ return set_err(err, errcap, "workspace config overflow");
+ }
+ return BUILD_OK;
+}
+
+int build_workspace_config_apply(const BuildWorkspace* ws, KitSlice profile,
+ const KitBuildKV* overrides,
+ size_t noverrides, BuildConfig* out,
+ char* err, size_t errcap) {
+ const BuildWorkspaceConfigProfile* def;
+ size_t i;
+ if (!ws || !out) return BUILD_ERR;
+ out->n = 0;
+ def = build_workspace_profile_find(ws, KIT_SLICE_LIT("default"));
+ if (apply_profile(ws, def, out, 0, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ if (profile.s && profile.len) {
+ const BuildWorkspaceConfigProfile* p =
+ build_workspace_profile_find(ws, profile);
+ if (!p) return set_err(err, errcap, "unknown workspace config profile");
+ if (apply_profile(ws, p, out, 0, err, errcap) != BUILD_OK)
+ return BUILD_ERR;
+ }
+ for (i = 0; i < noverrides; ++i) {
+ if (build_config_set(out, overrides[i].key, overrides[i].value) != BUILD_OK)
+ return set_err(err, errcap, "workspace config override overflow");
+ }
+ return BUILD_OK;
+}
diff --git a/src/build/workspace.h b/src/build/workspace.h
@@ -0,0 +1,59 @@
+#ifndef KIT_BUILD_WORKSPACE_H
+#define KIT_BUILD_WORKSPACE_H
+
+#include <kit/core.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "build.h"
+#include "cfg.h"
+
+typedef enum BuildWorkspaceExternalFormat {
+ BUILD_WS_EXT_NONE = 0,
+ BUILD_WS_EXT_TREE,
+ BUILD_WS_EXT_KPKG,
+ BUILD_WS_EXT_TARGZ,
+} BuildWorkspaceExternalFormat;
+
+typedef struct BuildWorkspaceConfigProfile {
+ char name[BUILD_KEY_MAX];
+ char inherits[BUILD_KEY_MAX];
+ BuildConfigEntry entries[64];
+ BuildConfig cfg;
+} BuildWorkspaceConfigProfile;
+
+typedef struct BuildWorkspaceExternal {
+ char name[BUILD_KEY_MAX];
+ BuildWorkspaceExternalFormat format;
+ uint8_t archive[BUILD_HASH_LEN];
+ int has_archive;
+ uint8_t package[BUILD_HASH_LEN];
+ int has_package;
+ char strip_prefix[BUILD_PATH_MAX];
+ char urls[8][BUILD_URL_MAX];
+ size_t n_urls;
+} BuildWorkspaceExternal;
+
+typedef struct BuildWorkspace {
+ char name[BUILD_KEY_MAX];
+ char version[BUILD_VAL_MAX];
+ char def_name[BUILD_PATH_MAX];
+ BuildWorkspaceConfigProfile profiles[16];
+ size_t n_profiles;
+ BuildWorkspaceExternal externals[16];
+ size_t n_externals;
+} BuildWorkspace;
+
+void build_workspace_init(BuildWorkspace*);
+int build_workspace_parse(const uint8_t* data, size_t len, BuildWorkspace* out,
+ char* err, size_t errcap);
+const BuildWorkspaceConfigProfile* build_workspace_profile_find(
+ const BuildWorkspace*, KitSlice name);
+const BuildWorkspaceExternal* build_workspace_external_find(
+ const BuildWorkspace*, KitSlice name);
+int build_workspace_config_apply(const BuildWorkspace*, KitSlice profile,
+ const KitBuildKV* overrides,
+ size_t noverrides, BuildConfig* out,
+ char* err, size_t errcap);
+
+#endif
diff --git a/src/dist/tar.c b/src/dist/tar.c
@@ -120,6 +120,8 @@ int dist_tar_iter(const uint8_t* data, size_t len, DistTarEntry* out,
out[n].name[DIST_PATH_MAX] = '\0';
out[n].data = data + off + TAR_BLOCK;
out[n].size = size;
+ out[n].mode = (unsigned)tar_parse_octal(hdr + TAR_MODE_OFF, 8);
+ out[n].type = (char)hdr[TAR_TYPE_OFF];
++n;
off += TAR_BLOCK + tar_round(size);
}
diff --git a/src/dist/tar.h b/src/dist/tar.h
@@ -24,6 +24,8 @@ typedef struct DistTarEntry {
char name[DIST_PATH_MAX + 1];
const uint8_t* data; /* aliases into the input buffer */
size_t size;
+ unsigned mode;
+ char type;
} DistTarEntry;
/* Parse `data`/`len` into up to `cap` entries, storing the count in *count.
diff --git a/test/build/build_pure_test.c b/test/build/build_pure_test.c
@@ -133,32 +133,103 @@ static void test_config_and_argv(void) {
static void test_defn(void) {
static const char text[] = "kit-build 1\n"
- "[target //app:bin]\n"
- "recipe recipes/app.sh\n"
- "[target //lib:core]\n"
+ "[target bin]\n"
+ "recipe //recipes/app.sh\n"
+ "[target core]\n"
"recipe recipes/lib.sh\n";
+ static const char text2[] = "kit-build 2\n"
+ "\n"
+ "[default c.object]\n"
+ "recipe //recipes/c-object.sh\n"
+ "\n"
+ "[rule object]\n"
+ "match *.o\n"
+ "type c.object\n"
+ "\n"
+ "[redo-defaults]\n"
+ "enabled true\n"
+ "search . do\n"
+ "walk-parents true\n";
static const char unsorted[] = "kit-build 1\n"
- "[target //z]\n"
+ "[target z]\n"
"recipe recipes/z.sh\n"
- "[target //a]\n"
+ "[target a]\n"
"recipe recipes/a.sh\n";
BuildTargetDefn storage[4];
+ BuildDefaultDefn defaults[2];
+ BuildRuleDefn rules[2];
BuildDefn defn;
const BuildTargetDefn* t;
+ const BuildRuleDefn* r;
+ const BuildDefaultDefn* d;
+ char label[BUILD_TARGET_MAX];
+ char package[BUILD_PATH_MAX];
+ char local[BUILD_TARGET_MAX];
+ int ambiguous = 0;
memset(&defn, 0, sizeof defn);
defn.targets = storage;
defn.cap_targets = 4;
+ defn.defaults = defaults;
+ defn.cap_defaults = 2;
+ defn.rules = rules;
+ defn.cap_rules = 2;
EXPECT(build_defn_parse((const uint8_t*)text, sizeof text - 1u, &defn, NULL,
0) == BUILD_OK,
"parse defn");
- t = build_defn_find(&defn, KIT_SLICE_LIT("//lib:core"));
+ t = build_defn_find(&defn, KIT_SLICE_LIT("core"));
EXPECT(t && strcmp(t->recipe_path, "recipes/lib.sh") == 0, "find target");
- EXPECT(build_defn_find(&defn, KIT_SLICE_LIT("//none")) == NULL,
+ EXPECT(build_defn_find(&defn, KIT_SLICE_LIT("none")) == NULL,
"missing target");
EXPECT(build_defn_parse((const uint8_t*)unsorted, sizeof unsorted - 1u, &defn,
NULL, 0) == BUILD_ERR,
"reject unsorted defn");
+ EXPECT(build_defn_parse((const uint8_t*)text2, sizeof text2 - 1u, &defn,
+ NULL, 0) == BUILD_OK,
+ "parse defn v2");
+ d = build_defn_default_find(&defn, KIT_SLICE_LIT("c.object"));
+ EXPECT(d && strcmp(d->recipe_path, "//recipes/c-object.sh") == 0,
+ "find default recipe");
+ r = build_defn_rule_match(&defn, KIT_SLICE_LIT("foo.o"), &ambiguous);
+ EXPECT(r && !ambiguous && strcmp(r->type, "c.object") == 0,
+ "match rule");
+ EXPECT(defn.redo.enabled && defn.redo.walk_parents &&
+ defn.redo.n_search == 2 &&
+ strcmp(defn.redo.search[0], ".") == 0 &&
+ strcmp(defn.redo.search[1], "do") == 0,
+ "parse redo defaults");
+ EXPECT(build_target_canonicalize(KIT_SLICE_LIT("//app"), KIT_SLICE_NULL,
+ KIT_SLICE_NULL, label, NULL, 0) ==
+ BUILD_OK &&
+ strcmp(label, "//app:app") == 0,
+ "canonical package shorthand");
+ EXPECT(build_target_canonicalize(KIT_SLICE_LIT(":lib"),
+ KIT_SLICE_NULL, KIT_SLICE_LIT("pkg/sub"),
+ label, NULL, 0) ==
+ BUILD_OK &&
+ strcmp(label, "//pkg/sub:lib") == 0,
+ "canonical relative label");
+ EXPECT(build_target_canonicalize(KIT_SLICE_LIT("//dep:lib"),
+ KIT_SLICE_LIT("ext"),
+ KIT_SLICE_LIT("pkg/sub"), label, NULL, 0) ==
+ BUILD_OK &&
+ strcmp(label, "@ext//dep:lib") == 0,
+ "canonical repo-rooted label inside external repo");
+ EXPECT(build_target_split_repo(KIT_SLICE_LIT("@ext//dep:lib"), label,
+ package, local) == BUILD_OK &&
+ strcmp(label, "ext") == 0 && strcmp(package, "dep") == 0 &&
+ strcmp(local, "lib") == 0,
+ "split external canonical target");
+ EXPECT(build_target_canonicalize(KIT_SLICE_LIT("@child//dep:lib"),
+ KIT_SLICE_LIT("ext"),
+ KIT_SLICE_LIT("pkg/sub"), label, NULL, 0) ==
+ BUILD_OK &&
+ strcmp(label, "@ext+child//dep:lib") == 0,
+ "canonical nested external repo label");
+ EXPECT(build_target_split(KIT_SLICE_LIT("//pkg/sub:lib"), package, local) ==
+ BUILD_OK &&
+ strcmp(package, "pkg/sub") == 0 && strcmp(local, "lib") == 0,
+ "split canonical target");
}
static void test_trace(void) {
diff --git a/test/buildcoord/run.sh b/test/buildcoord/run.sh
@@ -25,86 +25,68 @@ store="$work/store"
mkdir -p "$ws/recipes" "$ws/remote" "$ws/src/depfile" "$ws/src/globset" \
"$ws/src/tree/nested/deeper" "$store"
+make_pkg() {
+ pkg=$1
+ shift
+ mkdir -p "$ws/$pkg"
+ {
+ printf 'kit-build 1\n'
+ while [ "$#" -gt 0 ]; do
+ printf '[target %s]\nrecipe //recipes/%s\n' "$1" "$2"
+ shift 2
+ done
+ } > "$ws/$pkg/BUILD.kit"
+}
+
+make_pkg absent probe absent.sh
+make_pkg app bundle app.sh
+make_pkg argv argv argv.sh echo argv.sh
+make_pkg cfg default cfg_default.sh probe cfg.sh
+make_pkg client formats client_formats.sh
+make_pkg cycle a cycle_a.sh b cycle_b.sh self cycle_self.sh
+make_pkg defn probe defn_v1.sh unused defn_unused_v1.sh
+make_pkg depfile lines depfile_lines.sh probe depfile.sh
+make_pkg env probe env_probe.sh
+make_pkg extuser probe extuser.sh
+make_pkg fail probe fail.sh
+make_pkg fetch blob fetch.sh format fetch_format.sh
+make_pkg globset probe globset.sh
+make_pkg globstar probe globstar.sh
+make_pkg lib data lib.sh
+make_pkg material probe material.sh
+make_pkg needargv dep argv.sh probe needargv.sh
+make_pkg nondet counter nondet.sh
+make_pkg recipe stamp recipe.sh
+make_pkg repeat probe repeat.sh
+make_pkg scope leaf scope_leaf.sh parent-inherit scope_parent_inherit.sh \
+ parent-override scope_parent_override.sh plain scope_plain.sh
+make_pkg shadow leaf shadow_leaf.sh parent shadow_parent.sh
+make_pkg stable dep stable_dep.sh parent stable_parent.sh
+make_pkg submit echo argv.sh probe submit.sh
+make_pkg test fail test_fail.sh nondet test_nondet.sh parent test_parent.sh \
+ pass test_pass.sh separate test_pass.sh
+make_pkg wscfg probe wscfg.sh
+
+mkdir -p "$ws/redo/do"
+cat > "$ws/redo/BUILD.kit" <<'EOF'
+kit-build 2
+
+[redo-defaults]
+enabled true
+search . do
+walk-parents true
+EOF
+
cat > "$ws/BUILD.kit" <<'EOF'
-kit-build 1
-[target //absent:probe]
-recipe recipes/absent.sh
-[target //app:bundle]
-recipe recipes/app.sh
-[target //argv:echo]
-recipe recipes/argv.sh
-[target //cfg:default]
-recipe recipes/cfg_default.sh
-[target //cfg:probe]
-recipe recipes/cfg.sh
-[target //client:formats]
-recipe recipes/client_formats.sh
-[target //cycle:a]
-recipe recipes/cycle_a.sh
-[target //cycle:b]
-recipe recipes/cycle_b.sh
-[target //cycle:self]
-recipe recipes/cycle_self.sh
-[target //defn:probe]
-recipe recipes/defn_v1.sh
-[target //defn:unused]
-recipe recipes/defn_unused_v1.sh
-[target //depfile:lines]
-recipe recipes/depfile_lines.sh
-[target //depfile:probe]
-recipe recipes/depfile.sh
-[target //env:probe]
-recipe recipes/env_probe.sh
-[target //fail:probe]
-recipe recipes/fail.sh
-[target //fetch:blob]
-recipe recipes/fetch.sh
-[target //fetch:format]
-recipe recipes/fetch_format.sh
-[target //globset:probe]
-recipe recipes/globset.sh
-[target //globstar:probe]
-recipe recipes/globstar.sh
-[target //lib:data]
-recipe recipes/lib.sh
-[target //material:probe]
-recipe recipes/material.sh
-[target //needargv:probe]
-recipe recipes/needargv.sh
-[target //nondet:counter]
-recipe recipes/nondet.sh
-[target //recipe:stamp]
-recipe recipes/recipe.sh
-[target //repeat:probe]
-recipe recipes/repeat.sh
-[target //scope:leaf]
-recipe recipes/scope_leaf.sh
-[target //scope:parent-inherit]
-recipe recipes/scope_parent_inherit.sh
-[target //scope:parent-override]
-recipe recipes/scope_parent_override.sh
-[target //scope:plain]
-recipe recipes/scope_plain.sh
-[target //shadow:leaf]
-recipe recipes/shadow_leaf.sh
-[target //shadow:parent]
-recipe recipes/shadow_parent.sh
-[target //stable:dep]
-recipe recipes/stable_dep.sh
-[target //stable:parent]
-recipe recipes/stable_parent.sh
-[target //submit:probe]
-recipe recipes/submit.sh
-[target //test:fail]
-recipe recipes/test_fail.sh
-[target //test:nondet]
-recipe recipes/test_nondet.sh
-[target //test:parent]
-recipe recipes/test_parent.sh
-[target //test:pass]
-recipe recipes/test_pass.sh
-[target //test:separate]
-recipe recipes/test_pass.sh
+kit-build 2
+
+[target root]
+recipe //recipes/argv.sh
+
+[redo-defaults]
+enabled true
+search . do
+walk-parents true
EOF
cat > "$ws/src/a.txt" <<'EOF'
@@ -157,6 +139,23 @@ for p in $("$KIT" build glob 'src/*.txt'); do
done > "$KIT_BUILD_OUT/sources.txt"
cat "$lib_dir/lib.txt" > "$KIT_BUILD_OUT/lib.txt"
printf '%s\n' "$KIT_BUILD_TARGET" > "$KIT_BUILD_OUT/target.txt"
+printf '%s\n' "$KIT_BUILD_PACKAGE" > "$KIT_BUILD_OUT/package.txt"
+EOF
+
+cat > "$ws/recipes/extuser.sh" <<'EOF'
+#!/bin/sh
+set -eu
+dep=$("$KIT" build need @ext//:data)
+mkdir -p "$KIT_BUILD_OUT"
+cat "$dep/external.txt" > "$KIT_BUILD_OUT/via-external.txt"
+EOF
+
+cat > "$ws/recipes/wscfg.sh" <<'EOF'
+#!/bin/sh
+set -eu
+mode=$("$KIT" build config-get workspace-mode || true)
+mkdir -p "$KIT_BUILD_OUT"
+printf 'workspace:%s\n' "$mode" > "$KIT_BUILD_OUT/workspace.txt"
EOF
cat > "$ws/recipes/absent.sh" <<'EOF'
@@ -352,7 +351,7 @@ cat > "$ws/recipes/needargv.sh" <<'EOF'
#!/bin/sh
set -eu
mkdir -p "$KIT_BUILD_OUT"
-dep_dir=$("$KIT" build need //argv:echo -- dep-one dep-two)
+dep_dir=$("$KIT" build need :dep -- dep-one dep-two)
cat "$dep_dir/args.txt" > "$KIT_BUILD_OUT/dep-args.txt"
EOF
@@ -464,7 +463,7 @@ cat > "$ws/recipes/submit.sh" <<'EOF'
#!/bin/sh
set -eu
mkdir -p "$KIT_BUILD_OUT"
-tok=$("$KIT" build need-submit //argv:echo -- via-submit)
+tok=$("$KIT" build need-submit echo -- via-submit)
dep_dir=$("$KIT" build need-await "$tok")
cat "$dep_dir/args.txt" > "$KIT_BUILD_OUT/submit.txt"
dep_dir_again=$("$KIT" build need-await "$tok")
@@ -534,7 +533,113 @@ mkdir -p "$KIT_BUILD_OUT"
printf 'nondet-test:%s\n' "$count" > "$KIT_BUILD_OUT/nondet.txt"
printf 'nondet-stdout:%s\n' "$count"
EOF
+cat > "$ws/redo/do/default.o.do" <<'EOF'
+#!/bin/sh
+set -eu
+mkdir -p "$KIT_BUILD_OUT"
+printf '%s\n' "$KIT_BUILD_TARGET" > "$KIT_BUILD_OUT/redo.txt"
+EOF
chmod +x "$ws"/recipes/*.sh
+chmod +x "$ws/redo/do/default.o.do"
+
+mkdir -p "$work/extsrc"
+cat > "$work/extsrc/BUILD.kit" <<'EOF'
+kit-build 1
+[target data]
+recipe //build.sh
+EOF
+cat > "$work/extsrc/data.txt" <<'EOF'
+external-data
+EOF
+cat > "$work/extsrc/build.sh" <<'EOF'
+#!/bin/sh
+set -eu
+src=$("$KIT" build source data.txt)
+mode=$("$KIT" build config-get workspace-mode || true)
+mkdir -p "$KIT_BUILD_OUT"
+printf 'external:%s:%s:%s\n' "$(cat "$src")" "$KIT_BUILD_TARGET" "$mode" \
+ > "$KIT_BUILD_OUT/external.txt"
+EOF
+chmod +x "$work/extsrc/build.sh"
+if ! "$KIT" cas add-tree --cas "$store/cas" --root "$work/extsrc" \
+ > "$work/external-tree.out" 2> "$work/external-tree.err"; then
+ cat "$work/external-tree.err" >&2
+ exit 2
+fi
+ext_tree=$(awk 'NR == 1 {print $1}' "$work/external-tree.out")
+case "$ext_tree" in
+ ????????????????????????????????????????????????????????????????)
+ ;;
+ *)
+ echo "could not parse external tree id" >&2
+ exit 2
+ ;;
+esac
+
+have_targz_external=0
+ext_gz_blob=
+if command -v tar >/dev/null 2>&1 && command -v gzip >/dev/null 2>&1; then
+ mkdir -p "$work/targzsrc/extgz-1"
+ cat > "$work/targzsrc/extgz-1/BUILD.kit" <<'EOF'
+kit-build 1
+[target data]
+recipe //build.sh
+EOF
+ cat > "$work/targzsrc/extgz-1/data.txt" <<'EOF'
+external-gzip-data
+EOF
+ cat > "$work/targzsrc/extgz-1/build.sh" <<'EOF'
+#!/bin/sh
+set -eu
+src=$("$KIT" build source data.txt)
+mkdir -p "$KIT_BUILD_OUT"
+printf 'targz:%s:%s\n' "$(cat "$src")" "$KIT_BUILD_TARGET" \
+ > "$KIT_BUILD_OUT/targz.txt"
+EOF
+ chmod +x "$work/targzsrc/extgz-1/build.sh"
+ if (cd "$work/targzsrc" &&
+ COPYFILE_DISABLE=1 tar --format ustar -czf "$work/extgz.tar.gz" \
+ extgz-1) \
+ > "$work/targz-pack.out" 2> "$work/targz-pack.err" &&
+ "$KIT" cas add-blob --cas "$store/cas" "$work/extgz.tar.gz" \
+ > "$work/targz-blob.out" 2> "$work/targz-blob.err"; then
+ ext_gz_blob=$(awk 'NR == 1 {print $1}' "$work/targz-blob.out")
+ case "$ext_gz_blob" in
+ ????????????????????????????????????????????????????????????????)
+ have_targz_external=1
+ ;;
+ *)
+ have_targz_external=0
+ ;;
+ esac
+ fi
+fi
+
+cat > "$ws/WORKSPACE.kit" <<EOF
+kit-workspace 1
+name buildcoord
+version test
+
+[config default]
+workspace-mode default
+
+[config release]
+inherits default
+workspace-mode release
+
+[external ext]
+format tree
+archive $ext_tree
+EOF
+if [ "$have_targz_external" -eq 1 ]; then
+ cat >> "$ws/WORKSPACE.kit" <<EOF
+
+[external gz]
+format tar.gz
+archive $ext_gz_blob
+strip-prefix extgz-1
+EOF
+fi
run_build() {
name=$1
@@ -654,6 +759,7 @@ contains "buildcoord-source-a" "$cold_path/sources.txt" "alpha"
contains "buildcoord-source-b" "$cold_path/sources.txt" "beta"
contains "buildcoord-need-output" "$cold_path/lib.txt" "lib:debug"
contains "buildcoord-target-env" "$cold_path/target.txt" "//app:bundle"
+contains "buildcoord-package-env" "$cold_path/package.txt" "app"
run_build cached //app:bundle
if [ $? -eq 0 ]; then
@@ -706,6 +812,43 @@ run_fail "buildcoord-helper-outside-recipe-fails" \
contains "buildcoord-helper-outside-recipe-diag" \
"$work/buildcoord-helper-outside-recipe-fails.err" "unexpected argument"
+build_assert_ok buildcoord-workspace-default //wscfg:probe
+wscfg_default_path=$(tree_path_from "$work/buildcoord-workspace-default.out")
+contains "buildcoord-workspace-default-output" \
+ "$wscfg_default_path/workspace.txt" "workspace:default"
+
+build_assert_ok buildcoord-workspace-profile --profile release //wscfg:probe
+wscfg_release_path=$(tree_path_from "$work/buildcoord-workspace-profile.out")
+contains "buildcoord-workspace-profile-output" \
+ "$wscfg_release_path/workspace.txt" "workspace:release"
+
+build_assert_ok buildcoord-workspace-profile-override --profile release \
+ --config workspace-mode=cli //wscfg:probe
+wscfg_cli_path=$(tree_path_from \
+ "$work/buildcoord-workspace-profile-override.out")
+contains "buildcoord-workspace-profile-override-output" \
+ "$wscfg_cli_path/workspace.txt" "workspace:cli"
+
+build_assert_ok buildcoord-external-direct @ext//:data
+external_direct_path=$(tree_path_from "$work/buildcoord-external-direct.out")
+contains "buildcoord-external-direct-output" \
+ "$external_direct_path/external.txt" "external:external-data:@ext//:data:default"
+
+build_assert_ok buildcoord-external-need //extuser:probe
+external_need_path=$(tree_path_from "$work/buildcoord-external-need.out")
+contains "buildcoord-external-need-output" \
+ "$external_need_path/via-external.txt" \
+ "external:external-data:@ext//:data:default"
+
+if [ "$have_targz_external" -eq 1 ]; then
+ build_assert_ok buildcoord-external-targz @gz//:data
+ external_targz_path=$(tree_path_from "$work/buildcoord-external-targz.out")
+ contains "buildcoord-external-targz-output" \
+ "$external_targz_path/targz.txt" "targz:external-gzip-data:@gz//:data"
+else
+ kit_skip "buildcoord-external-targz" "host tar/gzip unavailable"
+fi
+
build_assert_ok buildcoord-absent-cold //absent:probe
absent_path=$(tree_path_from "$work/buildcoord-absent-cold.out")
contains "buildcoord-absent-cold-output" "$absent_path/optional.txt" "absent"
@@ -726,6 +869,10 @@ build_assert_ok buildcoord-argv-top //argv:echo -- one two
argv_path=$(tree_path_from "$work/buildcoord-argv-top.out")
contains "buildcoord-argv-top-one" "$argv_path/args.txt" "one"
contains "buildcoord-argv-top-two" "$argv_path/args.txt" "two"
+build_assert_ok buildcoord-argv-package-shorthand //argv -- short
+argv_short_path=$(tree_path_from "$work/buildcoord-argv-package-shorthand.out")
+contains "buildcoord-argv-package-shorthand-output" "$argv_short_path/args.txt" \
+ "short"
build_assert_ok buildcoord-argv-changed //argv:echo -- three
argv_changed_path=$(tree_path_from "$work/buildcoord-argv-changed.out")
contains "buildcoord-argv-changed-output" "$argv_changed_path/args.txt" "three"
@@ -741,6 +888,10 @@ need_argv_path=$(tree_path_from "$work/buildcoord-need-argv.out")
contains "buildcoord-need-argv-one" "$need_argv_path/dep-args.txt" "dep-one"
contains "buildcoord-need-argv-two" "$need_argv_path/dep-args.txt" "dep-two"
+build_assert_ok buildcoord-redo-default //redo:foo.o
+redo_path=$(tree_path_from "$work/buildcoord-redo-default.out")
+contains "buildcoord-redo-default-target" "$redo_path/redo.txt" "//redo:foo.o"
+
build_assert_ok buildcoord-recipe-v1 //recipe:stamp
recipe_v1_path=$(tree_path_from "$work/buildcoord-recipe-v1.out")
contains "buildcoord-recipe-v1-output" "$recipe_v1_path/stamp.txt" "recipe:v1"
@@ -1097,26 +1248,26 @@ contains "buildcoord-defn-v1-run" "$work/buildcoord-defn-v1.err" \
"recipes_run=1"
defn_v1_path=$(tree_path_from "$work/buildcoord-defn-v1.out")
contains "buildcoord-defn-v1-output" "$defn_v1_path/defn.txt" "defn:v1"
-sed 's|recipe recipes/defn_v1.sh|recipe recipes/defn_same.sh|' \
- "$ws/BUILD.kit" > "$ws/BUILD.next"
-mv "$ws/BUILD.next" "$ws/BUILD.kit"
+sed 's|recipe //recipes/defn_v1.sh|recipe //recipes/defn_same.sh|' \
+ "$ws/defn/BUILD.kit" > "$ws/defn/BUILD.next"
+mv "$ws/defn/BUILD.next" "$ws/defn/BUILD.kit"
build_assert_ok buildcoord-defn-same-content --stats //defn:probe
contains "buildcoord-defn-same-content-hit" \
"$work/buildcoord-defn-same-content.err" "deep_hits=1"
contains "buildcoord-defn-same-content-no-run" \
"$work/buildcoord-defn-same-content.err" "recipes_run=0"
-sed 's|recipe recipes/defn_same.sh|recipe recipes/defn_v2.sh|' \
- "$ws/BUILD.kit" > "$ws/BUILD.next"
-mv "$ws/BUILD.next" "$ws/BUILD.kit"
+sed 's|recipe //recipes/defn_same.sh|recipe //recipes/defn_v2.sh|' \
+ "$ws/defn/BUILD.kit" > "$ws/defn/BUILD.next"
+mv "$ws/defn/BUILD.next" "$ws/defn/BUILD.kit"
build_assert_ok buildcoord-defn-different-content --stats //defn:probe
contains "buildcoord-defn-different-content-run" \
"$work/buildcoord-defn-different-content.err" "recipes_run=1"
defn_v2_path=$(tree_path_from "$work/buildcoord-defn-different-content.out")
contains "buildcoord-defn-different-content-output" "$defn_v2_path/defn.txt" \
"defn:v2"
-sed 's|recipe recipes/defn_unused_v1.sh|recipe recipes/defn_unused_v2.sh|' \
- "$ws/BUILD.kit" > "$ws/BUILD.next"
-mv "$ws/BUILD.next" "$ws/BUILD.kit"
+sed 's|recipe //recipes/defn_unused_v1.sh|recipe //recipes/defn_unused_v2.sh|' \
+ "$ws/defn/BUILD.kit" > "$ws/defn/BUILD.next"
+mv "$ws/defn/BUILD.next" "$ws/defn/BUILD.kit"
build_assert_ok buildcoord-defn-unrelated-stanza --stats //defn:probe
contains "buildcoord-defn-unrelated-stanza-hit" \
"$work/buildcoord-defn-unrelated-stanza.err" "deep_hits=1"
diff --git a/test/tier1/system/buildcoord-kitchen.sh b/test/tier1/system/buildcoord-kitchen.sh
@@ -23,19 +23,24 @@ ws="$work/ws"
store="$work/store"
mkdir -p "$ws/recipes" "$ws/src" "$store"
-cat > "$ws/BUILD.kit" <<'EOF'
-kit-build 1
-[target //app:bundle]
-recipe recipes/app.sh
-[target //argv:echo]
-recipe recipes/argv.sh
-[target //fail:probe]
-recipe recipes/fail.sh
-[target //lib:data]
-recipe recipes/lib.sh
-[target //test:pass]
-recipe recipes/test_pass.sh
-EOF
+make_pkg() {
+ pkg=$1
+ shift
+ mkdir -p "$ws/$pkg"
+ {
+ printf 'kit-build 1\n'
+ while [ "$#" -gt 0 ]; do
+ printf '[target %s]\nrecipe //recipes/%s\n' "$1" "$2"
+ shift 2
+ done
+ } > "$ws/$pkg/BUILD.kit"
+}
+
+make_pkg app bundle app.sh
+make_pkg argv echo argv.sh
+make_pkg fail probe fail.sh
+make_pkg lib data lib.sh
+make_pkg test pass test_pass.sh
cat > "$ws/src/a.txt" <<'EOF'
alpha