Skip to content

feat: Incremental Build#1267

Draft
RandomByte wants to merge 259 commits into
mainfrom
feat/incremental-build-4
Draft

feat: Incremental Build#1267
RandomByte wants to merge 259 commits into
mainfrom
feat/incremental-build-4

Conversation

@RandomByte
Copy link
Copy Markdown
Member

Implementation of RFC 0017 Incremental Build

This PR supersedes previous PoC: #1238

JIRA: CPOUI5FOUNDATION-1174

@RandomByte RandomByte marked this pull request as draft January 7, 2026 12:28
@coveralls
Copy link
Copy Markdown

coveralls commented Jan 7, 2026

Coverage Status

coverage: 99.566% (+4.9%) from 94.658%
when pulling b727ebc on feat/incremental-build-4
into cb29ec1 on main.

@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch 3 times, most recently from 5224cd2 to 4904c84 Compare January 9, 2026 09:22
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 950fc6d to 41eed91 Compare January 14, 2026 15:28
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch from bb39565 to 2a21507 Compare January 20, 2026 10:01
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch 3 times, most recently from 6233816 to f858659 Compare January 20, 2026 16:58
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 71db1d0 to a2c371f Compare January 26, 2026 09:54
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 7364b4b to cf43f0c Compare February 9, 2026 13:32
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch 2 times, most recently from 252b966 to 874a943 Compare February 16, 2026 17:17
Comment thread packages/project/test/fixtures/application.a/custom-tasks/custom-task-2.js Outdated
Comment thread packages/project/test/fixtures/application.a/custom-tasks/custom-task-0.js Outdated
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch 5 times, most recently from df275e5 to 0345502 Compare February 27, 2026 10:34
Comment thread packages/project/test/fixtures/application.a/task.dependency-change.js Outdated
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 9fc2509 to 20ba653 Compare March 5, 2026 16:23
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch from 20ba653 to 9fc2509 Compare March 5, 2026 16:34
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 9fc2509 to 940376d Compare March 5, 2026 16:40
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 9197670 to 44d1107 Compare March 20, 2026 15:05
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch from 44d1107 to d7c402c Compare March 26, 2026 14:05
@maxreichmann
Copy link
Copy Markdown
Member

maxreichmann commented Mar 26, 2026

Rebased onto origin/main

Comment thread internal/e2e-tests/test/build.js Fixed
Comment thread internal/e2e-tests/test/version.js Fixed
… Module

Project._addReadersForWriter used unshift which reversed the stage
priority order, causing earlier stages (e.g. replaceCopyright) to
shadow later stages (e.g. replaceVersion) in the result reader chain.
Change to push so that later stages have higher priority, matching
ComponentProject behavior.
…hed tags

The build manifest version was bumped to "1.0" but the version check in
ProjectBuildContext only accepted "0.1" and "0.2", causing pre-built
dependencies to be rebuilt from scratch instead of being skipped.

Also ensure getProjectResourceTagCollection() applies cached stage tags
before returning the collection, matching the pattern already used by
getResourceTagCollection().
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from 0670d94 to d029efb Compare May 11, 2026 08:45
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch 3 times, most recently from 2ec8b8f to 4be8ab8 Compare May 13, 2026 11:34
@maxreichmann maxreichmann force-pushed the feat/incremental-build-4 branch from 4be8ab8 to 7164b79 Compare May 13, 2026 12:35
RandomByte and others added 21 commits May 20, 2026 14:34
Add tests for previously uncovered modules: MonitoredReader,
MonitoredReaderWriter, MonitoredResourceTagCollection, and Proxy reader.
Also extend existing tests for ResourceTagCollection, resourceFactory,
and AbstractReaderWriter to cover missing functions. Functions coverage
rises from 81% to 95%.
Add targeted tests to bring @ui5/project coverage above the configured
thresholds (95% statements, 90% branches, 95% functions, 95% lines).
Also fix AVA timeout for parallel test execution and remove dead code
module utils/sanitizeFileName.js.

New test files:
- build/cache/utils.js, index/TreeNode.js, index/ResourceIndex.js
- build/BuildReader.js, helpers/WatchHandler.js, helpers/ProjectBuilderOutputStyle.js
- ui5framework/maven/CacheMode.js

