Skip to content

Commit fce9cc6

Browse files
committed
Fixes for Ninja on Windows
1 parent a73d125 commit fce9cc6

File tree

5 files changed

+671
-8
lines changed

5 files changed

+671
-8
lines changed

modules/ninja/ninja_cpp.lua

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,17 @@ end
182182

183183
function m.prebuildcommandsrule(cfg, toolset)
184184
_p("rule prebuild")
185-
_p(" command = $prebuildcommands")
185+
-- Use shell wrapper based on os.shell() to ensure proper command execution
186+
-- Respects --shell option, defaults to cmd on Windows, posix elsewhere
187+
local shell = os.shell()
188+
if shell == "cmd" then
189+
_p(" command = cmd /c $prebuildcommands")
190+
elseif shell == "posix" then
191+
_p(" command = sh -c \"$prebuildcommands\"")
192+
else
193+
-- Default fallback: execute directly
194+
_p(" command = $prebuildcommands")
195+
end
186196
_p(" description = Running pre-build commands")
187197
_p("")
188198
end
@@ -196,7 +206,15 @@ end
196206

197207
function m.prelinkcommandsrule(cfg, toolset)
198208
_p("rule prelink")
199-
_p(" command = $prelinkcommands")
209+
-- Use shell wrapper based on target system's shell to ensure proper command execution
210+
local shell = _OPTIONS.shell or iif(cfg.system == p.WINDOWS, "cmd", "posix")
211+
if shell == "cmd" then
212+
_p(" command = cmd /c $prelinkcommands")
213+
elseif shell == "posix" then
214+
_p(" command = sh -c \"$prelinkcommands\"")
215+
else
216+
_p(" command = $prelinkcommands")
217+
end
200218
_p(" description = Running pre-link commands")
201219
_p("")
202220
end
@@ -210,7 +228,15 @@ end
210228

211229
function m.postbuildcommandsrule(cfg, toolset)
212230
_p("rule postbuild")
213-
_p(" command = $postbuildcommands")
231+
-- Use shell wrapper based on target system's shell to ensure proper command execution
232+
local shell = _OPTIONS.shell or iif(cfg.system == p.WINDOWS, "cmd", "posix")
233+
if shell == "cmd" then
234+
_p(" command = cmd /c $postbuildcommands")
235+
elseif shell == "posix" then
236+
_p(" command = sh -c \"$postbuildcommands\"")
237+
else
238+
_p(" command = $postbuildcommands")
239+
end
214240
_p(" description = Running post-build commands")
215241
_p("")
216242
end
@@ -436,7 +462,7 @@ function m.getFileCxxFlags(cfg, filecfg, toolset)
436462
local allExternalIncludedirs = table.join(cfg.externalincludedirs or {}, filecfg.externalincludedirs or {})
437463
local allFrameworkdirs = table.join(cfg.frameworkdirs or {}, filecfg.frameworkdirs or {})
438464
local allIncludedirsafter = table.join(cfg.includedirsafter or {}, filecfg.includedirsafter or {})
439-
local includedirs = toolset.getincludedirs(cfg, allIncludedirs, allExternalIncludedirs, allFrameworkdirs, allIncludedirsafter)
465+
local includedirs = toolset.getincludedirs(cfg, allIncludedirs, allExternalIncludedDirs, allFrameworkdirs, allIncludedirsafter)
440466
flags = table.join(flags, includedirs)
441467

442468
local forceincludes = toolset.getforceincludes(filecfg)
@@ -539,6 +565,13 @@ function m.getLdFlags(cfg, toolset)
539565
flags = table.join(flags, rpaths)
540566
end
541567

568+
-- For MSVC shared libraries, add /IMPLIB: to specify the import library location
569+
-- MSVC is Windows-only, so no need to check cfg.system
570+
if cfg.kind == p.SHAREDLIB and toolset == p.tools.msc and not cfg.flags.NoImportLib then
571+
local impLibPath = path.getrelative(cfg.workspace.location, cfg.linktarget.directory) .. "/" .. cfg.linktarget.name
572+
table.insert(flags, "/IMPLIB:" .. impLibPath)
573+
end
574+
542575
return flags
543576
end
544577

@@ -1112,7 +1145,16 @@ function m.linkTarget(cfg)
11121145

11131146
local hasPostBuild = #cfg.postbuildcommands > 0 or cfg.postbuildmessage
11141147

1115-
_p("build %s: %s %s%s", targetPath, rule, table.concat(cfg._objectFiles, " "), implicitDeps)
1148+
-- For MSVC shared libraries with import libraries, list the import library as an implicit output
1149+
-- Dependencies will reference the .lib file (linktarget), which Ninja will know how to build.
1150+
-- MSVC is Windows-only, so no need to check cfg.system
1151+
local implicitOutputs = ""
1152+
if cfg.kind == p.SHAREDLIB and toolset == p.tools.msc and not cfg.flags.NoImportLib then
1153+
local impLibPath = path.getrelative(cfg.workspace.location, cfg.linktarget.directory) .. "/" .. cfg.linktarget.name
1154+
implicitOutputs = " | " .. impLibPath
1155+
end
1156+
1157+
_p("build %s%s: %s %s%s", targetPath, implicitOutputs, rule, table.concat(cfg._objectFiles, " "), implicitDeps)
11161158

