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: 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/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/`). 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..4727a66756 100644 --- a/src/Fable.Build/FableLibrary/Core.fs +++ b/src/Fable.Build/FableLibrary/Core.fs @@ -1,19 +1,26 @@ namespace Build.FableLibrary 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 /// /// 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 +69,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-library-%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..12b3a41c3d 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..9f5c9a3d91 100644 --- a/src/Fable.Build/FableLibrary/Python.fs +++ b/src/Fable.Build/FableLibrary/Python.fs @@ -4,19 +4,25 @@ 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", 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 + let postFableBuildStage = defaultArg postFableBuildStage ignore override this.CopyStage() = // Copy all Python/F# files to the build directory @@ -53,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/FableLibrary/Rust.fs b/src/Fable.Build/FableLibrary/Rust.fs index 7989e0d946..f5b0139616 100644 --- a/src/Fable.Build/FableLibrary/Rust.fs +++ b/src/Fable.Build/FableLibrary/Rust.fs @@ -12,7 +12,13 @@ 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") + Path.Combine("src", "fable-library-rust", "Cargo.toml") + ] ) 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/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 diff --git a/src/Fable.Build/Quicktest/Python.fs b/src/Fable.Build/Quicktest/Python.fs index 8234fc182b..7c0bf8b61c 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") @@ -12,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 not (args |> List.contains "--skip-fable-library") then - BuildFableLibraryPython(skipCore = skipFableLibraryCore).Run(false) - // 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 - ("--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..09bcadbabc --- /dev/null +++ b/src/Fable.Build/Utils/IncrementalBuild.fs @@ -0,0 +1,71 @@ +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 + else + printfn "Skipping '%s' (inputs unchanged and outputs exist)" name 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 ```