From 00ea25d58aa0fc7c28a1499a8fa8333de8d8aa0f Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 22 Mar 2026 23:42:32 +0100 Subject: [PATCH 01/10] chore: implement incremental build for FableLibrary task --- .gitignore | 2 + src/Fable.Build/Fable.Build.fsproj | 4 +- src/Fable.Build/FableLibrary/Beam.fs | 7 +- src/Fable.Build/FableLibrary/Core.fs | 90 ++++++++++++------- src/Fable.Build/FableLibrary/Dart.fs | 4 + src/Fable.Build/FableLibrary/Python.fs | 8 +- src/Fable.Build/FableLibrary/Rust.fs | 7 +- src/Fable.Build/FableLibrary/TypeScript.fs | 4 + src/Fable.Build/Main.fs | 12 +-- src/Fable.Build/Package.fs | 14 +-- src/Fable.Build/Publish.fs | 18 ++-- src/Fable.Build/Quicktest/Python.fs | 8 +- src/Fable.Build/Standalone.fs | 7 +- src/Fable.Build/Test/Beam.fs | 4 +- src/Fable.Build/Test/Dart.fs | 4 +- src/Fable.Build/Test/JavaScript.fs | 4 +- src/Fable.Build/Test/Python.fs | 4 +- src/Fable.Build/Test/Rust.fs | 4 +- src/Fable.Build/Test/TypeScript.fs | 4 +- src/Fable.Build/Utils/IncrementalBuild.fs | 69 ++++++++++++++ src/Fable.Build/WorkerJs.fs | 4 +- .../Python/TYPE-ANNOTATIONS.md | 4 +- 22 files changed, 202 insertions(+), 84 deletions(-) create mode 100644 src/Fable.Build/Utils/IncrementalBuild.fs diff --git a/.gitignore b/.gitignore index b8245fda37..acc979811a 100644 --- a/.gitignore +++ b/.gitignore @@ -232,3 +232,5 @@ Cargo.lock # This file is copied as part of the Restore task tests/React/Components.Copied.fs + +.build-cache/ diff --git a/src/Fable.Build/Fable.Build.fsproj b/src/Fable.Build/Fable.Build.fsproj index 30cdaeab89..f46a6d9f0c 100644 --- a/src/Fable.Build/Fable.Build.fsproj +++ b/src/Fable.Build/Fable.Build.fsproj @@ -6,8 +6,9 @@ - + + @@ -46,6 +47,7 @@ + diff --git a/src/Fable.Build/FableLibrary/Beam.fs b/src/Fable.Build/FableLibrary/Beam.fs index a8a24ded71..69b533845d 100644 --- a/src/Fable.Build/FableLibrary/Beam.fs +++ b/src/Fable.Build/FableLibrary/Beam.fs @@ -9,7 +9,12 @@ type BuildFableLibraryBeam() = Path.Combine("src", "fable-library-beam"), Path.Combine("src", "fable-library-beam"), Path.Combine("temp", "fable-library-beam"), - Path.Combine("temp", "fable-library-beam") + Path.Combine("temp", "fable-library-beam"), + [ + Path.Combine("src", "fable-library-beam", "**", "*.erl") + Path.Combine("src", "fable-library-beam", "**", "*.fs") + Path.Combine("src", "fable-library-ts", "**", "*.fs") + ] ) override this.CopyStage() = diff --git a/src/Fable.Build/FableLibrary/Core.fs b/src/Fable.Build/FableLibrary/Core.fs index 8d56e782f4..6b0faa024f 100644 --- a/src/Fable.Build/FableLibrary/Core.fs +++ b/src/Fable.Build/FableLibrary/Core.fs @@ -1,5 +1,6 @@ namespace Build.FableLibrary +open System open BlackFox.CommandLine open Fake.IO open System.IO @@ -7,13 +8,23 @@ open Build.Utils open Build.Utils open System.Diagnostics open SimpleExec +open Microsoft.Extensions.FileSystemGlobbing +open Microsoft.Extensions.FileSystemGlobbing.Abstractions /// /// Building fable-library is similar enough for all the targets /// that we can use this class to standardise the process. /// type BuildFableLibrary - (language: string, libraryDir: string, sourceDir: string, buildDir: string, outDir: string, ?fableLibArg: string) + ( + language: string, + libraryDir: string, + sourceDir: string, + buildDir: string, + outDir: string, + inputPatterns: string list, + ?fableLibArg: string + ) = // It seems like the different target have a different way of supporting @@ -62,37 +73,50 @@ type BuildFableLibrary tryDelete 3 - member this.Run(?skipIfExist: bool) = + member this.Run(?forceBuild: bool) = let toConsole (s: string) = System.Console.WriteLine(s) - let skipIfExist = defaultArg skipIfExist false - - if skipIfExist && Directory.Exists outDir then - "Skipping Fable build stage" |> toConsole - - else - - "Cleaning build directory" |> toConsole - - this.deleteDirectoryRobust buildDir - - "Building Fable.Library" |> toConsole - - let args = - CmdLine.appendRaw sourceDir - >> CmdLine.appendPrefix "--outDir" outDir - >> CmdLine.appendPrefix "--fableLib" fableLibArg - >> CmdLine.appendPrefix "--lang" language - >> CmdLine.appendPrefix "--exclude" "Fable.Core" - >> CmdLine.appendPrefix "--define" "FABLE_LIBRARY" - >> CmdLine.appendRaw "--noCache" - // Target implementation can require additional arguments - >> this.FableArgsBuilder - - Command.Fable(args) - - "Copy stage" |> toConsole - this.CopyStage() - - "Post Fable build stage" |> toConsole - this.PostFableBuildStage() + let matcher = new Matcher() + matcher.AddIncludePatterns(inputPatterns) + // Ignore well-known build output folders from MSBuild + matcher.AddExcludePatterns([ "**/obj/**"; "**/bin/**" ]) + + let directoryInfo = DirectoryInfo(Path.Resolve()) + let dirWrapper = DirectoryInfoWrapper(directoryInfo) + + let inputFiles = + matcher.Execute(dirWrapper).Files + |> Seq.map (fun file -> Path.Combine(directoryInfo.FullName, file.Path)) + |> Seq.toList + + IncrementalBuild.run + $"fable-libary-%s{this.Language}" + (defaultArg forceBuild false) + inputFiles + [ Path.Resolve(this.OutDir) ] + (fun () -> + "Cleaning build directory" |> toConsole + + this.deleteDirectoryRobust buildDir + + "Building Fable.Library" |> toConsole + + let args = + CmdLine.appendRaw sourceDir + >> CmdLine.appendPrefix "--outDir" outDir + >> CmdLine.appendPrefix "--fableLib" fableLibArg + >> CmdLine.appendPrefix "--lang" language + >> CmdLine.appendPrefix "--exclude" "Fable.Core" + >> CmdLine.appendPrefix "--define" "FABLE_LIBRARY" + >> CmdLine.appendRaw "--noCache" + // Target implementation can require additional arguments + >> this.FableArgsBuilder + + Command.Fable(args) + + "Copy stage" |> toConsole + this.CopyStage() + + "Post Fable build stage" |> toConsole + this.PostFableBuildStage() + ) diff --git a/src/Fable.Build/FableLibrary/Dart.fs b/src/Fable.Build/FableLibrary/Dart.fs index aabe273f3b..a8dd6df773 100644 --- a/src/Fable.Build/FableLibrary/Dart.fs +++ b/src/Fable.Build/FableLibrary/Dart.fs @@ -11,6 +11,10 @@ type BuildFableLibraryDart() = Path.Combine("src", "fable-library-dart"), Path.Combine("temp", "fable-library-dart"), Path.Combine("temp", "fable-library-dart"), + [ + Path.Combine("src", "fable-library-dart", "**", "dart") + Path.Combine("src", "fable-library-dart", "**", "*.fs") + ], Path.Combine(".", "temp", "fable-library-dart") ) diff --git a/src/Fable.Build/FableLibrary/Python.fs b/src/Fable.Build/FableLibrary/Python.fs index 39d2ca775e..2969e1b6d3 100644 --- a/src/Fable.Build/FableLibrary/Python.fs +++ b/src/Fable.Build/FableLibrary/Python.fs @@ -13,7 +13,13 @@ type BuildFableLibraryPython(?skipCore: bool) = libraryDir = Path.Combine("src", "fable-library-py"), sourceDir = Path.Combine("src", "fable-library-py", "fable_library"), buildDir = Path.Combine("temp", "fable-library-py"), - outDir = Path.Combine("temp", "fable-library-py", "fable_library") + outDir = Path.Combine("temp", "fable-library-py", "fable_library"), + inputPatterns = + [ + Path.Combine("src", "fable-library-py", "**", "*.py") + Path.Combine("src", "fable-library-py", "**", "*.fs") + Path.Combine("src", "fable-library-ts", "**", "*.fs") + ] ) let skipCore = defaultArg skipCore false diff --git a/src/Fable.Build/FableLibrary/Rust.fs b/src/Fable.Build/FableLibrary/Rust.fs index 7989e0d946..c7fb3b4d1b 100644 --- a/src/Fable.Build/FableLibrary/Rust.fs +++ b/src/Fable.Build/FableLibrary/Rust.fs @@ -12,7 +12,12 @@ type BuildFableLibraryRust() = Path.Combine("src", "fable-library-rust"), Path.Combine("src", "fable-library-rust", "src"), Path.Combine("temp", "fable-library-rust"), - Path.Combine("temp", "fable-library-rust", "src") + Path.Combine("temp", "fable-library-rust", "src"), + inputPatterns = + [ + Path.Combine("src", "fable-library-rust", "**", "*.rs") + Path.Combine("src", "fable-library-rust", "**", "*.fs") + ] ) override this.PostFableBuildStage() = diff --git a/src/Fable.Build/FableLibrary/TypeScript.fs b/src/Fable.Build/FableLibrary/TypeScript.fs index fafdf3bb54..6275e9b4a7 100644 --- a/src/Fable.Build/FableLibrary/TypeScript.fs +++ b/src/Fable.Build/FableLibrary/TypeScript.fs @@ -13,6 +13,10 @@ type BuildFableLibraryTypeScript() = Path.Combine("src", "fable-library-ts"), Path.Combine("temp", "fable-library-ts"), Path.Combine("temp", "fable-library-ts"), + [ + Path.Combine("src", "fable-library-ts", "**", "*.ts") + Path.Combine("src", "fable-library-ts", "**", "*.fs") + ], Path.Combine(".", "temp", "fable-library-ts") ) diff --git a/src/Fable.Build/Main.fs b/src/Fable.Build/Main.fs index 4e0ab078e3..8ef91cf414 100644 --- a/src/Fable.Build/Main.fs +++ b/src/Fable.Build/Main.fs @@ -32,7 +32,7 @@ Available commands: beam Run for BEAM (Erlang) Options: - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library --watch Watch for changes and re-run the tests test Run the main tests suite @@ -49,7 +49,7 @@ Available commands: Options for all except integration and standalone: --watch Watch for changes and re-run the tests - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library --no-dotnet When in watch mode, do not run the .NET tests Options for JavaScript: @@ -73,20 +73,20 @@ Available commands: on top of of Node.js Options: - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library --no-minify Don't minify the JavaScript output --watch Watch for changes and recompile worker-js Compile the worker for the standalone version of Fable Options: - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library --no-minify Don't minify the JavaScript output compiler-js Compile the Fable compiler to JavaScript Options: - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library --no-minify Don't minify the JavaScript output package Generate local package for Fable.Cli and Fable.Core @@ -94,7 +94,7 @@ Available commands: inside of other projects Options: - --skip-fable-library Skip building fable-library if folder already exists + --force-fable-library Force building fable-library publish Publish the different packages to NuGet and NPM based on the CHANGELOG.md files diff --git a/src/Fable.Build/Package.fs b/src/Fable.Build/Package.fs index a53232eec9..0298fb1d64 100644 --- a/src/Fable.Build/Package.fs +++ b/src/Fable.Build/Package.fs @@ -12,14 +12,14 @@ open EasyBuild.Tools.PackageJson let private packageDestination = Path.Resolve("temp", "packages") let handle (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" // Build all the fable-libraries - BuildFableLibraryBeam().Run(skipFableLibrary) - BuildFableLibraryDart().Run(skipFableLibrary) - BuildFableLibraryJavaScript().Run(skipFableLibrary) - BuildFableLibraryPython().Run(skipFableLibrary) - BuildFableLibraryRust().Run(skipFableLibrary) - BuildFableLibraryTypeScript().Run(skipFableLibrary) + BuildFableLibraryBeam().Run(forceFableLibrary) + BuildFableLibraryDart().Run(forceFableLibrary) + BuildFableLibraryJavaScript().Run(forceFableLibrary) + BuildFableLibraryPython().Run(forceFableLibrary) + BuildFableLibraryRust().Run(forceFableLibrary) + BuildFableLibraryTypeScript().Run(forceFableLibrary) Directory.clean packageDestination diff --git a/src/Fable.Build/Publish.fs b/src/Fable.Build/Publish.fs index 5ce0fe2509..99cb68997a 100644 --- a/src/Fable.Build/Publish.fs +++ b/src/Fable.Build/Publish.fs @@ -87,12 +87,14 @@ let private publishNpm (projectDir: string) = let handle (args: string list) = // Build all the fable-libraries - BuildFableLibraryBeam().Run() - BuildFableLibraryDart().Run() - BuildFableLibraryJavaScript().Run() - BuildFableLibraryPython().Run() - BuildFableLibraryRust().Run() - BuildFableLibraryTypeScript().Run() + // Force rebuld of fable-libraries to make sure they are generated with the latest + // version of the compiler + BuildFableLibraryBeam().Run(true) + BuildFableLibraryDart().Run(true) + BuildFableLibraryJavaScript().Run(true) + BuildFableLibraryPython().Run(true) + BuildFableLibraryRust().Run(true) + BuildFableLibraryTypeScript().Run(true) // Handle the NPM packages @@ -132,8 +134,8 @@ let handle (args: string list) = // Trigger fable-compiler-js target to make sure everything is ready for publish // Note: fable-standalone is built as part of fable-compiler-js // so no need to build it separately - // Note 2: We already built fable-library, so we skip it here - CompilerJs.handle [ "--skip-fable-library" ] + // Note 2: We already built fable-library, it will be skipped thanks to incremental build + CompilerJs.handle [] publishNpm ProjectDir.fable_standalone publishNpm ProjectDir.fable_compiler_js diff --git a/src/Fable.Build/Quicktest/Python.fs b/src/Fable.Build/Quicktest/Python.fs index 8234fc182b..b309999031 100644 --- a/src/Fable.Build/Quicktest/Python.fs +++ b/src/Fable.Build/Quicktest/Python.fs @@ -3,8 +3,6 @@ module Build.Quicktest.Python open Build.FableLibrary open Build.Quicktest.Core open SimpleExec -open System.IO -open Build open Build.Utils let private fableLibraryBuildDir = Path.Resolve("temp", "fable-library-py") @@ -14,8 +12,8 @@ let handle (args: string list) = // Install local fable-library as editable package for testing // This ensures quicktest uses the locally built version, not PyPI - if not (args |> List.contains "--skip-fable-library") then - BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(false) + if (args |> List.contains "--force-fable-library") then + BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(true) // Install fable-library in editable mode Command.Run("uv", $"pip install -e {fableLibraryBuildDir}") @@ -28,4 +26,4 @@ let handle (args: string list) = RunMode = RunScript } // Always skip library in genericQuicktest since we handled it above - ("--skip-fable-library" :: args) + args diff --git a/src/Fable.Build/Standalone.fs b/src/Fable.Build/Standalone.fs index e16ac2f5e4..1af3b039d2 100644 --- a/src/Fable.Build/Standalone.fs +++ b/src/Fable.Build/Standalone.fs @@ -108,13 +108,10 @@ let build (minify: bool) = let handle (args: string list) = let minify = args |> List.contains "--no-minify" |> not - let isWatch = args |> List.contains "--watch" - let skipFableLibrary = args |> List.contains "--skip-fable-library" Command.Run("npm", "install", workingDirectory = projectDir) - BuildFableLibraryJavaScript().Run(skipFableLibrary) + BuildFableLibraryJavaScript().Run() build minify - // Force skip fable library for worker, because it's already built - WorkerJs.handle ("--skip-fable-library" :: args) + WorkerJs.handle args diff --git a/src/Fable.Build/Test/Beam.fs b/src/Fable.Build/Test/Beam.fs index 169b1122f3..7de9a55f06 100644 --- a/src/Fable.Build/Test/Beam.fs +++ b/src/Fable.Build/Test/Beam.fs @@ -18,9 +18,9 @@ let private testProjectName = let handle (args: string list) = let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" - BuildFableLibraryBeam().Run(skipFableLibrary) + BuildFableLibraryBeam().Run(forceFableLibrary) Directory.clean buildDir diff --git a/src/Fable.Build/Test/Dart.fs b/src/Fable.Build/Test/Dart.fs index 0792c6f776..a4185bdf02 100644 --- a/src/Fable.Build/Test/Dart.fs +++ b/src/Fable.Build/Test/Dart.fs @@ -12,11 +12,11 @@ let private testsFolder = Path.Resolve("tests", "Dart") let private testsFsprojFolder = Path.Resolve("tests", "Dart", "src") let handle (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" - BuildFableLibraryDart().Run(skipFableLibrary) + BuildFableLibraryDart().Run(forceFableLibrary) Directory.clean buildDir diff --git a/src/Fable.Build/Test/JavaScript.fs b/src/Fable.Build/Test/JavaScript.fs index f197253125..8bf1552886 100644 --- a/src/Fable.Build/Test/JavaScript.fs +++ b/src/Fable.Build/Test/JavaScript.fs @@ -159,7 +159,7 @@ let handle (args: string list) = let isReactOnly = args |> List.contains "--react-only" let isStandaloneOnly = args |> List.contains "--standalone-only" let isAdaptiveOnly = args |> List.contains "--adaptive-only" - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" @@ -171,7 +171,7 @@ let handle (args: string list) = | _ -> () - BuildFableLibraryJavaScript().Run(skipFableLibrary) + BuildFableLibraryJavaScript().Run(forceFableLibrary) if isReactOnly then testReact isWatch diff --git a/src/Fable.Build/Test/Python.fs b/src/Fable.Build/Test/Python.fs index 83f40737dc..87081b3596 100644 --- a/src/Fable.Build/Test/Python.fs +++ b/src/Fable.Build/Test/Python.fs @@ -11,7 +11,7 @@ let private sourceDir = Path.Resolve("tests", "Python") let private fableLibraryBuildDir = Path.Resolve("temp", "fable-library-py") let handle (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let skipFableLibraryCore = args |> List.contains "--skip-fable-library-core" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" @@ -19,7 +19,7 @@ let handle (args: string list) = let runTyping = args |> List.contains "--type-check" let runFormat = args |> List.contains "--format" - BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(skipFableLibrary) + BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(forceFableLibrary) Directory.clean buildDir diff --git a/src/Fable.Build/Test/Rust.fs b/src/Fable.Build/Test/Rust.fs index a576b758f7..e37805cdbe 100644 --- a/src/Fable.Build/Test/Rust.fs +++ b/src/Fable.Build/Test/Rust.fs @@ -24,7 +24,7 @@ let mainTestsDestinationDir = Path.Resolve("temp", "tests", "Rust") let mainTestsProjectDir = Path.Resolve("tests", "Rust") let handle (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let astOnly = args |> List.contains "--ast-only" let noStd = args |> List.contains "--no_std" @@ -34,7 +34,7 @@ let handle (args: string list) = if noStd && threaded then failwith "Cannot use --no-std and --threaded at the same time" - BuildFableLibraryRust().Run(skipFableLibrary) + BuildFableLibraryRust().Run(forceFableLibrary) if astOnly then testAst isWatch diff --git a/src/Fable.Build/Test/TypeScript.fs b/src/Fable.Build/Test/TypeScript.fs index a7395e7deb..01caf9e9ca 100644 --- a/src/Fable.Build/Test/TypeScript.fs +++ b/src/Fable.Build/Test/TypeScript.fs @@ -12,11 +12,11 @@ let private fableDest = Path.Resolve("temp", "tests", "TypeScript") let private tscDest = Path.Resolve("temp", "tests", "TypeScriptCompiled") let handle (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" let noDotnet = args |> List.contains "--no-dotnet" - BuildFableLibraryTypeScript().Run(skipFableLibrary) + BuildFableLibraryTypeScript().Run(forceFableLibrary) Directory.clean fableDest Directory.clean tscDest diff --git a/src/Fable.Build/Utils/IncrementalBuild.fs b/src/Fable.Build/Utils/IncrementalBuild.fs new file mode 100644 index 0000000000..8ad618398a --- /dev/null +++ b/src/Fable.Build/Utils/IncrementalBuild.fs @@ -0,0 +1,69 @@ +module IncrementalBuild + +open System +open System.IO +open System.Security.Cryptography +open System.Text +open Build.Utils + +let private cacheDir = Path.Resolve(".build-cache") + +/// Stable hash over file contents + arbitrary string keys. +/// File paths are hashed by content; plain strings are hashed as-is. +let private computeInputHash (inputs: string list) = + use sha = SHA256.Create() + use ms = new MemoryStream() + + for input in List.sort inputs do + if File.Exists input then + use fs = File.OpenRead input + let contentHash = sha.ComputeHash fs + ms.Write(contentHash, 0, contentHash.Length) + else + // Treat as a plain string key (flag, version, env var...) + let bytes = Encoding.UTF8.GetBytes input + let keyHash = sha.ComputeHash bytes + ms.Write(keyHash, 0, keyHash.Length) + + ms.ToArray() |> sha.ComputeHash |> Convert.ToHexString + +let private cachePath (name: string) = + let safe = name.Replace('/', '_').Replace('\\', '_').Replace(' ', '_') + Path.Combine(cacheDir, $"{safe}.cache") + +let private tryLoadCache (name: string) : string option = + try + let path = cachePath name + + if File.Exists path then + File.ReadAllText path |> Some + else + None + with _ -> + None + +let private saveCache (name: string) (entry: string) = + Directory.CreateDirectory cacheDir |> ignore + File.WriteAllText(cachePath name, entry) + +let run (name: string) (forceBuild: bool) (inputs: string list) (outputs: string list) (callback: unit -> unit) = + let inputHash = computeInputHash inputs + + let outputsExist = + outputs + |> List.forall (fun output -> File.Exists output || Directory.Exists output) + + let isStale = + if forceBuild then + true + else if not outputsExist then + true + else + match tryLoadCache name with + | None -> true + | Some cached -> cached <> inputHash + + if isStale then + callback () + + saveCache name inputHash diff --git a/src/Fable.Build/WorkerJs.fs b/src/Fable.Build/WorkerJs.fs index 190b77e7cb..a045a26ea3 100644 --- a/src/Fable.Build/WorkerJs.fs +++ b/src/Fable.Build/WorkerJs.fs @@ -16,9 +16,9 @@ let private distDir = Path.Combine(projectDir, "dist") let handle (args: string list) = let minify = args |> List.contains "--no-minify" |> not - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" - BuildFableLibraryJavaScript().Run(skipFableLibrary) + BuildFableLibraryJavaScript().Run(forceFableLibrary) let fableArgs = CmdLine.empty diff --git a/src/Fable.Transforms/Python/TYPE-ANNOTATIONS.md b/src/Fable.Transforms/Python/TYPE-ANNOTATIONS.md index 7b5cb106b5..a73ec0031c 100644 --- a/src/Fable.Transforms/Python/TYPE-ANNOTATIONS.md +++ b/src/Fable.Transforms/Python/TYPE-ANNOTATIONS.md @@ -237,9 +237,9 @@ Protocol methods use uncurried types to match runtime behavior. A method paramet ```bash # Test rebuilding everything (fable-library + fable.library.core + tests) +./build.sh test python --force-fable-library +# Test rebuilding, but skipping fable-library rebuild (thanks to incremental build) ./build.sh test python -# Test rebuilding, but skipping fable-library rebuild -./build.sh test python --skip-fable-library # Test rebuilding tests and fable-library but skipping fable-library-core rebuild ./build.sh test python --skip-fable-library-core ``` From f64aad94b98fa24df5e50640369423874c31a830 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 22 Mar 2026 23:53:40 +0100 Subject: [PATCH 02/10] fix CLAUDE instructions --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 82dda76cc7..8bc972fbf3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ The build system is implemented in F# at `src/Fable.Build/`. All commands go thr # Run tests (targets: javascript, typescript, python, dart, rust, beam, integration) ./build.sh test javascript ./build.sh test javascript --watch # Watch mode (recompile on changes) -./build.sh test javascript --skip-fable-library # Skip fable-library rebuild (only if fable-library source is unchanged) +./build.sh test javascript --force-fable-library # Force fable-library rebuild (needed if you modified Fable compiler to fix the generation of `src/fable-library-*/**/*.fs` files) # Python-specific options ./build.sh test python --skip-fable-library-core # Skip slow Rust extension + .pyi rebuild (Python only) @@ -29,7 +29,7 @@ The build system is implemented in F# at `src/Fable.Build/`. All commands go thr ./build.sh quicktest javascript # Also: typescript, python, dart, rust, beam ``` -`--skip-fable-library` is safe when changes are only in `src/Fable.Transforms/` or other compiler code. If you modified runtime library source (e.g., `src/fable-library-py/`, `src/fable-library-ts/`), do not skip. If you already ran `./build.sh fable-library` separately, use `--skip-fable-library` when running tests right afterwards to avoid rebuilding. +In most cases, you should not need to use `--force-fable-library`. Only use it if you modified compiler code that affects how F# files used in `src/fable-library-*/` are generated (e.g., changes to `FSharp2Fable.fs` that affect how F# AST is transformed into Fable AST for library files). Build output goes to `temp/`: transpiled runtime libraries in `temp/fable-library-/` and test output in `temp/tests//` (e.g., `temp/fable-library-beam/` and `temp/tests/beam/`). From 4f155b2e87393f26fdc4e074f0020437ca9b090c Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 22 Mar 2026 23:56:45 +0100 Subject: [PATCH 03/10] fix: replace old `--skip-fable-library` usage with the new `--force-fable-library` --- src/Fable.Build/Quicktest/Core.fs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Fable.Build/Quicktest/Core.fs b/src/Fable.Build/Quicktest/Core.fs index 6bad17af83..1044ffba5c 100644 --- a/src/Fable.Build/Quicktest/Core.fs +++ b/src/Fable.Build/Quicktest/Core.fs @@ -20,10 +20,10 @@ type QuicktestConfig = } let genericQuicktest (config: QuicktestConfig) (args: string list) = - let skipFableLibrary = args |> List.contains "--skip-fable-library" + let forceFableLibrary = args |> List.contains "--force-fable-library" let isWatch = args |> List.contains "--watch" - config.FableLibBuilder.Run(skipFableLibrary) + config.FableLibBuilder.Run(forceFableLibrary) let appendRunMode (cmdLine: CmdLine) = match config.RunMode with From bd1b5c5de185c3685135038c40853945dfee8749 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Sun, 22 Mar 2026 23:56:57 +0100 Subject: [PATCH 04/10] ci: remove non needed `--skip-fable-library` --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b1cbcdb90b..5db15cc7bc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -175,11 +175,11 @@ jobs: - name: Fable Tests - Python (linux) if: matrix.platform == 'ubuntu-latest' - run: ./build.sh test python --skip-fable-library + run: ./build.sh test python - name: Fable Tests - Python (Windows) if: matrix.platform == 'windows-latest' - run: .\build.bat test python --skip-fable-library + run: .\build.bat test python # Separate build job for Rust (will run in parallel) build-rust: @@ -209,7 +209,7 @@ jobs: run: ./build.sh fable-library --rust - name: Fable Tests - Rust - run: ./build.sh test rust --skip-fable-library --${{ matrix.test }} + run: ./build.sh test rust --${{ matrix.test }} # Separate build job for Dart build-dart: From 6bce66522535750bc0ec31f9c1de5df28d9cb9d9 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:02:08 +0100 Subject: [PATCH 05/10] chore: fix typo --- src/Fable.Build/FableLibrary/Core.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Build/FableLibrary/Core.fs b/src/Fable.Build/FableLibrary/Core.fs index 6b0faa024f..72a1937b3a 100644 --- a/src/Fable.Build/FableLibrary/Core.fs +++ b/src/Fable.Build/FableLibrary/Core.fs @@ -90,7 +90,7 @@ type BuildFableLibrary |> Seq.toList IncrementalBuild.run - $"fable-libary-%s{this.Language}" + $"fable-library-%s{this.Language}" (defaultArg forceBuild false) inputFiles [ Path.Resolve(this.OutDir) ] From e99fc5788be8cc15bcc5008f97b16d533b5060c7 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:02:31 +0100 Subject: [PATCH 06/10] fix: glob pattern for Dart files --- src/Fable.Build/FableLibrary/Dart.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Fable.Build/FableLibrary/Dart.fs b/src/Fable.Build/FableLibrary/Dart.fs index a8dd6df773..12b3a41c3d 100644 --- a/src/Fable.Build/FableLibrary/Dart.fs +++ b/src/Fable.Build/FableLibrary/Dart.fs @@ -12,7 +12,7 @@ type BuildFableLibraryDart() = Path.Combine("temp", "fable-library-dart"), Path.Combine("temp", "fable-library-dart"), [ - Path.Combine("src", "fable-library-dart", "**", "dart") + Path.Combine("src", "fable-library-dart", "**", "*.dart") Path.Combine("src", "fable-library-dart", "**", "*.fs") ], Path.Combine(".", "temp", "fable-library-dart") From 79b797dcfad8bfa631b141a66b5b04dc55cd34b0 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:05:26 +0100 Subject: [PATCH 07/10] fix: include `Cargo.toml` in the incremental cache for rust --- src/Fable.Build/FableLibrary/Rust.fs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Fable.Build/FableLibrary/Rust.fs b/src/Fable.Build/FableLibrary/Rust.fs index c7fb3b4d1b..f5b0139616 100644 --- a/src/Fable.Build/FableLibrary/Rust.fs +++ b/src/Fable.Build/FableLibrary/Rust.fs @@ -17,6 +17,7 @@ type BuildFableLibraryRust() = [ Path.Combine("src", "fable-library-rust", "**", "*.rs") Path.Combine("src", "fable-library-rust", "**", "*.fs") + Path.Combine("src", "fable-library-rust", "Cargo.toml") ] ) From 799e42493f854015321992fb573b97b5a69d17a9 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:05:58 +0100 Subject: [PATCH 08/10] chore: clean up import in `src/Fable.Build/FableLibrary/Core.fs` --- src/Fable.Build/FableLibrary/Core.fs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Fable.Build/FableLibrary/Core.fs b/src/Fable.Build/FableLibrary/Core.fs index 72a1937b3a..4727a66756 100644 --- a/src/Fable.Build/FableLibrary/Core.fs +++ b/src/Fable.Build/FableLibrary/Core.fs @@ -1,12 +1,8 @@ namespace Build.FableLibrary -open System open BlackFox.CommandLine -open Fake.IO open System.IO open Build.Utils -open Build.Utils -open System.Diagnostics open SimpleExec open Microsoft.Extensions.FileSystemGlobbing open Microsoft.Extensions.FileSystemGlobbing.Abstractions From e7347578dbb290031440de379a313cadb2249162 Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:06:51 +0100 Subject: [PATCH 09/10] feat: add a log when incremental build skipped a task --- src/Fable.Build/Utils/IncrementalBuild.fs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Fable.Build/Utils/IncrementalBuild.fs b/src/Fable.Build/Utils/IncrementalBuild.fs index 8ad618398a..09bcadbabc 100644 --- a/src/Fable.Build/Utils/IncrementalBuild.fs +++ b/src/Fable.Build/Utils/IncrementalBuild.fs @@ -67,3 +67,5 @@ let run (name: string) (forceBuild: bool) (inputs: string list) (outputs: string callback () saveCache name inputHash + else + printfn "Skipping '%s' (inputs unchanged and outputs exist)" name From 4fb551f6a7490e2b792a22fd858896f2f030986f Mon Sep 17 00:00:00 2001 From: Mangel Maxime Date: Mon, 23 Mar 2026 18:38:51 +0100 Subject: [PATCH 10/10] fix: quicktest for Python --- src/Fable.Build/FableLibrary/Python.fs | 7 +++++-- src/Fable.Build/Quicktest/Python.fs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Fable.Build/FableLibrary/Python.fs b/src/Fable.Build/FableLibrary/Python.fs index 2969e1b6d3..9f5c9a3d91 100644 --- a/src/Fable.Build/FableLibrary/Python.fs +++ b/src/Fable.Build/FableLibrary/Python.fs @@ -4,9 +4,8 @@ open System.IO open Fake.IO open Build.Utils open SimpleExec -open BlackFox.CommandLine -type BuildFableLibraryPython(?skipCore: bool) = +type BuildFableLibraryPython(?skipCore: bool, ?postFableBuildStage: unit -> unit) = inherit BuildFableLibrary( language = "python", @@ -23,6 +22,7 @@ type BuildFableLibraryPython(?skipCore: bool) = ) let skipCore = defaultArg skipCore false + let postFableBuildStage = defaultArg postFableBuildStage ignore override this.CopyStage() = // Copy all Python/F# files to the build directory @@ -59,3 +59,6 @@ type BuildFableLibraryPython(?skipCore: bool) = Command.Run("uv", $"run ruff check --select I,F401 --fix {this.BuildDir}") // Run Ruff formatter on all generated files Command.Run("uv", $"run ruff format {this.BuildDir}") + + // Run the post fable buil stage if provided (used for quicktest to install the library in editable mode) + postFableBuildStage () diff --git a/src/Fable.Build/Quicktest/Python.fs b/src/Fable.Build/Quicktest/Python.fs index b309999031..7c0bf8b61c 100644 --- a/src/Fable.Build/Quicktest/Python.fs +++ b/src/Fable.Build/Quicktest/Python.fs @@ -10,20 +10,20 @@ let private fableLibraryBuildDir = Path.Resolve("temp", "fable-library-py") let handle (args: string list) = let skipFableLibraryCore = args |> List.contains "--skip-fable-library-core" - // Install local fable-library as editable package for testing - // This ensures quicktest uses the locally built version, not PyPI - if (args |> List.contains "--force-fable-library") then - BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(true) - // Install fable-library in editable mode - Command.Run("uv", $"pip install -e {fableLibraryBuildDir}") - genericQuicktest { Language = "python" - FableLibBuilder = BuildFableLibraryPython() + FableLibBuilder = + BuildFableLibraryPython( + skipCore = skipFableLibraryCore, + postFableBuildStage = + (fun () -> + // Install the fable-library-py in editable mode so that it's available for the quicktest script + Command.Run("uv", $"pip install -e {fableLibraryBuildDir}") + ) + ) ProjectDir = "src/quicktest-py" Extension = ".py" RunMode = RunScript } - // Always skip library in genericQuicktest since we handled it above args