11171159
if cfg.kind ~= p.STATICLIB then
11181160
_p(" ldflags = $ldflags_%s", ninja.key(cfg))

modules/ninja/tests/_tests.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ return {
1515
"test_ninja_custom_build.lua",
1616
"test_ninja_custom_rules.lua",
1717
"test_ninja_dependson.lua",
18+
"test_ninja_implib.lua",
1819
"test_ninja_pch.lua",
1920
"test_ninja_perfile_config.lua",
2021
"test_ninja_project.lua",
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
--
2+
-- test_ninja_build_commands.lua
3+
-- Test pre-build, pre-link, and post-build commands for Ninja.
4+
-- Author: Nick Clark
5+
-- Copyright (c) 2025 Jess Perkins and the Premake project
6+
--
7+
8+
local suite = test.declare("ninja_build_commands")
9+
10+
local p = premake
11+
local ninja = p.modules.ninja
12+
local cpp = ninja.cpp
13+
14+
15+
--
16+
-- Setup and teardown
17+
--
18+
19+
local wks, prj
20+
21+
function suite.setup()
22+
p.action.set("ninja")
23+
wks, prj = test.createWorkspace()
24+
end
25+
26+
local function prepare()
27+
local cfg = test.getconfig(prj, "Debug")
28+
return cfg
29+
end
30+
31+
32+
---
33+
-- Pre-build command tests
34+
---
35+
36+
--
37+
-- Check that prebuild commands use cmd /c on Windows
38+
--
39+
40+
function suite.prebuildCommands_usesCmdOnWindows()
41+
system "Windows"
42+
files { "test.cpp" }
43+
prebuildcommands { "echo test" }
44+
45+
local cfg = prepare()
46+
cpp.buildPreBuildEvents(cfg)
47+
48+
test.capture [[
49+
build bin/Debug/MyProject.prebuild: prebuild
50+
prebuildcommands = echo test
51+
]]
52+
end
53+
54+
55+
--
56+
-- Check that prebuild commands don't use cmd /c on Linux
57+
--
58+
59+
function suite.prebuildCommands_noCmdOnLinux()
60+
system "Linux"
61+
files { "test.cpp" }
62+
prebuildcommands { "echo test" }
63+
64+
local cfg = prepare()
65+
cpp.buildPreBuildEvents(cfg)
66+
67+
test.capture [[
68+
build bin/Debug/MyProject.prebuild: prebuild
69+
prebuildcommands = echo test
70+
]]
71+
end
72+
73+
74+
--
75+
-- Check that prebuild with message and commands combines them properly
76+
--
77+
78+
function suite.prebuildMessageAndCommands_onWindows()
79+
system "Windows"
80+
files { "test.cpp" }
81+
prebuildmessage "Running prebuild"
82+
prebuildcommands { "echo test" }
83+
84+
local cfg = prepare()
85+
cpp.buildPreBuildEvents(cfg)
86+
87+
test.capture [[
88+
build bin/Debug/MyProject.prebuild: prebuild
89+
prebuildcommands = echo "Running prebuild" && echo test
90+
]]
91+
end
92+
93+
94+
---
95+
-- Pre-link command tests
96+
---
97+
98+
--
99+
-- Check that prelink commands use cmd /c on Windows
100+
--
101+
102+
function suite.prelinkCommands_usesCmdOnWindows()
103+
system "Windows"
104+
files { "test.cpp" }
105+
prelinkcommands { "echo test" }
106+
107+
local cfg = prepare()
108+
cfg._objectFiles = { "obj/Debug/test.obj" }
109+
cpp.buildPreLinkEvents(cfg, cfg._objectFiles)
110+
111+
test.capture [[
112+
build bin/Debug/MyProject.prelinkevents: prelink obj/Debug/test.obj
113+
prelinkcommands = echo test
114+
]]
115+
end
116+
117+
118+
--
119+
-- Check that prelink commands don't use cmd /c on Linux
120+
--
121+
122+
function suite.prelinkCommands_noCmdOnLinux()
123+
system "Linux"
124+
files { "test.cpp" }
125+
prelinkcommands { "echo test" }
126+
127+
local cfg = prepare()
128+
cfg._objectFiles = { "obj/Debug/test.obj" }
129+
cpp.buildPreLinkEvents(cfg, cfg._objectFiles)
130+
131+
test.capture [[
132+
build bin/Debug/MyProject.prelinkevents: prelink obj/Debug/test.obj
133+
prelinkcommands = echo test
134+
]]
135+
end
136+
137+
138+
---
139+
-- Post-build command tests
140+
---
141+
142+
--
143+
-- Check that postbuild commands use cmd /c on Windows
144+
--
145+
146+
function suite.postbuildCommands_usesCmdOnWindows()
147+
system "Windows"
148+
files { "test.cpp" }
149+
postbuildcommands { "echo test" }
150+
151+
local cfg = prepare()
152+
local targetPath = "bin/Debug/MyProject.exe"
153+
cpp.buildPostBuildEvents(cfg, targetPath)
154+
155+
test.capture [[
156+
build bin/Debug/MyProject.postbuild: postbuild | bin/Debug/MyProject.exe
157+
postbuildcommands = echo test
158+
]]
159+
end
160+
161+
162+
--
163+
-- Check that postbuild commands don't use cmd /c on Linux
164+
--
165+
166+
function suite.postbuildCommands_noCmdOnLinux()
167+
system "Linux"
168+
files { "test.cpp" }
169+
postbuildcommands { "echo test" }
170+
171+
local cfg = prepare()
172+
local targetPath = "bin/Debug/MyProject"
173+
cpp.buildPostBuildEvents(cfg, targetPath)
174+
175+
test.capture [[
176+
build bin/Debug/MyProject.postbuild: postbuild | bin/Debug/MyProject
177+
postbuildcommands = echo test
178+
]]
179+
end
180+
181+
182+
--
183+
-- Check that complex commands with quotes work on Windows
184+
--
185+
186+
function suite.prebuildCommands_withQuotedPaths_onWindows()
187+
system "Windows"
188+
files { "test.cpp" }
189+
prebuildcommands { "{COPYFILE} %[src/file.txt] %[dest/file.txt]" }
190+
191+
local cfg = prepare()
192+
cpp.buildPreBuildEvents(cfg)
193+
194+
-- The command should have quotes around paths
195+
test.string_contains(premake.captured(), 'copy /B /Y')
196+
end

modules/ninja/tests/test_ninja_build_rules.lua

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,12 @@ rule copy
397397
--
398398

399399
function suite.prebuildCommandsRule()
400+
_TARGET_OS = "windows"
400401
local cfg = prepare()
401402
cpp.prebuildcommandsrule(cfg)
402403
test.capture [[
403404
rule prebuild
404-
command = $prebuildcommands
405+
command = cmd /c $prebuildcommands
405406
description = Running pre-build commands
406407
407408
]]
@@ -429,11 +430,12 @@ rule prebuildmessage
429430
--
430431

431432
function suite.prelinkCommandsRule()
433+
_TARGET_OS = "windows"
432434
local cfg = prepare()
433435
cpp.prelinkcommandsrule(cfg)
434436
test.capture [[
435437
rule prelink
436-
command = $prelinkcommands
438+
command = cmd /c $prelinkcommands
437439
description = Running pre-link commands
438440
439441
]]
@@ -461,11 +463,12 @@ rule prelinkmessage
461463
--
462464

463465
function suite.postbuildCommandsRule()
466+
_TARGET_OS = "windows"
464467
local cfg = prepare()
465468
cpp.postbuildcommandsrule(cfg)
466469
test.capture [[
467470
rule postbuild
468-
command = $postbuildcommands
471+
command = cmd /c $postbuildcommands
469472
description = Running post-build commands
470473
471474
]]
@@ -488,6 +491,57 @@ rule postbuildmessage
488491
end
489492

490493

494+
--
495+
-- Check the pre-build commands rule on Linux doesn't use cmd /c.
496+
--
497+
498+
function suite.prebuildCommandsRule_onLinux()
499+
_TARGET_OS = "linux"
500+
local cfg = prepare()
501+
cpp.prebuildcommandsrule(cfg)
502+
test.capture [[
503+
rule prebuild
504+
command = sh -c "$prebuildcommands"
505+
description = Running pre-build commands
506+
507+
]]
508+
end
509+
510+
511+
--
512+
-- Check the pre-link commands rule on Linux doesn't use cmd /c.
513+
--
514+
515+
function suite.prelinkCommandsRule_onLinux()
516+
_TARGET_OS = "linux"
517+
local cfg = prepare()
518+
cpp.prelinkcommandsrule(cfg)
519+
test.capture [[
520+
rule prelink
521+
command = sh -c "$prelinkcommands"
522+
description = Running pre-link commands
523+
524+
]]
525+
end
526+
527+
528+
--
529+
-- Check the post-build commands rule on Linux doesn't use cmd /c.
530+
--
531+
532+
function suite.postbuildCommandsRule_onLinux()
533+
_TARGET_OS = "linux"
534+
local cfg = prepare()
535+
cpp.postbuildcommandsrule(cfg)
536+
test.capture [[
537+
rule postbuild
538+
command = sh -c "$postbuildcommands"
539+
description = Running post-build commands
540+
541+
]]
542+
end
543+
544+
491545
--
492546
-- Check the custom command rule.
493547
--

0 commit comments

Comments
 (0)