Problem
A Windows GUI application needs its PE image marked with the WINDOWS subsystem. Otherwise the loader allocates a console window when the program is launched from Explorer or from an installer shortcut, and closing that console kills the application.
On the MSVC ABI this takes two linker options that only work as a pair:
/subsystem:windows alone makes the linker default the entry to WinMainCRTStartup, so a portable int main() fails with LNK2019: unresolved external symbol WinMain.
/entry:mainCRTStartup keeps CRT initialization and calls main(). Writing /entry:main instead links, but it skips CRT initialization, so static constructors and stdio setup never run.
MinGW spells the same selection -mwindows.
mcpp has no scope where this selection belongs. It is a property of one executable, but every place that can carry a link option today is package-wide:
| Where |
Why it does not fit |
[build] ldflags / [target.windows.build] ldflags |
Lands in the global $ldflags of build.ninja, so every test binary from mcpp test also becomes a GUI-subsystem program with no console output |
mcpp:link-flag= from build.mcpp |
Documented to reach the consumer, exactly like [build] ldflags (docs/30-build-mcpp.md) |
[targets.<name>] |
Accepts defines / cxxflags / cflags / required_features (plus kind / main / soname / exports); the flags apply to the entry TU only, and there is no link-side key |
The subsystem also cannot be added by a shared build-rule package. A rule package's configure() is used by component libraries too, and anything it emits through link-flag reaches their consumers.
What projects do today
HuxerUI puts linker directives into every application entry source (HuxerUI/HuxerUI@445488a):
#if defined(_WIN32) && defined(_MSC_VER)
// Keep GUI linking local to this entry; mcpp package link flags also reach tests and consumers.
#pragma comment(linker, "/subsystem:windows")
#pragma comment(linker, "/entry:mainCRTStartup")
#endif
int main() { return huxerui::RunApplication(); }
This is the only leak-free spelling available, because the directive lives in the one object file that only the application links. Its costs:
- It only works on the MSVC ABI. GNU
ld ignores #pragma comment(linker), so a windows-gnu build of the same application still opens a console, and nothing in the source can fix that.
- It is repeated in every entry source: 2 application templates and 3 examples in HuxerUI, plus every application generated from those templates.
- The build tool cannot see it. mcpp has no way to know the artifact is a GUI program (for example, when
mcpp run launches it and the terminal shows no output).
#365's reporter used the package-wide form, which has the test and consumer leak described above:
[target.'cfg(windows)'.build]
ldflags = ["-Wl,-subsystem:windows", "-Wl,-entry:mainCRTStartup", ...]
The design note for #365 already set this aside for later: .agents/docs/2026-08-07-windows-resources-and-version-identity-design.md §A6 says subsystem / entry belong to the same "Windows GUI application" story and deserve separate triage. [resources] has landed, and this is the missing half.
Proposal
Add a target-scoped, dialect-neutral statement on kind = "bin" targets, in the style of exports and [resources] (one statement, rendered per target):
[targets.myapp]
kind = "bin"
main = "src/main.cpp"
subsystem = "windows" # "console" (default) | "windows"; name open to discussion
| Target |
Rendering |
| PE, MSVC ABI |
/subsystem:windows /entry:mainCRTStartup (the -Wl, form under the GNU driver) |
| PE, MinGW |
-mwindows |
| ELF / Mach-O |
Inapplicable: no diagnostic, byte-identical build (same policy as [resources]) |
Details worth settling:
- Entry function. The default should keep a portable
main(). Programs that define WinMain / wWinMain / wmain need the matching CRT startup (WinMainCRTStartup, wWinMainCRTStartup, wmainCRTStartup), either through an explicit key such as entry = "wWinMain" or through a spelled-out rule.
- Scope. The option applies to that target's
LinkUnit::Binary only. It must never reach TestBinary link units, other targets of the package, or consumers.
- Implementation surface. The engine already carries per-link-edge flags.
LinkUnit::linkFlags (src/build/plan.cppm) is rendered into $unit_ldflags per edge (src/build/ninja_backend.cppm), so this looks like manifest parsing and validation plus appending to the matching link unit, with no new plumbing.
Optional follow-up: a build-program directive naming a target, such as mcpp::target_subsystem("myapp", "windows"). A framework's rule package already knows the application's bin target (HuxerUI's installer rule receives it), so it could select the subsystem without the application writing anything. Because the directive names a target of the package being built, it would not leak the way link-flag does. This is the mcpp equivalent of CMake's WIN32_EXECUTABLE being set by a framework's add_app helper.
A generic [targets.<name>] ldflags would also close the leak, but it keeps the linker-specific spelling, needs a cfg per ABI, and leaves the subsystem/entry pairing to every user. It could be added as an escape hatch, but it does not replace the neutral key.
Prior art
- CMake:
add_executable(app WIN32 ...) / the WIN32_EXECUTABLE target property
- Meson:
executable(..., win_subsystem: 'windows')
- Rust:
#![windows_subsystem = "windows"]; rustc renders /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup on MSVC
Acceptance
- A Windows
kind = "bin" target with subsystem = "windows" and int main() links on both the MSVC ABI and MinGW, and its PE optional header reads IMAGE_SUBSYSTEM_WINDOWS_GUI (checked by bytes).
- In the same package, test binaries built by
mcpp test and any other bin target keep IMAGE_SUBSYSTEM_WINDOWS_CUI, and a consumer of a library in the graph is unaffected.
- Static constructors in the GUI target run before
main(), which shows CRT initialization is kept.
- The same manifest builds on Linux and macOS with zero warnings and a byte-identical artifact.
- Changing the key triggers a relink.
Environment
- mcpp 2026.9.11.2 (docs and sources read at c58d61e)
- Reporter's project: HuxerUI (clang targeting
x86_64-pc-windows-msvc, mcpp's default Windows toolchain)
Problem
A Windows GUI application needs its PE image marked with the
WINDOWSsubsystem. Otherwise the loader allocates a console window when the program is launched from Explorer or from an installer shortcut, and closing that console kills the application.On the MSVC ABI this takes two linker options that only work as a pair:
/subsystem:windowsalone makes the linker default the entry toWinMainCRTStartup, so a portableint main()fails withLNK2019: unresolved external symbol WinMain./entry:mainCRTStartupkeeps CRT initialization and callsmain(). Writing/entry:maininstead links, but it skips CRT initialization, so static constructors and stdio setup never run.MinGW spells the same selection
-mwindows.mcpp has no scope where this selection belongs. It is a property of one executable, but every place that can carry a link option today is package-wide:
[build] ldflags/[target.windows.build] ldflags$ldflagsofbuild.ninja, so every test binary frommcpp testalso becomes a GUI-subsystem program with no console outputmcpp:link-flag=frombuild.mcpp[build] ldflags(docs/30-build-mcpp.md)[targets.<name>]defines/cxxflags/cflags/required_features(pluskind/main/soname/exports); the flags apply to the entry TU only, and there is no link-side keyThe subsystem also cannot be added by a shared build-rule package. A rule package's
configure()is used by component libraries too, and anything it emits throughlink-flagreaches their consumers.What projects do today
HuxerUI puts linker directives into every application entry source (HuxerUI/HuxerUI@445488a):
This is the only leak-free spelling available, because the directive lives in the one object file that only the application links. Its costs:
ldignores#pragma comment(linker), so awindows-gnubuild of the same application still opens a console, and nothing in the source can fix that.mcpp runlaunches it and the terminal shows no output).#365's reporter used the package-wide form, which has the test and consumer leak described above:
The design note for #365 already set this aside for later:
.agents/docs/2026-08-07-windows-resources-and-version-identity-design.md§A6 sayssubsystem/entrybelong to the same "Windows GUI application" story and deserve separate triage.[resources]has landed, and this is the missing half.Proposal
Add a target-scoped, dialect-neutral statement on
kind = "bin"targets, in the style ofexportsand[resources](one statement, rendered per target):/subsystem:windows /entry:mainCRTStartup(the-Wl,form under the GNU driver)-mwindows[resources])Details worth settling:
main(). Programs that defineWinMain/wWinMain/wmainneed the matching CRT startup (WinMainCRTStartup,wWinMainCRTStartup,wmainCRTStartup), either through an explicit key such asentry = "wWinMain"or through a spelled-out rule.LinkUnit::Binaryonly. It must never reachTestBinarylink units, other targets of the package, or consumers.LinkUnit::linkFlags(src/build/plan.cppm) is rendered into$unit_ldflagsper edge (src/build/ninja_backend.cppm), so this looks like manifest parsing and validation plus appending to the matching link unit, with no new plumbing.Optional follow-up: a build-program directive naming a target, such as
mcpp::target_subsystem("myapp", "windows"). A framework's rule package already knows the application's bin target (HuxerUI's installer rule receives it), so it could select the subsystem without the application writing anything. Because the directive names a target of the package being built, it would not leak the waylink-flagdoes. This is the mcpp equivalent of CMake'sWIN32_EXECUTABLEbeing set by a framework'sadd_apphelper.A generic
[targets.<name>] ldflagswould also close the leak, but it keeps the linker-specific spelling, needs acfgper ABI, and leaves the subsystem/entry pairing to every user. It could be added as an escape hatch, but it does not replace the neutral key.Prior art
add_executable(app WIN32 ...)/ theWIN32_EXECUTABLEtarget propertyexecutable(..., win_subsystem: 'windows')#![windows_subsystem = "windows"]; rustc renders/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartupon MSVCAcceptance
kind = "bin"target withsubsystem = "windows"andint main()links on both the MSVC ABI and MinGW, and its PE optional header readsIMAGE_SUBSYSTEM_WINDOWS_GUI(checked by bytes).mcpp testand any otherbintarget keepIMAGE_SUBSYSTEM_WINDOWS_CUI, and a consumer of a library in the graph is unaffected.main(), which shows CRT initialization is kept.Environment
x86_64-pc-windows-msvc, mcpp's default Windows toolchain)