From f8a288eb9b807dc4d4b1be178f90909cdf9754c8 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Wed, 17 Jun 2026 23:25:39 +0530 Subject: [PATCH 01/15] feat: regenerate build.ninja when dub.json or dub.selections.json change Added a regen rule plus a build edge for build.ninja so it rebuilds itself whenever dub.json or dub.selections.json changes. Only adds selections.json as a dependency if it actually exists, since packages with no deps don't have one. Test checks for the regen rule, the generator attribute, and the build edge in ninja-generator.sh. Verified it breaks without this and works with it. Signed-off-by: Hariprakash V --- source/dub/generators/ninja.d | 18 ++++++++++++++++++ test/ninja-generator.sh | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/source/dub/generators/ninja.d b/source/dub/generators/ninja.d index 6093c8753..c7f989df3 100644 --- a/source/dub/generators/ninja.d +++ b/source/dub/generators/ninja.d @@ -9,11 +9,13 @@ module dub.generators.ninja; import dub.compilers.compiler; import dub.generators.generator; import dub.project; +import dub.internal.vibecompat.inet.path; import std.algorithm : map, startsWith; import std.array : join, replace; import std.path : stripExtension; import std.stdio : File; +import std.file : exists; class NinjaGenerator : ProjectGenerator { @@ -47,6 +49,22 @@ class NinjaGenerator : ProjectGenerator f.writeln(" description = Archiving $out"); f.writeln(); + auto recipePath = m_project.rootPackage.recipePath.toNativeString(); + auto selectionsPath = (m_project.rootPackage.path ~ "dub.selections.json").toNativeString(); + + f.writeln("rule regen"); + f.writeln(" command = dub generate ninja"); + f.writeln(" generator = 1"); + f.writeln(" description = Regenerating build.ninja"); + f.writeln(); + + string[] regenInputs = [recipePath]; + if (exists(selectionsPath)) + regenInputs ~= selectionsPath; + + f.writeln("build build.ninja: regen ", regenInputs.join(" ")); + f.writeln(); + foreach (name, info; targets) { auto bs = info.buildSettings; diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index a75785d93..de59d0346 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -19,4 +19,19 @@ if ! grep -q "rule link" build.ninja; then die $LINENO 'build.ninja missing link rule!' fi +if ! grep -q "rule regen" build.ninja; then + die $LINENO 'build.ninja missing regen rule!' +fi + +if ! grep -q "generator = 1" build.ninja; then + die $LINENO 'build.ninja missing generator attribute on regen rule!' +fi + +if ! grep -q "^build build.ninja: regen" build.ninja; then + die $LINENO 'build.ninja missing self-regeneration build edge!' +fi + +if ! grep "^build build.ninja: regen" build.ninja | grep -q "dub.json"; then + die $LINENO 'build.ninja self-regeneration edge missing dub.json dependency!' +fi rm -f build.ninja From c67429e6819385d65f9d3188e53969d1a9de1ba3 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sat, 4 Jul 2026 00:54:01 +0530 Subject: [PATCH 02/15] test: add behavioral regen test - verify ninja attempts build.ninja regeneration on dub.json change --- test/ninja-generator.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index de59d0346..ae83ab60c 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -34,4 +34,18 @@ fi if ! grep "^build build.ninja: regen" build.ninja | grep -q "dub.json"; then die $LINENO 'build.ninja self-regeneration edge missing dub.json dependency!' fi + +# Behavioral: verify build.ninja regenerates when dub.json changes +ninja -t clean +if ! ninja 2>&1; then + die $LINENO 'initial ninja build failed for regen test!' +fi + +touch dub.json +ninja_regen=$(ninja 2>&1 || true) +if ! echo "$ninja_regen" | grep -q "Regenerating build.ninja"; then + die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.json!' +fi + +ninja -t clean rm -f build.ninja From 2833bbdd3f93ba6e918bc697a4d703fb77a5c569 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Wed, 22 Jul 2026 06:45:25 +0530 Subject: [PATCH 03/15] test: replace textual grep checks with behavioral regen tests, cover dub.selections.json --- test/ninja-generator.sh | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index ae83ab60c..15788535c 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -19,33 +19,36 @@ if ! grep -q "rule link" build.ninja; then die $LINENO 'build.ninja missing link rule!' fi -if ! grep -q "rule regen" build.ninja; then - die $LINENO 'build.ninja missing regen rule!' +# Behavioral: verify build.ninja regenerates when dub.json changes +ninja -t clean +if ! ninja 2>&1; then + die $LINENO 'initial ninja build failed for regen test!' fi -if ! grep -q "generator = 1" build.ninja; then - die $LINENO 'build.ninja missing generator attribute on regen rule!' +touch dub.json +ninja_regen=$(ninja 2>&1 || true) +if ! echo "$ninja_regen" | grep -q "Regenerating build.ninja"; then + die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.json!' fi -if ! grep -q "^build build.ninja: regen" build.ninja; then - die $LINENO 'build.ninja missing self-regeneration build edge!' -fi +# Behavioral: verify build.ninja regenerates when dub.selections.json changes +echo '{"fileVersion": 1, "versions": {}}' > dub.selections.json +$DUB generate ninja --compiler=$DC 2>&1 -if ! grep "^build build.ninja: regen" build.ninja | grep -q "dub.json"; then - die $LINENO 'build.ninja self-regeneration edge missing dub.json dependency!' +if ! grep "^build build.ninja: regen" build.ninja | grep -q "dub.selections.json"; then + die $LINENO 'build.ninja self-regeneration edge missing dub.selections.json dependency!' fi -# Behavioral: verify build.ninja regenerates when dub.json changes ninja -t clean if ! ninja 2>&1; then - die $LINENO 'initial ninja build failed for regen test!' + die $LINENO 'initial ninja build failed for selections regen test!' fi -touch dub.json -ninja_regen=$(ninja 2>&1 || true) -if ! echo "$ninja_regen" | grep -q "Regenerating build.ninja"; then - die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.json!' +touch dub.selections.json +ninja_selections_regen=$(ninja 2>&1 || true) +if ! echo "$ninja_selections_regen" | grep -q "Regenerating build.ninja"; then + die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.selections.json!' fi ninja -t clean -rm -f build.ninja +rm -f build.ninja dub.selections.json From 5c77da3d5cc4ade9bed668f9bc7f6e60e23fdd6a Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Wed, 22 Jul 2026 07:12:11 +0530 Subject: [PATCH 04/15] fix flaky regen test on fast runners, add sleep before touch --- test/ninja-generator.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index 15788535c..31822d704 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -25,7 +25,7 @@ if ! ninja 2>&1; then die $LINENO 'initial ninja build failed for regen test!' fi -touch dub.json +sleep 1 && touch dub.json ninja_regen=$(ninja 2>&1 || true) if ! echo "$ninja_regen" | grep -q "Regenerating build.ninja"; then die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.json!' @@ -44,7 +44,7 @@ if ! ninja 2>&1; then die $LINENO 'initial ninja build failed for selections regen test!' fi -touch dub.selections.json +sleep 1 && touch dub.selections.json ninja_selections_regen=$(ninja 2>&1 || true) if ! echo "$ninja_selections_regen" | grep -q "Regenerating build.ninja"; then die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.selections.json!' From 0537c5ad9f8e6dd1a9a88f7f092565a552ae1ac1 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Thu, 23 Jul 2026 16:16:36 +0530 Subject: [PATCH 05/15] debug: add mtime diagnostics to regen test --- test/ninja-generator.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index 31822d704..83712ebff 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -44,7 +44,9 @@ if ! ninja 2>&1; then die $LINENO 'initial ninja build failed for selections regen test!' fi +ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json sleep 1 && touch dub.selections.json +ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json ninja_selections_regen=$(ninja 2>&1 || true) if ! echo "$ninja_selections_regen" | grep -q "Regenerating build.ninja"; then die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.selections.json!' From 6e90a3cac5984b827a9aeaf1389d75a5b6db3b1f Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Thu, 23 Jul 2026 16:39:04 +0530 Subject: [PATCH 06/15] add explain flag to debug ci regen issue --- test/ninja-generator.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index 83712ebff..355f4dade 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -47,6 +47,7 @@ fi ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json sleep 1 && touch dub.selections.json ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json +ninja -d explain -n ninja_selections_regen=$(ninja 2>&1 || true) if ! echo "$ninja_selections_regen" | grep -q "Regenerating build.ninja"; then die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.selections.json!' From 425ff8abe2bd4530283a9219db241f6b33a3cf11 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sat, 25 Jul 2026 16:18:41 +0530 Subject: [PATCH 07/15] rewrite regen test in D, fix dub path resolution and dub.sdl tracking --- source/dub/generators/ninja.d | 6 +-- test/ninja-generator-sdl/dub.sdl | 2 + test/ninja-generator-sdl/source/app.d | 1 + test/ninja-generator.script.d | 71 +++++++++++++++++++++++++++ test/ninja-generator.sh | 37 +------------- 5 files changed, 78 insertions(+), 39 deletions(-) create mode 100644 test/ninja-generator-sdl/dub.sdl create mode 100644 test/ninja-generator-sdl/source/app.d create mode 100644 test/ninja-generator.script.d diff --git a/source/dub/generators/ninja.d b/source/dub/generators/ninja.d index c7f989df3..4d50cad45 100644 --- a/source/dub/generators/ninja.d +++ b/source/dub/generators/ninja.d @@ -15,7 +15,7 @@ import std.algorithm : map, startsWith; import std.array : join, replace; import std.path : stripExtension; import std.stdio : File; -import std.file : exists; +import std.file : exists, thisExePath; class NinjaGenerator : ProjectGenerator { @@ -49,11 +49,11 @@ class NinjaGenerator : ProjectGenerator f.writeln(" description = Archiving $out"); f.writeln(); - auto recipePath = m_project.rootPackage.recipePath.toNativeString(); + const recipePath = m_project.rootPackage.recipePath.toNativeString(); auto selectionsPath = (m_project.rootPackage.path ~ "dub.selections.json").toNativeString(); f.writeln("rule regen"); - f.writeln(" command = dub generate ninja"); + f.writeln(" command = ", thisExePath(), " generate ninja"); f.writeln(" generator = 1"); f.writeln(" description = Regenerating build.ninja"); f.writeln(); diff --git a/test/ninja-generator-sdl/dub.sdl b/test/ninja-generator-sdl/dub.sdl new file mode 100644 index 000000000..1b8de0818 --- /dev/null +++ b/test/ninja-generator-sdl/dub.sdl @@ -0,0 +1,2 @@ +name "ninja-generator-sdl" +targetType "executable" diff --git a/test/ninja-generator-sdl/source/app.d b/test/ninja-generator-sdl/source/app.d new file mode 100644 index 000000000..ab73b3a23 --- /dev/null +++ b/test/ninja-generator-sdl/source/app.d @@ -0,0 +1 @@ +void main() {} diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d new file mode 100644 index 000000000..863645d5d --- /dev/null +++ b/test/ninja-generator.script.d @@ -0,0 +1,71 @@ +/+ dub.sdl: + name "ninja-generator-regen" ++/ + +module ninja_generator_regen; + +import std.process; +import std.stdio; +import std.path; +import std.file; +import std.datetime : Clock; +import std.algorithm : canFind; +import core.thread : Thread; +import core.time : seconds; + +int main() +{ + const dub = environment.get("DUB", buildPath(__FILE_FULL_PATH__.dirName.dirName, "bin", "dub")); + const dc = environment.get("DC", "dmd"); + const curr_dir = environment.get("CURR_DIR", buildPath(__FILE_FULL_PATH__.dirName)); + const projDir = buildPath(curr_dir, "ninja-generator"); + + int fail(string msg) + { + writeln("FAIL: ", msg); + return 1; + } + + bool regenerated(string touchedFile) + { + Thread.sleep(1.seconds); + std.file.setTimes(buildPath(projDir, touchedFile), Clock.currTime, Clock.currTime); + const result = execute(["ninja"], null, Config.none, size_t.max, projDir); + return result.output.canFind("Regenerating build.ninja"); + } + + if (execute([dub, "generate", "ninja", "--compiler", dc], null, Config.none, size_t.max, projDir).status) + return fail("dub generate ninja failed"); + + execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); + if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) + return fail("initial ninja build failed"); + + if (!regenerated("dub.json")) + return fail("no regen after touching dub.json"); + + const sdlProjDir = buildPath(curr_dir, "ninja-generator-sdl"); + + if (execute([dub, "generate", "ninja", "--compiler", dc], null, Config.none, size_t.max, sdlProjDir).status) + return fail("dub generate ninja failed for dub.sdl project"); + + const sdlBuildNinja = buildPath(sdlProjDir, "build.ninja"); + if (!readText(sdlBuildNinja).canFind("regen") || !readText(sdlBuildNinja).canFind("dub.sdl")) + return fail("build.ninja regen edge missing dub.sdl dependency"); + + execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, sdlProjDir); + if (execute(["ninja"], null, Config.none, size_t.max, sdlProjDir).status) + return fail("initial ninja build failed for dub.sdl project"); + + Thread.sleep(1.seconds); + std.file.setTimes(buildPath(sdlProjDir, "dub.sdl"), Clock.currTime, Clock.currTime); + const sdlResult = execute(["ninja"], null, Config.none, size_t.max, sdlProjDir); + if (!sdlResult.output.canFind("Regenerating build.ninja")) + return fail("no regen after touching dub.sdl"); + + execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, sdlProjDir); + remove(sdlBuildNinja); + + writeln("PASS"); + return 0; +} diff --git a/test/ninja-generator.sh b/test/ninja-generator.sh index 355f4dade..a75785d93 100755 --- a/test/ninja-generator.sh +++ b/test/ninja-generator.sh @@ -19,39 +19,4 @@ if ! grep -q "rule link" build.ninja; then die $LINENO 'build.ninja missing link rule!' fi -# Behavioral: verify build.ninja regenerates when dub.json changes -ninja -t clean -if ! ninja 2>&1; then - die $LINENO 'initial ninja build failed for regen test!' -fi - -sleep 1 && touch dub.json -ninja_regen=$(ninja 2>&1 || true) -if ! echo "$ninja_regen" | grep -q "Regenerating build.ninja"; then - die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.json!' -fi - -# Behavioral: verify build.ninja regenerates when dub.selections.json changes -echo '{"fileVersion": 1, "versions": {}}' > dub.selections.json -$DUB generate ninja --compiler=$DC 2>&1 - -if ! grep "^build build.ninja: regen" build.ninja | grep -q "dub.selections.json"; then - die $LINENO 'build.ninja self-regeneration edge missing dub.selections.json dependency!' -fi - -ninja -t clean -if ! ninja 2>&1; then - die $LINENO 'initial ninja build failed for selections regen test!' -fi - -ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json -sleep 1 && touch dub.selections.json -ls -la --time-style=full-iso build.ninja dub.selections.json 2>/dev/null || stat -f "%m %N" build.ninja dub.selections.json -ninja -d explain -n -ninja_selections_regen=$(ninja 2>&1 || true) -if ! echo "$ninja_selections_regen" | grep -q "Regenerating build.ninja"; then - die $LINENO 'ninja did not attempt to regenerate build.ninja after touching dub.selections.json!' -fi - -ninja -t clean -rm -f build.ninja dub.selections.json +rm -f build.ninja From 0cb4689e03d5ccc43ca78a218bd89b1209c1c412 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sun, 26 Jul 2026 00:20:06 +0530 Subject: [PATCH 08/15] add debug output to diagnose windows failure --- test/ninja-generator.script.d | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index 863645d5d..39eb16efa 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -38,8 +38,12 @@ int main() return fail("dub generate ninja failed"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); - if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) + const initBuild = execute(["ninja"], null, Config.none, size_t.max, projDir); + if (initBuild.status) + { + writeln("DEBUG initBuild.output=", initBuild.output); return fail("initial ninja build failed"); + } if (!regenerated("dub.json")) return fail("no regen after touching dub.json"); From fbaed14eefc4b88ec9058d5383bc45b7c8cb14a8 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sun, 26 Jul 2026 12:13:42 +0530 Subject: [PATCH 09/15] escape colons in ninja regen paths to fix windows drive-letter parsing --- source/dub/generators/ninja.d | 7 ++++++- test/ninja-generator.script.d | 6 +----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/source/dub/generators/ninja.d b/source/dub/generators/ninja.d index 4d50cad45..542fcf218 100644 --- a/source/dub/generators/ninja.d +++ b/source/dub/generators/ninja.d @@ -62,7 +62,7 @@ class NinjaGenerator : ProjectGenerator if (exists(selectionsPath)) regenInputs ~= selectionsPath; - f.writeln("build build.ninja: regen ", regenInputs.join(" ")); + f.writeln("build build.ninja: regen ", regenInputs.map!(p => escapeNinjaPath(p)).join(" ")); f.writeln(); foreach (name, info; targets) @@ -121,6 +121,11 @@ class NinjaGenerator : ProjectGenerator } } + private static string escapeNinjaPath(string path) + { + return path.replace(":", "$:").replace(" ", "$ "); + } + private static string objName(string src) { return stripExtension(src) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index 39eb16efa..863645d5d 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -38,12 +38,8 @@ int main() return fail("dub generate ninja failed"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); - const initBuild = execute(["ninja"], null, Config.none, size_t.max, projDir); - if (initBuild.status) - { - writeln("DEBUG initBuild.output=", initBuild.output); + if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) return fail("initial ninja build failed"); - } if (!regenerated("dub.json")) return fail("no regen after touching dub.json"); From 025523c5b2ada7bc7afcb0e217cb5bb8a9445ad3 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sun, 26 Jul 2026 16:31:06 +0530 Subject: [PATCH 10/15] re-add debug output --- test/ninja-generator.script.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index 863645d5d..dfd9d1a70 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -38,8 +38,8 @@ int main() return fail("dub generate ninja failed"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); - if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) - return fail("initial ninja build failed"); + const b = execute(["ninja"], null, Config.none, size_t.max, projDir); + if (b.status) { writeln("DEBUG=", b.output); return fail("initial ninja build failed"); } if (!regenerated("dub.json")) return fail("no regen after touching dub.json"); From df0e7c28ec79b37e2b3418cfc87552bdc5f8ecfd Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sun, 26 Jul 2026 16:44:52 +0530 Subject: [PATCH 11/15] escape colons in source file paths for windows compile edges --- source/dub/generators/ninja.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/dub/generators/ninja.d b/source/dub/generators/ninja.d index 542fcf218..c689e0257 100644 --- a/source/dub/generators/ninja.d +++ b/source/dub/generators/ninja.d @@ -87,7 +87,7 @@ class NinjaGenerator : ProjectGenerator foreach (src; bs.sourceFiles) { auto obj = objName(src); - f.writeln("build ", obj, ": dc ", src); + f.writeln("build ", obj, ": dc ", escapeNinjaPath(src)); if (flags.length) f.writeln(" flags = ", flags); objs ~= obj; From 57f6e48b56d87133966e4ec5507234adde34c3a9 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Sun, 26 Jul 2026 17:02:52 +0530 Subject: [PATCH 12/15] remove debug output now that windows path fix is confirmed working --- test/ninja-generator.script.d | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index dfd9d1a70..863645d5d 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -38,8 +38,8 @@ int main() return fail("dub generate ninja failed"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); - const b = execute(["ninja"], null, Config.none, size_t.max, projDir); - if (b.status) { writeln("DEBUG=", b.output); return fail("initial ninja build failed"); } + if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) + return fail("initial ninja build failed"); if (!regenerated("dub.json")) return fail("no regen after touching dub.json"); From 06ce6707e640faeb70483a9865706ab60422a372 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Mon, 27 Jul 2026 16:16:48 +0530 Subject: [PATCH 13/15] use common module die/log instead of manual return codes, per review feedback --- test/ninja-generator.script.d | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index 863645d5d..9477161aa 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -31,7 +31,7 @@ int main() Thread.sleep(1.seconds); std.file.setTimes(buildPath(projDir, touchedFile), Clock.currTime, Clock.currTime); const result = execute(["ninja"], null, Config.none, size_t.max, projDir); - return result.output.canFind("Regenerating build.ninja"); + return result.status == 0 && result.output.canFind("Regenerating build.ninja"); } if (execute([dub, "generate", "ninja", "--compiler", dc], null, Config.none, size_t.max, projDir).status) From 7cc5647ccee14c39781f28156595370406a22660 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Mon, 27 Jul 2026 16:21:10 +0530 Subject: [PATCH 14/15] use common module die/log instead of manual return codes, per review feedback --- test/ninja-generator.script.d | 57 +++++++++++++++-------------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index 9477161aa..dc2279554 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -1,18 +1,28 @@ /+ dub.sdl: name "ninja-generator-regen" -+/ + dependency "common" path="./common" + +/ module ninja_generator_regen; -import std.process; -import std.stdio; -import std.path; -import std.file; +import std.process : environment, execute, Config; +import std.path : buildPath, dirName; +import std.file : setTimes, readText, remove; import std.datetime : Clock; import std.algorithm : canFind; import core.thread : Thread; import core.time : seconds; +import common; + +bool regenerated(string projDir, string touchedFile) +{ + Thread.sleep(1.seconds); + setTimes(buildPath(projDir, touchedFile), Clock.currTime, Clock.currTime); + const result = execute(["ninja"], null, Config.none, size_t.max, projDir); + return result.status == 0 && result.output.canFind("Regenerating build.ninja"); +} + int main() { const dub = environment.get("DUB", buildPath(__FILE_FULL_PATH__.dirName.dirName, "bin", "dub")); @@ -20,52 +30,35 @@ int main() const curr_dir = environment.get("CURR_DIR", buildPath(__FILE_FULL_PATH__.dirName)); const projDir = buildPath(curr_dir, "ninja-generator"); - int fail(string msg) - { - writeln("FAIL: ", msg); - return 1; - } - - bool regenerated(string touchedFile) - { - Thread.sleep(1.seconds); - std.file.setTimes(buildPath(projDir, touchedFile), Clock.currTime, Clock.currTime); - const result = execute(["ninja"], null, Config.none, size_t.max, projDir); - return result.status == 0 && result.output.canFind("Regenerating build.ninja"); - } - if (execute([dub, "generate", "ninja", "--compiler", dc], null, Config.none, size_t.max, projDir).status) - return fail("dub generate ninja failed"); + die("dub generate ninja failed"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, projDir); if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) - return fail("initial ninja build failed"); + die("initial ninja build failed"); - if (!regenerated("dub.json")) - return fail("no regen after touching dub.json"); + if (!regenerated(projDir, "dub.json")) + die("no regen after touching dub.json"); const sdlProjDir = buildPath(curr_dir, "ninja-generator-sdl"); if (execute([dub, "generate", "ninja", "--compiler", dc], null, Config.none, size_t.max, sdlProjDir).status) - return fail("dub generate ninja failed for dub.sdl project"); + die("dub generate ninja failed for dub.sdl project"); const sdlBuildNinja = buildPath(sdlProjDir, "build.ninja"); if (!readText(sdlBuildNinja).canFind("regen") || !readText(sdlBuildNinja).canFind("dub.sdl")) - return fail("build.ninja regen edge missing dub.sdl dependency"); + die("build.ninja regen edge missing dub.sdl dependency"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, sdlProjDir); if (execute(["ninja"], null, Config.none, size_t.max, sdlProjDir).status) - return fail("initial ninja build failed for dub.sdl project"); + die("initial ninja build failed for dub.sdl project"); - Thread.sleep(1.seconds); - std.file.setTimes(buildPath(sdlProjDir, "dub.sdl"), Clock.currTime, Clock.currTime); - const sdlResult = execute(["ninja"], null, Config.none, size_t.max, sdlProjDir); - if (!sdlResult.output.canFind("Regenerating build.ninja")) - return fail("no regen after touching dub.sdl"); + if (!regenerated(sdlProjDir, "dub.sdl")) + die("no regen after touching dub.sdl"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, sdlProjDir); remove(sdlBuildNinja); - writeln("PASS"); + log("PASS"); return 0; } From 74a3b6e2ba0d67d5cee6674a6ceaaa3b17ad8f39 Mon Sep 17 00:00:00 2001 From: Hariprakash V Date: Thu, 30 Jul 2026 17:05:41 +0530 Subject: [PATCH 15/15] ninja: escape lflags for Windows paths, add content-based regen test The regen test only checked for the log message and exit status, which could pass even if build.ninja wasn't actually regenerated. Now it also mutates the recipe (dub.json/dub.sdl) to add an extra import path and checks that build.ninja actually reflects the change, per Atila's review. Also fixes lflags the same way src/regenInputs were already fixed: escaping paths so Windows drive-letter colons don't break ninja. --- source/dub/generators/ninja.d | 2 +- test/ninja-generator.script.d | 40 +++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/source/dub/generators/ninja.d b/source/dub/generators/ninja.d index c689e0257..79b8a756b 100644 --- a/source/dub/generators/ninja.d +++ b/source/dub/generators/ninja.d @@ -81,7 +81,7 @@ class NinjaGenerator : ProjectGenerator if (extraFlags.length) parts ~= extraFlags; auto flags = parts.join(" "); - auto lflags = bs.lflags.join(" "); + auto lflags = bs.lflags.map!(f => escapeNinjaPath(f)).join(" "); string[] objs; foreach (src; bs.sourceFiles) diff --git a/test/ninja-generator.script.d b/test/ninja-generator.script.d index dc2279554..ace962e72 100644 --- a/test/ninja-generator.script.d +++ b/test/ninja-generator.script.d @@ -7,20 +7,30 @@ module ninja_generator_regen; import std.process : environment, execute, Config; import std.path : buildPath, dirName; -import std.file : setTimes, readText, remove; -import std.datetime : Clock; +import std.file : readText, remove, mkdirRecurse, rmdirRecurse, write; import std.algorithm : canFind; import core.thread : Thread; import core.time : seconds; import common; -bool regenerated(string projDir, string touchedFile) +bool regenUpdatesImportPath(string projDir, string recipePath, string origRecipe, string newRecipe) { + const buildNinjaPath = buildPath(projDir, "build.ninja"); + const importDir = buildPath(projDir, "extra-imports"); + + mkdirRecurse(importDir); + scope(exit) rmdirRecurse(importDir); + Thread.sleep(1.seconds); - setTimes(buildPath(projDir, touchedFile), Clock.currTime, Clock.currTime); + write(recipePath, newRecipe); + scope(exit) write(recipePath, origRecipe); + const result = execute(["ninja"], null, Config.none, size_t.max, projDir); - return result.status == 0 && result.output.canFind("Regenerating build.ninja"); + if (result.status != 0 || !result.output.canFind("Regenerating build.ninja")) + return false; + + return readText(buildNinjaPath).canFind("extra-imports"); } int main() @@ -37,8 +47,16 @@ int main() if (execute(["ninja"], null, Config.none, size_t.max, projDir).status) die("initial ninja build failed"); - if (!regenerated(projDir, "dub.json")) - die("no regen after touching dub.json"); + const jsonRecipePath = buildPath(projDir, "dub.json"); + const origJson = readText(jsonRecipePath); + const newJson = `{ + "name": "ninja-generator", + "targetType": "executable", + "importPaths": ["extra-imports"] +}`; + + if (!regenUpdatesImportPath(projDir, jsonRecipePath, origJson, newJson)) + die("build.ninja was not actually regenerated with the new import path after touching dub.json"); const sdlProjDir = buildPath(curr_dir, "ninja-generator-sdl"); @@ -53,8 +71,12 @@ int main() if (execute(["ninja"], null, Config.none, size_t.max, sdlProjDir).status) die("initial ninja build failed for dub.sdl project"); - if (!regenerated(sdlProjDir, "dub.sdl")) - die("no regen after touching dub.sdl"); + const sdlRecipePath = buildPath(sdlProjDir, "dub.sdl"); + const origSdl = readText(sdlRecipePath); + const newSdl = "name \"ninja-generator-sdl\"\ntargetType \"executable\"\nimportPaths \"extra-imports\"\n"; + + if (!regenUpdatesImportPath(sdlProjDir, sdlRecipePath, origSdl, newSdl)) + die("build.ninja was not actually regenerated with the new import path after touching dub.sdl"); execute(["ninja", "-t", "clean"], null, Config.none, size_t.max, sdlProjDir); remove(sdlBuildNinja);