diff --git a/changes/root-source-dir-rebuild.fix.md b/changes/root-source-dir-rebuild.fix.md new file mode 100644 index 0000000000..032a3e3257 --- /dev/null +++ b/changes/root-source-dir-rebuild.fix.md @@ -0,0 +1,2 @@ +- Stop `rescript` from rebuilding the modules of a `"."` or `"./src"` source + dir, and their dependents, on every build when nothing changed. diff --git a/rewatch-ocaml/build_artifacts.ml b/rewatch-ocaml/build_artifacts.ml index 19d00d4dd6..2b48144049 100644 --- a/rewatch-ocaml/build_artifacts.ml +++ b/rewatch-ocaml/build_artifacts.ml @@ -12,10 +12,13 @@ let relative_output_directory path (spec : Config.package_spec) = let generated_js_path (config : Config.t) path (spec : Config.package_spec) = let output_dir = relative_output_directory path spec in + (* The output of a module of the package root is "A.js" or "lib/es6/A.js", + as the output inventory spells it, not "./A.js" or "lib/es6/./A.js". *) Filename.concat config.root - (Filename.concat output_dir - (Filename.remove_extension (Filename.basename path) - ^ Config.package_spec_suffix config spec)) + (Source.without_current_dir_segments + (Filename.concat output_dir + (Filename.remove_extension (Filename.basename path) + ^ Config.package_spec_suffix config spec))) let published_ast_path ~ocaml_dir source = Filename.concat ocaml_dir (Filename.basename (Source.ast_path source)) diff --git a/rewatch-ocaml/source.ml b/rewatch-ocaml/source.ml index 750f8a838d..214b71b310 100644 --- a/rewatch-ocaml/source.ml +++ b/rewatch-ocaml/source.ml @@ -81,6 +81,22 @@ let interface_mismatch_error implementation interface = different path names or different cases: `%s` vs `%s`" implementation interface) +(* [path] without its "." segments, so a file found below source dir "." or + "./src" has the path bsc records for it, "A.res" or "src/A.res". Cleanup and + freshness match modules to their ASTs and outputs by these paths: a + "./A.res" matched no AST, and a "./ui/U.js" in the inventory of "." matched + no module's output, so each build removed them and rebuilt the module. *) +let rec without_current_dir_segments path = + let directory = Filename.dirname path in + let base = Filename.basename path in + if directory = path || path = Filename.current_dir_name then path + else if base = Filename.current_dir_name then + without_current_dir_segments directory + else + match without_current_dir_segments directory with + | directory when directory = Filename.current_dir_name -> base + | directory -> Filename.concat directory base + (* A package source tree has three consumers with deliberately different recursion rules. Compilation follows directory links and honors source activation/subdirs; cleanup inventories every real descendant but treats @@ -134,7 +150,11 @@ let scan_source ~root (source : Config.source) ~discover_modules ~on_missing | Some kind -> candidates := { - file = {path = relative_path; modified = metadata.Unix.st_mtime}; + file = + { + path = without_current_dir_segments relative_path; + modified = metadata.Unix.st_mtime; + }; kind; is_dev = source.is_dev; } @@ -143,7 +163,9 @@ let scan_source ~root (source : Config.source) ~discover_modules ~on_missing List.iter (fun name -> let relative_path = Filename.concat relative name in - let absolute_path = Filename.concat root relative_path in + let absolute_path = + Filename.concat root (without_current_dir_segments relative_path) + in match Unix.lstat absolute_path with | metadata -> ( match metadata.Unix.st_kind with diff --git a/rewatch-ocaml/source.mli b/rewatch-ocaml/source.mli index aca9f6f8df..c81748d8b9 100644 --- a/rewatch-ocaml/source.mli +++ b/rewatch-ocaml/source.mli @@ -23,6 +23,10 @@ type source_kind = Implementation | Interface val source_kind : string -> source_kind option val module_name : string -> string + +val without_current_dir_segments : string -> string +(** [path] without its "." segments: "./src/./A.res" is "src/A.res". *) + val namespace_members : entry:string option -> module_ list -> module_ list val duplicate_error : diff --git a/rewatch/tests/compile/23-root-source-dir-rebuild.sh b/rewatch/tests/compile/23-root-source-dir-rebuild.sh new file mode 100755 index 0000000000..eaa0591746 --- /dev/null +++ b/rewatch/tests/compile/23-root-source-dir-rebuild.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +cd $(dirname $0) +source "../utils.sh" + +bold "Test: Modules of source dir \".\" are not rebuilt when nothing changed" + +fixtures=$(mktemp -d 2>/dev/null || mktemp -d -t rewatch-root-source-dir) +trap "rm -rf '$fixtures'" EXIT + +# "." spells its modules "./Root.res" unless the build system drops the +# segment, while bsc records "Root.res" in the AST and the output inventory +# finds "Root.mjs" or "lib/es6/Root.mjs". A mismatch makes every build clean +# those artifacts. +check_layout() { + local name=$1 in_source=$2 root_output=$3 ui_output=$4 + local fixture="$fixtures/$name" + mkdir -p "$fixture/ui" + + cat > "$fixture/rescript.json" < "$fixture/Root.res" + printf 'let value = Root.value + 1\n' > "$fixture/ui/Component.res" + + if ! compiler_output=$(cd "$fixture" && "$REWATCH_EXECUTABLE" build 2>&1); then + error "Initial $name fixture build failed" + printf "%s\n" "$compiler_output" >&2 + return 1 + fi + + # Several unchanged builds, since a removed output only shows on the next one. + for attempt in 1 2 3; do + compiler_output=$(cd "$fixture" && "$REWATCH_EXECUTABLE" build 2>&1) + if ! printf '%s\n' "$compiler_output" | grep -q "Compiled 0 modules"; then + error "Unchanged $name build $attempt recompiled modules" + printf "%s\n" "$compiler_output" >&2 + return 1 + fi + done + + if [ ! -f "$fixture/$root_output" ] || [ ! -f "$fixture/$ui_output" ]; then + error "Unchanged $name builds removed generated outputs" + return 1 + fi +} + +check_layout in-source true Root.mjs ui/Component.mjs || exit 1 +check_layout out-of-source false lib/es6/Root.mjs lib/es6/ui/Component.mjs || exit 1 + +success "Modules of source dir \".\" are not rebuilt when nothing changed" diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 83ea156111..a1551ffcc9 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -88,6 +88,7 @@ fi ./compile/20-schedule-independent-modules-after-error.sh && ./compile/21-rebuild-dependency-for-package-specs.sh && ./compile/22-qualified-own-namespace-dependency.sh && +./compile/23-root-source-dir-rebuild.sh && ./compile/14-no-testrepo-changes.sh && ./compile/15-no-new-files.sh && ./compile/16-snapshots-unchanged.sh &&