Skip to content

chore: implement incremental build for FableLibrary tasks#4438

Merged
MangelMaxime merged 10 commits intomainfrom
incremental-build
Mar 23, 2026
Merged

chore: implement incremental build for FableLibrary tasks#4438
MangelMaxime merged 10 commits intomainfrom
incremental-build

Conversation

@MangelMaxime
Copy link
Member

This PR introduce incremental build support for fable library tasks.

I was tired of always having to switch between ./build.sh quicktest javascript and ./build.sh quicktest javascript --skip-fable-library, etc.

This PR replace --skip-fable-library with an incremental build function.

A SHA-256 hash of all input source files is computed and cached in .build-cache/; a rebuild is skipped if the hash matches the last successful build and the output directory exists.

A new flag --force-fable-library has been introduced allowing to force a rebuild of the fable library, independently of the cache status.

@dbrattli I tried to update the src/Fable.Transforms/Python/TYPE-ANNOTATIONS.md file to match the previous behavior but it would be nice if you could check it out.

@MangelMaxime MangelMaxime requested review from dbrattli and ncave March 22, 2026 22:49
@MangelMaxime
Copy link
Member Author

@claude review

@github-actions
Copy link
Contributor

Python Type Checking Results (Pyright)

Metric Value
Total errors 18
Files with errors 4
Excluded files 4
New errors ✅ No
Excluded files with errors (4 files)

These files have known type errors and are excluded from CI. Remove from pyrightconfig.ci.json as errors are fixed.

File Errors Status
temp/tests/Python/test_applicative.py 12 Excluded
temp/tests/Python/test_hash_set.py 3 Excluded
temp/tests/Python/test_nested_and_recursive_pattern.py 2 Excluded
temp/tests/Python/fable_modules/thoth_json_python/encode.py 1 Excluded

@dbrattli
Copy link
Collaborator

@claude review

@claude
Copy link

claude bot commented Mar 23, 2026

Claude finished @dbrattli's task in 2m 25s —— View job


Code Review

  • Read changed files
  • Analyze implementation
  • Provide review feedback

The overall design is solid — SHA-256 content hashing with a simple cache file is a clean approach. The --force-fable-library flag and the Publish.fs hardcoded true are both the right calls. A few issues worth addressing:


Bugs

1. Typo in cache keyCore.fs:93

IncrementalBuild.run
    $"fable-libary-%s{this.Language}"   // "libary" missing an 'r'

Should be fable-library. Currently the .build-cache/ files will be named e.g. fable-libary-typescript.cache. Not a functional bug yet, but fixing the typo later will silently orphan all existing cache files and force a one-time rebuild. Fix this →


2. Dart glob pattern matches literal file named dart, not *.dart filesDart.fs

Path.Combine("src", "fable-library-dart", "**", "dart")   // BUG: matches a file named "dart"
Path.Combine("src", "fable-library-dart", "**", "*.fs")

The first pattern needs to be "*.dart". As written, no .dart source files will be included in the input hash, so changes to .dart files won't invalidate the cache. Fix this →


3. Python quicktest: pip install -e is never called on incremental first-time buildsQuicktest/Python.fs

The new code:

if (args |> List.contains "--force-fable-library") then
    BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(true)
    Command.Run("uv", $"pip install -e {fableLibraryBuildDir}")

On the very first run (no cache, no --force-fable-library), genericQuicktest will call Run(false) which correctly detects the output doesn't exist and builds the library — but pip install -e is never called. Tests will silently use the PyPI version instead of the locally built one.

The old code handled this by always building + installing unless --skip-fable-library was explicitly passed. The new code inverts the condition without preserving the pip install for the incremental-triggered build path.

Also: when --force-fable-library is passed, the library is built twice — once here with Run(true), and again inside genericQuicktest which also sees --force-fable-library. The old code avoided this by injecting --skip-fable-library before calling genericQuicktest. Fix this →


Minor Issues

4. No skip messageIncrementalBuild.fs

When the build is skipped, the function exits silently. The old code printed "Skipping Fable build stage". Without any message, a fast run with no output could confuse users into thinking nothing ran at all. Consider adding a log line like:

if isStale then
    callback()
    saveCache name inputHash
else
    printfn $"Skipping %s{name} build (no changes detected)"

5. Rust input patterns missing Cargo.tomlRust.fs

The CopyStage copies Cargo.toml into the build dir, but Cargo.toml isn't included in inputPatterns. Changes to Rust dependencies won't invalidate the cache. Low risk since Cargo.toml rarely changes, but worth noting.

6. Pre-existing: duplicate open Build.UtilsCore.fs:7-8

Lines 7 and 8 both open Build.Utils. Not introduced by this PR but visible in the diff.


Summary

The incremental build approach is a welcome improvement to the developer workflow. The Dart glob bug (#2) and Python quicktest regression (#3) are the most important to fix before merging. The typo (#1) is minor but creates a messy transition if fixed later.

@fable-compiler fable-compiler deleted a comment from claude bot Mar 23, 2026
@MangelMaxime
Copy link
Member Author

@claude Please review the PR, I made the changes to fix the issues you reported earlier

@claude
Copy link

claude bot commented Mar 23, 2026

Claude finished @MangelMaxime's task in 1m 2s —— View job


Code Review (Follow-up)

  • Read changed files
  • Verify fixes for previously reported issues
  • Provide updated review feedback

All 6 issues from the previous review have been addressed. Here's the verification:

1. Typo in cache keyCore.fs:89

$"fable-library-%s{this.Language}"   // "library" now spelled correctly

2. Dart glob patternDart.fs:15

Path.Combine("src", "fable-library-dart", "**", "*.dart")   // "*.dart" fixed

3. Python quicktest pip install -e regressionQuicktest/Python.fs

The fix is elegant: the pip install -e is now injected as a postFableBuildStage callback into BuildFableLibraryPython, so it runs as part of the library build itself whenever a rebuild happens (first run, cache miss, or --force-fable-library). No more double-build — genericQuicktest calls Run() exactly once.

4. Skip messageIncrementalBuild.fs:71

printfn "Skipping '%s' (inputs unchanged and outputs exist)" name

5. Rust Cargo.toml in input patternsRust.fs:20

Path.Combine("src", "fable-library-rust", "Cargo.toml")

6. Duplicate open Build.UtilsCore.fs
Removed, only one open Build.Utils remains.


The implementation looks correct and ready to merge. The postFableBuildStage callback approach for Python is cleaner than the old conditional logic.

Copy link
Collaborator

@ncave ncave left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MangelMaxime MangelMaxime merged commit 27382b7 into main Mar 23, 2026
25 checks passed
@MangelMaxime MangelMaxime deleted the incremental-build branch March 23, 2026 19:30
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.

3 participants