Extended existing tests:
- HashTree.js, SharedHashTree.js, CacheManager.js
- specifications/Project.js, types/Component.js, types/Library.js
- types/Module.js, types/ThemeLibrary.js, extensions/Task.js
The CacheManager is a singleton per cache directory. When multiple
concurrent builds share the same singleton (e.g. in parallel test
execution), the first build to finish would close the database and
break any other build still using it.

Track the number of active consumers via a reference count that is
incremented on each create() call and decremented on close(). The
underlying SQLite database is only closed when the last consumer
releases its reference.
… BuildServer

When source files are modified while a build is in progress (detected by
condition rather than a fatal error. This fixes a race condition where
the build takes close to or exceeds 500ms, causing the integration
test's file writes to land during source revalidation.

- Add SourceChangedDuringBuildError for typed error handling
- Handle it alongside AbortBuildError in BuildServer (re-queue projects)
- Reset source index state on error to prevent infinite retry loops
- Pass abort signal to allTasksCompleted() so watcher-triggered aborts
  take precedence over the source-change error
Replace fragile setTimeout(0) with deterministic promise-based signals
to wait for the handler's async work to complete. On Node 22, esmock's
off-thread ESM loader hooks can take more than one microtask tick to
resolve dynamic imports, causing afterEach's esmock.purge() to nullify
the mock cache before the handler's imports resolve.

Use handlerReady (resolves on first stdout write) for most tests, and
openCalled (resolves when the open stub fires) for --open tests which
have an additional dynamic import after stdout output.
… handles

On Windows, SQLite kept the cache.db file handle open after close,
preventing subsequent test runs and concurrent builds from cleaning their
output directories (fs.rm fails with EBUSY). The root cause is twofold:
WAL-mode SQLite retains file mappings until a checkpoint, and the
CacheManager singleton was not being released by all consumers.

Resolve the issue with:

- Add `PRAGMA wal_checkpoint(TRUNCATE)` before close in BuildCacheStorage
  to flush WAL content back to the main database and release file handles
  cleanly. This single change resolves the underlying handle leak across
  cache tests and integration tests without per-test workarounds.
- Close the CacheManager at the end of `buildToTarget()`, after all
  deferred disk writes have completed.
- Expose a public `ProjectBuilder#closeCacheManager()` method and call
  it from `BuildServer.destroy()` so long-running consumers release the
  database handle on shutdown.
- Add a `#destroyed` flag to BuildServer to prevent new builds from
  being triggered after destroy(). Clear pending build queue and timeout
  before awaiting active build to avoid races with setTimeout-based
  queue processing.
- Document the lifecycle on `ProjectBuilder#build`, `#buildToTarget`, and
  `#closeCacheManager`: `buildToTarget` closes the CacheManager itself;
  consumers of `build` must call `closeCacheManager` when done.
- Close singleton instances in CacheManager `create()` test, make
  `after.always` rimraf best-effort in cache tests (test/tmp is already
  cleaned at start of each run), and add maxRetries to rimraf in
  integration tests for any residual Windows handle-release delays.
Add tests for previously untested error-handling paths:

- lib/server.js: HTTP server error event handler, buildServer error
  event forwarding, and the close() rejection handler when
  buildServer.destroy() fails.
- middleware/serveIndex: byGlob rejection.
- sslUtil: chmod failure callbacks when fixing outdated file permissions.
…e during index re-init

After a SourceChangedDuringBuildError, the source index is nulled and state
reset to RESTORING_PROJECT_INDICES. If projectSourcesChanged or
dependencyResourcesChanged was called before initSourceIndex ran again, the
state was incorrectly transitioned to REQUIRES_UPDATE, causing initSourceIndex
to skip initialization and a subsequent NPE on the null source index.
… and missing error handler

Close CacheManager in a finally block within buildToTarget() so the
SQLite database is released even when the build throws. Previously,
builds that threw (e.g. SourceChangedDuringBuildError) leaked a refcount,
keeping the DB memory-mapped on Windows and preventing cleanup by
subsequent tests.

Add an error event handler to the JSDoc child process spawn. Without it,
a failed spawn on Windows would never emit close, hanging the promise
indefinitely.
…esource.setStream

Streams from older userland packages like readable-stream@2 (used by
replacestream) have .pipe but no Symbol.asyncIterator, causing
"stream is not async iterable" errors when the resource content is later
consumed by node:stream/consumers. Also handle WHATWG TransformStream
objects by unwrapping to their readable side.
The custom task reads dependencies via taskUtil.getProject().getReader()
which bypasses the monitored dependencies reader. These reads are not
tracked by ResourceRequestManager, so dependency changes don't
invalidate the application's result cache.
…c-generate

Replace type expressions unsupported by JSDoc's Catharsis parser:
- Union types inside Record literals in generics ({string|number|...})
- TypeScript-style import() type syntax
- Tuple types ([T, U]) inside generics
- Optional property syntax (?) inside Record generics
…ware configuration (#1367)

JIRA: CPOUI5FOUNDATION-1206

---------

Co-authored-by: Copilot <copilot@github.com>
matchResourceMetadataStrict and the duplicated fast path in
HashTree#upsertResources Phase 1 returned "unchanged" whenever a file's
mtime matched the cached value, without consulting size or content.
External operations that preserve mtime - cp -p, tar -x, rsync -t, and
atomic rename of a temp file with copied stat - therefore passed silently
as cache hits and produced stale build output.

Verify size before the mtime-based short-circuit can return true. This
matches the cheap-path semantics used by rsync's -t quick-check and
git's stat-bundle compare, and closes the gap without forcing an
integrity hash on every comparison. For FS-loaded resources getSize()
resolves synchronously from statInfo, so the only added cost is one
microtask per comparison.

Remove the duplicated mtime fast path from HashTree#upsertResources and
route every existing resource through Phase 2, which delegates to the
now-corrected matchResourceMetadataStrict and already handles tag-only
updates.
Extend matchResourceMetadataStrict to compare the resource inode against
the cached inode. When both sides expose an inode and they disagree, the
file has been replaced (atomic rename, cp, tar extraction, ...) — the
mtime+size short-circuit is suppressed and the comparison falls through
to the integrity check.

Inode is now always populated by createResourceIndex; the optional
includeInode flag is removed. When either side has no inode (virtual
resources, caches written before this change), the inode check is
skipped and behavior matches the previous mtime+size path.

Falling through to integrity rather than returning false avoids
unnecessary task re-execution when the replacement preserves content
(e.g. an idempotent rsync). When the replacement also alters content,
the integrity check correctly classifies the resource as changed,
closing a stale-cache blind spot that mtime+size alone could not detect.
…changed

Drop the unused matchResourceMetadata variant (no longer used) and
rename matchResourceMetadataStrict to isResourceUnchanged.
Remove unused firstTruthy method, drop the stale local ResourceMetadata
typedef (use existing type from HashTree.js), and strip the
UI5_CACHE_PERF instrumentation.
createStat() previously returned a synthetic statInfo with ino: 0, which
Resource.clone() forwarded as statInfo into the new instance. The
constructor's `this.#inode ??= statInfo.ino` then set the clone's inode
to 0, so memory-only resources read through Memory.byPath / Memory.byGlob
advertised a fabricated inode. That defeated the inode short-circuit in
the build cache's isResourceUnchanged whenever cached metadata was later
compared against an FS-backed read of the same path.

Set ino to undefined in createStat() so memory-only resources and their
clones report getInode() === undefined, leaving the cache to fall back
on size + lastModified + integrity.

Add Resource tests covering inode propagation through construction,
clone, and setBuffer for both memory-only and FS-backed resources.
Expose ui5DataDir as a programmatic option on ProjectGraph.build(),
plumbed through ProjectBuilder, BuildContext, and CacheManager.create().
Previously the build cache location could only be configured via the
UI5_DATA_DIR env var or the ~/.ui5rc config file, both of which are
process-global and unsuitable for parallel test isolation.

Builder integration tests now route the SQLite-backed build cache
into a per-test directory via a new isolatedUi5DataDir helper. This
fixes intermittent ERR_SQLITE_ERROR "database is locked" failures on
Windows CI caused by parallel AVA workers contending on the shared
~/.ui5/buildCache database, and removes the UI5_DATA_DIR fallback
from the builder ava.config.js.
@RandomByte RandomByte force-pushed the feat/incremental-build-4 branch from c9c4972 to 2862e0f Compare May 20, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants