Skip to content

Windows GUI subsystem has no target-scoped declaration: package ldflags reach tests and consumers, so apps fall back to #pragma comment(linker) #618

Description

@Sunrisepeak

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

  1. 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).
  2. 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.
  3. Static constructors in the GUI target run before main(), which shows CRT initialization is kept.
  4. The same manifest builds on Linux and macOS with zero warnings and a byte-identical artifact.
  5. 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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions