Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Use this table to locate source files. ALWAYS read the relevant source file befo
| `ResourceRequestGraph` | `lib/build/cache/ResourceRequestGraph.js` | DAG of request sets with delta encoding and best-parent optimization |
| `ResourceIndex` | `lib/build/cache/index/ResourceIndex.js` | Wrapper around hash trees with delta detection |
| `HashTree` | `lib/build/cache/index/HashTree.js` | Directory-based Merkle tree for resource hashing |
| `TaskInputSet` | `lib/build/cache/index/TaskInputSet.js` | Flat set of a task's non-resource inputs (env vars, tracked TaskUtil reads), hashed into one signature folded into the task's project-component signature. A sibling of `HashTree` (shares the `version`/`toCacheObject`/`fromCache` conventions) but deliberately not a Merkle tree: inputs are few, unordered, and non-hierarchical. Persists only entry type/name; values are re-read on lookup. Exports `normalizeInputValue`, the shared record/lookup value normalizer |
| `TaskInputSet` | `lib/build/cache/index/TaskInputSet.js` | Flat set of a task's non-resource inputs (env vars, tracked TaskUtil reads), hashed into the input component of the stage signature. A sibling of `HashTree` (shares the `version`/`toCacheObject`/`fromCache` conventions) but deliberately not a Merkle tree: inputs are few, unordered, and non-hierarchical. Persists only entry type/name; values are re-read on lookup. Exports `normalizeInputValue`, the shared record/lookup value normalizer |
| `SharedHashTree` | `lib/build/cache/index/SharedHashTree.js` | HashTree with structural sharing via TreeRegistry |
| `TreeRegistry` | `lib/build/cache/index/TreeRegistry.js` | Batch update coordinator for shared trees |
| `TreeNode` | `lib/build/cache/index/TreeNode.js` | Merkle tree node (resource or directory) |
Expand Down Expand Up @@ -273,14 +273,24 @@ The cache uses content-based signatures at multiple levels:
| **Stage signature** | An explicit four-component tuple `projectIndexSignature-dependencyIndexSignature-inputSignature-rootSignature`, joined by `-` (each component a SHA-256 hex digest, so the separator never occurs inside a component and the split is lossless). The four dimensions (project resources, dependency resources, non-resource **input signature**, root resources) are independent slots rather than folded into two, so a delta pairs a changed project or dependency signature with the current input and root signatures with no reverse mapping. The join/split primitives live in `cache/stageSignature.js`; `BuildStageCache.getStageSignatures()` composes the exact-match candidates. One stage per step for a step-based task, one per legacy task | `ProjectBuildCache.prepareStageExecutionAndValidateCache()`, `BuildStageCache.getStageSignatures()` |
| **Result signature** | A four-component tuple `sourceSignature-combinedDependencySignature-aggregatedInputSignature-aggregatedRootSignature`. The dependency component is a cartesian product over per-stage candidate dependency signatures; the aggregated input and root signatures are single current values (constants across the product). Both the candidate list and the stored signature derive the per-stage dependency list from the single stage order (`#stageOrder`), so the store and lookup sides cover the same stages in the same order (a declared stage missing a signature throws rather than silently never matching). Grows by one factor per stage, bounded because only dependency-reading stages with a delta contribute a factor >1 | `ProjectBuildCache.#getResultStageSignature()` / `#getPossibleResultStageSignatures()` |

### Cache Validation

Persistent-cache initialization restores the stage order and every stage's project, dependency, input, and root request metadata before result validation starts. The first validation therefore computes result-signature candidates from the current source and dependency indices, freshly resolved non-resource input values, and refreshed root indices. A matching result can be imported without executing its stages. A miss continues to per-stage lookup, where the same four stage-signature dimensions decide which stages can be restored.

`ProjectBuildContext` and its `ProjectBuildCache` are reused across sequential runs. The cache must compare the current aggregated input signature with the aggregate used by the last successful validation or build before a fresh in-memory result bypasses `#findResultCache()`. The comparison must participate in the early `possiblyRequiresBuild()` decision as well as `ProjectBuildCache.validateCache()`, because `ProjectBuilder` can otherwise skip validation for a cache already marked fresh.

### Cache Modes

`Default` reads valid cache entries and rebuilds missing or invalid results. `ReadOnly` uses valid entries but does not persist new results. `Off` bypasses cache lookup and persistence. `Force` permits only a valid cached result and must fail before task execution whenever any result-signature dimension has no match. The Force check belongs at the common result-validation boundary so source, dependency, non-resource input, root-resource, and metadata failures follow the same rule.

### Non-Resource Task Inputs

A task's output can depend on inputs that are not resources: an environment variable, or a value read through the `TaskUtil` interface (`isRootProject()`, `getDependencies()`, a dependency's version via `getProject(name).getVersion()`, `getCustomConfiguration()`, framework getters). None of these feed the resource indices or the build signature, so without tracking, changing one between builds leaves a stale cached result being served. The canonical example: `generateLibraryManifest` embeds a dependency's version as the manifest `minVersion` via `getProject(depName).getVersion()`; removing or bumping that dependency must re-run the task even though no source resource changed.

Tracking has a record side and a lookup side, mirroring the resource-request flow:

- **Record** (task executes): the TaskRunner hands the task a `MonitoredTaskUtil` instead of the raw `TaskUtil`. It is a Proxy that preserves the wrapped shape (a custom task's limited interface stays limited) and records every tracked read as `{type, name, value}`, normalizing the value via `normalizeInputValue`. `getProject(name)` returns a wrapped project whose tracked accessors record under the project's name; its `getReader()` result is wrapped in a `MonitoredReader` so the resources the task reads through it are recorded as resource requests, and, for the project being built, its `getRootReader()` result is wrapped too so reads of files outside the UI5 resource model are recorded as root requests (see the reader-monitoring note below), while untracked members (`getRootPath`, `getSpecVersion`, ...) pass straight through unrecorded. After the task, the TaskRunner drains `getInputRecording()` into `ProjectBuildCache.recordStageResult`, which builds a `TaskInputSet` and folds its signature into the task's project-component signature (`combineProjectAndInputSignature`). Only entry type/name are persisted (`stage_request_metadata` type `"input"`), never values.
- **Lookup** (later build): `BuildStageCache.getInputSignature(resolveValue)` recomputes the input signature, re-reading each recorded input's *current* value through `ProjectBuildContext.resolveInputValue(type, name)` (which reaches `process.env` and the current project graph). A value that differs from the one baked into the cached stage signature misses the cache and re-runs the task. Record and lookup normalize through the same `normalizeInputValue`, so equal values compare equal.
- **Record** (task executes): the TaskRunner hands the task a `MonitoredTaskUtil` instead of the raw `TaskUtil`. It is a Proxy that preserves the wrapped shape (a custom task's limited interface stays limited) and records every tracked read as `{type, name, value}`, normalizing the value via `normalizeInputValue`. `getProject(name)` returns a wrapped project whose tracked accessors record under the project's name; its `getReader()` result is wrapped in a `MonitoredReader` so the resources the task reads through it are recorded as resource requests, and, for the project being built, its `getRootReader()` result is wrapped too so reads of files outside the UI5 resource model are recorded as root requests (see the reader-monitoring note below), while untracked members (`getRootPath`, `getSpecVersion`, ...) pass straight through unrecorded. After the task, the TaskRunner drains `getInputRecording()` into `ProjectBuildCache.recordStageResult`. `BuildStageCache.recordRequests()` builds a `TaskInputSet` and stores its signature in the non-resource input slot of the explicit `[project, dependency, input, root]` stage-signature tuple. Only entry type/name are persisted (`stage_request_metadata` type `"input"`), never values.
- **Lookup** (later build): `BuildStageCache.getInputSignature(resolveValue)` recomputes the input signature, re-reading each recorded input's *current* value through `ProjectBuildContext.resolveInputValue(type, name)` (which reaches `process.env` and the current project graph). Project-level result lookup hashes the current per-stage input signatures into `aggregatedInputSignature`, while stage lookup uses each current input signature directly. A value that differs from the one baked into a cached signature misses that result or stage once validation reaches the corresponding lookup. Record and lookup normalize through the same `normalizeInputValue`, so equal values compare equal.

Excluded from input-value tracking: mutations (`setTag`/`clearTag`, already captured as tag operations in the hash trees), constructors (`resourceFactory`), readers (`getReader`/`getRootReader`), side effects (`registerCleanupTask`), and the FS-path accessors (`getRootPath`/`getSourcePath`) whose absolute, machine-specific values would make cache entries non-portable.

Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions .claude/skills
Loading