Skip to content

gyp-to-cmake: ldflags and cflags produce broken builds for valid binding.gyp input #481

Open

Description

@kraenhansen

ldflags support landed in #476, and it shares its path with cflags. Both fields produce broken builds for valid binding.gyp input, and cflags has every problem below except where noted.

Line references are to transformer.ts at ff237c0. The flags go through .flatMap(mapExpansion).map(escapeSpaces) (lines 122-123) and are then joined unquoted into target_compile_options / target_link_options (lines 225-233).

Reproduced by generating a CMakeLists from bindingGypToCmakeLists and building it with CMake 4.4.3 and Ninja on macOS arm64. The ldflags Android result used NDK 27.1.

Problems

1. Flags are emitted on platforms where gyp ignores them

On mac, gyp's make.py takes both flag sets from xcode_settings (GetCflags at line 1416, GetLdflags at line 1717). It only reads the target-level cflags and ldflags on other platforms. We emit them everywhere, so flags written for Linux reach Apple builds.

Field Input Result
ldflags -Wl,--as-needed macOS: ld: unknown options: --as-needed
ldflags -Wl,-dead_strip Android: ld.lld: error: unknown argument '-dead_strip'
cflags -msse2, -mfpu=neon, -fno-gnu-unique Rejected by Apple clang on arm64 (checked by compiling directly, not through the generator)

2. CMake de-duplicates options, which corrupts multi-token flags

Field Input Result
cflags ["-include", "a.h", "-include", "b.h"] no such file or directory: 'b.h'
cflags ["-Xclang", "-fno-pch-timestamp", "-Xclang", "-fno-validate-pch"] Second -Xclang dropped: unknown argument '-fno-validate-pch'
cflags <!@echo -include a.h -include b.h Same as the first row
ldflags ["-Xlinker", "-rpath", "-Xlinker", "/a/b"] Becomes -Xlinker -rpath /a/b
ldflags <!@echo -framework CoreFoundation -framework Foundation Becomes -framework CoreFoundation Foundation

3. escapeSpaces turns a multi-word flag into one argument

gyp's QuoteIfNecessary only quotes flags containing ", so a flag with spaces is split by the shell. We keep it as one argument.

Field Input Result
cflags -include a.h fatal error: ' a.h' file not found
ldflags -framework CoreFoundation unknown argument: '-framework CoreFoundation'

The existing tests assert this escaped output as correct, including -DNAME=value\ with\ space for cflags.

4. Flags aren't quoted as CMake arguments

Field Input Result
cflags -DX=a#b CMake configure error
cflags -DX=a;b Split into two arguments, build fails
cflags -DX=(1) Split at the parentheses, build fails
cflags -IC:\inc CMake configure error, because cflags and ldflags skip transformPath
ldflags -Wl,-rpath,"/a b" Builds, but the rpath contains literal quotes

-DGREETING="hi" in cflags builds correctly.

5. gyp's $$ escape isn't unescaped

-Wl,-rpath,'$$ORIGIN' builds, but the rpath is literally '$$ORIGIN'. There's no build error, so it only shows up at runtime. This is specific to ldflags in practice.

6. gyp expansions inside a string aren't handled

isCmdExpansion (line 22) only checks the start of the value, and <(var) isn't handled at all. Both -I<!(pwd)/inc and -I<(module_root_dir)/inc pass straight through and fail with no such file or directory: '('. The same applies to -L in ldflags.

Suggested fix

  • Wrap the emitted cflags and ldflags lines in if(NOT APPLE) to match gyp. This has not been tested. Apple builds then get none of these flags until xcode_settings is read.

  • Emit all flags for a target as one quoted SHELL: argument, which fixes 2 and 3 and most of 4. This form built correctly on macOS for repeated -include, -framework and -Xlinker groups, for # and (), and for ; when escaped as \;:

    target_compile_options(foo PRIVATE "SHELL:-include a.h -include b.h")
    target_link_options(foo PRIVATE "SHELL:-framework CoreFoundation -framework Foundation")

    Those cases were hand-written and shell-quoted, so the generator still needs an escaping function that handles shell and CMake quoting. It also needs a decision for a flag that really is one argument containing spaces, since word-splitting matches gyp but changes the current -DNAME=value with space behaviour.

  • Add tests that run cmake configure on the generated CMakeLists and assert on the flags CMake resolves, in place of comparing strings. No build or addon compile is needed: CMake applies de-duplication and argument parsing at configure time. Generating the example from problem 2 and only configuring gives these link fragments from the File API:

    ["-framework", "CoreFoundation", "Foundation"]
    • Read the result through cmake-file-api (compileCommandFragments and link.commandFragments), added as a dev dependency. compile_commands.json also shows compile flags, but not link flags.
    • This should catch problems 2 to 6 by asserting on the resolved flags and on configure failures. Problem 1 needs an assertion per host, so that the flags are absent on macos-latest and present on ubuntu-latest.
    • project() still detects a compiler during configure, and unit-tests runs on Ubuntu, Windows and macOS, so check that CMake is available on all three. Assertions shouldn't depend on how each toolchain groups fragments.

Not covered

Activity

  1. added a commit that references this issue on Oct 31, 2026
    dba124c
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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions