From 1db2bf3562948d18b5ae164c1f7485a69a9d0cbd Mon Sep 17 00:00:00 2001 From: Kevin Sellers Date: Thu, 10 Sep 2026 19:05:04 +0000 Subject: [PATCH 1/4] fix: build precompiled headers after generated inputs Object files get a target's dependencies, and its actions and rules outputs, as order-only prerequisites. The .gch built from a prefix header got neither, so a prefix header that reaches a generated header races code generation: make starts the .gch as soon as the prefix header itself exists, and a clean build fails on the missing generated header. --- pylib/gyp/generator/make.py | 21 +++++++++++++++++++++ test/fixtures/pch_prefix.gyp | 23 +++++++++++++++++++++++ test/integration_test.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 test/fixtures/pch_prefix.gyp diff --git a/pylib/gyp/generator/make.py b/pylib/gyp/generator/make.py index 16b6f4e8..9d4fdfdb 100644 --- a/pylib/gyp/generator/make.py +++ b/pylib/gyp/generator/make.py @@ -1477,6 +1477,27 @@ def WriteSources( self.WriteLn(f"{obj}: {gch}") self.WriteLn("# End precompiled header dependencies") + # A prefix header may include generated headers, so the .gch has to + # wait for the same generated inputs the objects wait for. The rules + # above give the objects these as order-only prerequisites; without + # the same rules here the .gch races code generation, and a clean + # build fails on a missing generated header. + gchs = sorted({gch for _, _, gch in pchdeps}) + if deps: + self.WriteMakeRule( + gchs, + deps, + comment="Make sure our dependencies are built before the pch.", + order_only=True, + ) + if extra_outputs: + self.WriteMakeRule( + gchs, + extra_outputs, + comment="Make sure our actions/rules run before the pch.", + order_only=True, + ) + if objs: extra_link_deps.append("$(OBJS)") self.WriteLn( diff --git a/test/fixtures/pch_prefix.gyp b/test/fixtures/pch_prefix.gyp new file mode 100644 index 00000000..09658481 --- /dev/null +++ b/test/fixtures/pch_prefix.gyp @@ -0,0 +1,23 @@ +{ + 'targets': [ + { + 'target_name': 'pch_prefix', + 'type': 'executable', + 'sources': [ 'pch.cc' ], + 'xcode_settings': { + 'GCC_PREFIX_HEADER': 'pch.h', + 'GCC_PRECOMPILE_PREFIX_HEADER': 'YES', + }, + # A prefix header is allowed to include generated headers, so the .gch has + # to wait for this target's actions the same way its objects do. + 'actions': [ + { + 'action_name': 'make_header', + 'inputs': [], + 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/generated.h' ], + 'action': [ 'touch', '<(SHARED_INTERMEDIATE_DIR)/generated.h' ], + }, + ], + }, + ] +} diff --git a/test/integration_test.py b/test/integration_test.py index 8244708a..41da0797 100644 --- a/test/integration_test.py +++ b/test/integration_test.py @@ -13,6 +13,7 @@ fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures") gyp_file = os.path.join(fixture_dir, "integration.gyp") pch_gyp_file = os.path.join(fixture_dir, "pch.gyp") +pch_prefix_gyp_file = os.path.join(fixture_dir, "pch_prefix.gyp") if sys.platform == "win32": sysname = sys.platform @@ -80,6 +81,36 @@ def test_cmake(self) -> None: assert_file(self, "out/Default/CMakeLists.txt", "cmake/CMakeLists.txt") +class TestGypMac(unittest.TestCase): + def setUp(self) -> None: + if sysname != "darwin": + self.skipTest("macOS-only test") + shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) + + def test_make_prefix_header_waits_for_generated_inputs(self) -> None: + rc = gyp.main( + [ + "-f", + "make", + "--depth", + fixture_dir, + "--generator-output", + "out", + pch_prefix_gyp_file, + ] + ) + assert rc == 0 + + with open(os.path.join(fixture_dir, "out/pch_prefix.target.mk")) as in_file: + mk = in_file.read() + + gch = "$(obj).target/$(TARGET)/pch-cc/pch.h.gch" + assert f"$(obj).target/$(TARGET)/pch.o: {gch}" in mk + # A prefix header may include generated headers, so the .gch must carry + # the same order-only prerequisites the objects carry. + self.assertRegex(mk, re.escape(f"{gch}: |") + r".*make_header") + + class TestGypWindows(unittest.TestCase): def setUp(self) -> None: if sys.platform != "win32": From 0acf212f26cd1241870ac6da9a2a995f115a91df Mon Sep 17 00:00:00 2001 From: gengjiawen Date: Mon, 19 Oct 2026 09:40:56 +0800 Subject: [PATCH 2/4] fix: order every gch after generated inputs, in make and ninja Move the make ordering into WritePchTargets, so all four gch rules get the target's dependencies and action/rule outputs as order-only prerequisites, not only those of languages that have objects, and set TOOLSET on them like on the objects. Give the ninja gch builds the objects' predepends too; they raced code generation the same way. The test now builds each gch on its own for a prefix header that includes a header generated by the target's action, and for one that includes a header generated by a dependency. --- pylib/gyp/generator/make.py | 37 +++++++++++-------------------- pylib/gyp/generator/ninja.py | 18 ++++++++++++---- test/fixtures/pch_action.h | 1 + test/fixtures/pch_dep.h | 1 + test/fixtures/pch_prefix.gyp | 42 ++++++++++++++++++++++++++++-------- test/integration_test.py | 33 +++++++++++++++++++++------- 6 files changed, 87 insertions(+), 45 deletions(-) create mode 100644 test/fixtures/pch_action.h create mode 100644 test/fixtures/pch_dep.h diff --git a/pylib/gyp/generator/make.py b/pylib/gyp/generator/make.py index 9d4fdfdb..b61c09d6 100644 --- a/pylib/gyp/generator/make.py +++ b/pylib/gyp/generator/make.py @@ -1477,27 +1477,6 @@ def WriteSources( self.WriteLn(f"{obj}: {gch}") self.WriteLn("# End precompiled header dependencies") - # A prefix header may include generated headers, so the .gch has to - # wait for the same generated inputs the objects wait for. The rules - # above give the objects these as order-only prerequisites; without - # the same rules here the .gch races code generation, and a clean - # build fails on a missing generated header. - gchs = sorted({gch for _, _, gch in pchdeps}) - if deps: - self.WriteMakeRule( - gchs, - deps, - comment="Make sure our dependencies are built before the pch.", - order_only=True, - ) - if extra_outputs: - self.WriteMakeRule( - gchs, - extra_outputs, - comment="Make sure our actions/rules run before the pch.", - order_only=True, - ) - if objs: extra_link_deps.append("$(OBJS)") self.WriteLn( @@ -1542,7 +1521,9 @@ def WriteSources( "$(CFLAGS_OBJCC_$(BUILDTYPE))" ) - self.WritePchTargets(precompiled_header.GetPchBuildCommands()) + self.WritePchTargets( + precompiled_header.GetPchBuildCommands(), deps + extra_outputs + ) # If there are any object files in our input file list, link them into our # output. @@ -1550,8 +1531,13 @@ def WriteSources( self.WriteLn() - def WritePchTargets(self, pch_commands): - """Writes make rules to compile prefix headers.""" + def WritePchTargets(self, pch_commands, order_only_deps): + """Writes make rules to compile prefix headers. + + A prefix header may include generated headers, so like the object files, + the gch files are built after |order_only_deps|: the target's dependencies + and the outputs of its actions and rules. + """ if not pch_commands: return @@ -1568,6 +1554,9 @@ def WritePchTargets(self, pch_commands): "m": "GYP_PCH_OBJCFLAGS", "mm": "GYP_PCH_OBJCXXFLAGS", }[lang] + if order_only_deps: + self.WriteMakeRule([gch], order_only_deps, order_only=True) + self.WriteLn(f"{gch}: TOOLSET := $(TOOLSET)") self.WriteLn( f"{gch}: {var_name} := {lang_flag} " + "$(DEFS_$(BUILDTYPE)) " "$(INCS_$(BUILDTYPE)) " diff --git a/pylib/gyp/generator/ninja.py b/pylib/gyp/generator/ninja.py index 3ceaf470..679ab479 100644 --- a/pylib/gyp/generator/ninja.py +++ b/pylib/gyp/generator/ninja.py @@ -1274,13 +1274,17 @@ def WriteSourcesForArch( ], ) - self.WritePchTargets(ninja_file, pch_commands) + self.WritePchTargets(ninja_file, pch_commands, predepends) ninja_file.newline() return outputs - def WritePchTargets(self, ninja_file, pch_commands): - """Writes ninja rules to compile prefix headers.""" + def WritePchTargets(self, ninja_file, pch_commands, predepends): + """Writes ninja rules to compile prefix headers. + + A prefix header may include generated headers, so like the object files, + the gch files are built after |predepends|. + """ if not pch_commands: return @@ -1299,7 +1303,13 @@ def WritePchTargets(self, ninja_file, pch_commands): "mm": "objcxx", } cmd = map.get(lang) - ninja_file.build(gch, cmd, input, variables=[(var_name, lang_flag)]) + ninja_file.build( + gch, + cmd, + input, + order_only=predepends, + variables=[(var_name, lang_flag)], + ) def WriteLink(self, spec, config_name, config, link_deps, compile_deps): """Write out a link step. Fills out target.binary.""" diff --git a/test/fixtures/pch_action.h b/test/fixtures/pch_action.h new file mode 100644 index 00000000..5e3d629b --- /dev/null +++ b/test/fixtures/pch_action.h @@ -0,0 +1 @@ +#include "action_generated.h" diff --git a/test/fixtures/pch_dep.h b/test/fixtures/pch_dep.h new file mode 100644 index 00000000..7df152ab --- /dev/null +++ b/test/fixtures/pch_dep.h @@ -0,0 +1 @@ +#include "dep_generated.h" diff --git a/test/fixtures/pch_prefix.gyp b/test/fixtures/pch_prefix.gyp index 09658481..58a49b40 100644 --- a/test/fixtures/pch_prefix.gyp +++ b/test/fixtures/pch_prefix.gyp @@ -1,23 +1,47 @@ { 'targets': [ { - 'target_name': 'pch_prefix', - 'type': 'executable', + # The prefix header includes a header generated by the target's action. + 'target_name': 'pch_action', + 'type': 'static_library', 'sources': [ 'pch.cc' ], + 'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ], 'xcode_settings': { - 'GCC_PREFIX_HEADER': 'pch.h', + 'GCC_PREFIX_HEADER': 'pch_action.h', 'GCC_PRECOMPILE_PREFIX_HEADER': 'YES', }, - # A prefix header is allowed to include generated headers, so the .gch has - # to wait for this target's actions the same way its objects do. 'actions': [ { - 'action_name': 'make_header', + 'action_name': 'generate_action_header', 'inputs': [], - 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/generated.h' ], - 'action': [ 'touch', '<(SHARED_INTERMEDIATE_DIR)/generated.h' ], + 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/action_generated.h' ], + 'action': [ 'touch', '<@(_outputs)' ], }, ], }, - ] + { + # The prefix header includes a header generated by a dependency. + 'target_name': 'pch_dep', + 'type': 'static_library', + 'dependencies': [ 'generate_dep_header' ], + 'sources': [ 'pch.cc' ], + 'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ], + 'xcode_settings': { + 'GCC_PREFIX_HEADER': 'pch_dep.h', + 'GCC_PRECOMPILE_PREFIX_HEADER': 'YES', + }, + }, + { + 'target_name': 'generate_dep_header', + 'type': 'none', + 'actions': [ + { + 'action_name': 'generate_dep_header', + 'inputs': [], + 'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/dep_generated.h' ], + 'action': [ 'touch', '<@(_outputs)' ], + }, + ], + }, + ], } diff --git a/test/integration_test.py b/test/integration_test.py index 41da0797..6fb27282 100644 --- a/test/integration_test.py +++ b/test/integration_test.py @@ -5,6 +5,7 @@ import os import re import shutil +import subprocess import sys import unittest @@ -82,12 +83,18 @@ def test_cmake(self) -> None: class TestGypMac(unittest.TestCase): + # The prefix header of each target includes a header generated by its + # action (pch_action) or by its dependency (pch_dep), so even a gch built + # on its own must not start before that header exists. + pch_targets = ("pch_action", "pch_dep") + pch_langs = ("c", "cc", "m", "mm") + def setUp(self) -> None: if sysname != "darwin": self.skipTest("macOS-only test") shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) - def test_make_prefix_header_waits_for_generated_inputs(self) -> None: + def test_make_precompiled_prefix_header(self) -> None: rc = gyp.main( [ "-f", @@ -101,14 +108,24 @@ def test_make_prefix_header_waits_for_generated_inputs(self) -> None: ) assert rc == 0 - with open(os.path.join(fixture_dir, "out/pch_prefix.target.mk")) as in_file: - mk = in_file.read() + for target in self.pch_targets: + for lang in self.pch_langs: + gch = f"out/Default/obj.target/{target}/pch-{lang}/{target}.h.gch" + subprocess.run( + ["make", "-C", os.path.join(fixture_dir, "out"), gch], check=True + ) + + def test_ninja_precompiled_prefix_header(self) -> None: + rc = gyp.main(["-f", "ninja", "--depth", fixture_dir, pch_prefix_gyp_file]) + assert rc == 0 - gch = "$(obj).target/$(TARGET)/pch-cc/pch.h.gch" - assert f"$(obj).target/$(TARGET)/pch.o: {gch}" in mk - # A prefix header may include generated headers, so the .gch must carry - # the same order-only prerequisites the objects carry. - self.assertRegex(mk, re.escape(f"{gch}: |") + r".*make_header") + for target in self.pch_targets: + for lang in self.pch_langs: + gch = f"obj/{target}.{target}.h-{lang}.gch" + subprocess.run( + ["ninja", "-C", os.path.join(fixture_dir, "out/Default"), gch], + check=True, + ) class TestGypWindows(unittest.TestCase): From 576b0744aa2ed12840f91c1fb706d39102c74e85 Mon Sep 17 00:00:00 2001 From: gengjiawen Date: Mon, 19 Oct 2026 09:41:21 +0800 Subject: [PATCH 3/4] fix(make): compile the C prefix header with CFLAGS cmd_pch_c passed $(CXXFLAGS.$(TOOLSET)) while the C objects that include the gch are compiled with $(CFLAGS.$(TOOLSET)), so a C++-only flag such as -std=c++17 in CXXFLAGS broke the C gch, and other flag differences made it incompatible with the objects. --- pylib/gyp/generator/make.py | 2 +- test/integration_test.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pylib/gyp/generator/make.py b/pylib/gyp/generator/make.py index b61c09d6..8272f6a5 100644 --- a/pylib/gyp/generator/make.py +++ b/pylib/gyp/generator/make.py @@ -603,7 +603,7 @@ def CalculateGeneratorInputInfo(params): # Commands for precompiled header files. quiet_cmd_pch_c = CXX($(TOOLSET)) $@ -cmd_pch_c = $(CC.$(TOOLSET)) $(GYP_PCH_CFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $< +cmd_pch_c = $(CC.$(TOOLSET)) $(GYP_PCH_CFLAGS) $(DEPFLAGS) $(CFLAGS.$(TOOLSET)) -c -o $@ $< quiet_cmd_pch_cc = CXX($(TOOLSET)) $@ cmd_pch_cc = $(CC.$(TOOLSET)) $(GYP_PCH_CXXFLAGS) $(DEPFLAGS) $(CXXFLAGS.$(TOOLSET)) -c -o $@ $< quiet_cmd_pch_m = CXX($(TOOLSET)) $@ diff --git a/test/integration_test.py b/test/integration_test.py index 6fb27282..bff1a792 100644 --- a/test/integration_test.py +++ b/test/integration_test.py @@ -108,11 +108,15 @@ def test_make_precompiled_prefix_header(self) -> None: ) assert rc == 0 + # CXXFLAGS that a C compile rejects check the C gch doesn't get them. + env = {**os.environ, "CXXFLAGS": "-std=c++17"} for target in self.pch_targets: for lang in self.pch_langs: gch = f"out/Default/obj.target/{target}/pch-{lang}/{target}.h.gch" subprocess.run( - ["make", "-C", os.path.join(fixture_dir, "out"), gch], check=True + ["make", "-C", os.path.join(fixture_dir, "out"), gch], + env=env, + check=True, ) def test_ninja_precompiled_prefix_header(self) -> None: From f51974cef4dd95d5e13a9a952fad6fee80840d17 Mon Sep 17 00:00:00 2001 From: gengjiawen Date: Mon, 19 Oct 2026 09:43:18 +0800 Subject: [PATCH 4/4] fix: include the prefix header itself in assembly sources With GCC_PRECOMPILE_PREFIX_HEADER, assembly sources got the C objects' -include of the C gch, but nothing builds that gch for them, so a target with .S sources and no .c ones failed with "pch-c/... file not found", and others raced the C gch. There is no compiled header for assembly, so include the prefix header itself, as without precompilation. GCC also rejects a C gch for assembly. The test now builds each output from a clean build, so none passes on a header or gch that an earlier one built. --- pylib/gyp/generator/make.py | 22 +++++++++++++++++++--- pylib/gyp/generator/ninja.py | 6 ++++++ pylib/gyp/xcode_emulation.py | 5 ++++- test/fixtures/pch_asm.S | 2 ++ test/fixtures/pch_prefix.gyp | 4 ++-- test/integration_test.py | 26 +++++++++++++------------- 6 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 test/fixtures/pch_asm.S diff --git a/pylib/gyp/generator/make.py b/pylib/gyp/generator/make.py index 8272f6a5..c47050a5 100644 --- a/pylib/gyp/generator/make.py +++ b/pylib/gyp/generator/make.py @@ -1485,13 +1485,29 @@ def WriteSources( # See "Target-specific Variable Values" in the GNU Make manual.""" ) self.WriteLn("$(OBJS): TOOLSET := $(TOOLSET)") - self.WriteLn( - "$(OBJS): GYP_CFLAGS := " + gyp_cflags = ( "$(DEFS_$(BUILDTYPE)) " "$(INCS_$(BUILDTYPE)) " - "%s " % precompiled_header.GetInclude("c") + "$(CFLAGS_$(BUILDTYPE)) " + "%s " + "$(CFLAGS_$(BUILDTYPE)) " "$(CFLAGS_C_$(BUILDTYPE))" ) + self.WriteLn( + "$(OBJS): GYP_CFLAGS := " + + gyp_cflags % precompiled_header.GetInclude("c") + ) + asm_objs = [ + obj + for source, obj in zip(compilable, objs) + if source.endswith((".s", ".S")) + ] + if asm_objs and precompiled_header.compile_headers: + # Assembly is compiled with GYP_CFLAGS too, but without a gch + # for it, the -include of the C one fails. + self.WriteLn( + "%s: GYP_CFLAGS := " % " ".join(asm_objs) + + gyp_cflags % precompiled_header.GetInclude("asm") + ) self.WriteLn( "$(OBJS): GYP_CXXFLAGS := " "$(DEFS_$(BUILDTYPE)) " diff --git a/pylib/gyp/generator/ninja.py b/pylib/gyp/generator/ninja.py index 679ab479..49ce563e 100644 --- a/pylib/gyp/generator/ninja.py +++ b/pylib/gyp/generator/ninja.py @@ -1253,6 +1253,12 @@ def WriteSourcesForArch( cflags_cc, self.ExpandSpecial, ) + elif ext in ("s", "S") and precompiled_header.compile_headers: + # Assembly gets $cflags_pch_c too, but without a gch for it, the + # -include of the C one fails. + variables = [ + ("cflags_pch_c", precompiled_header.GetInclude("asm", arch)) + ] ninja_file.build( output, command, diff --git a/pylib/gyp/xcode_emulation.py b/pylib/gyp/xcode_emulation.py index d13eaa9a..7ae3c4f9 100644 --- a/pylib/gyp/xcode_emulation.py +++ b/pylib/gyp/xcode_emulation.py @@ -1429,7 +1429,10 @@ def _CompiledHeader(self, lang, arch): return h def GetInclude(self, lang, arch=None): - """Gets the cflags to include the prefix header for language |lang|.""" + """Gets the cflags to include the prefix header for language |lang|. + + There is no compiled header for assembly ('asm'), which includes the + prefix header itself.""" if self.compile_headers and lang in self.compiled_headers: return "-include %s" % self._CompiledHeader(lang, arch) elif self.header: diff --git a/test/fixtures/pch_asm.S b/test/fixtures/pch_asm.S new file mode 100644 index 00000000..84d6417a --- /dev/null +++ b/test/fixtures/pch_asm.S @@ -0,0 +1,2 @@ +// Assembly gets the prefix header too, but has no precompiled header. +.text diff --git a/test/fixtures/pch_prefix.gyp b/test/fixtures/pch_prefix.gyp index 58a49b40..c505559c 100644 --- a/test/fixtures/pch_prefix.gyp +++ b/test/fixtures/pch_prefix.gyp @@ -4,7 +4,7 @@ # The prefix header includes a header generated by the target's action. 'target_name': 'pch_action', 'type': 'static_library', - 'sources': [ 'pch.cc' ], + 'sources': [ 'pch.cc', 'pch_asm.S' ], 'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ], 'xcode_settings': { 'GCC_PREFIX_HEADER': 'pch_action.h', @@ -24,7 +24,7 @@ 'target_name': 'pch_dep', 'type': 'static_library', 'dependencies': [ 'generate_dep_header' ], - 'sources': [ 'pch.cc' ], + 'sources': [ 'pch.cc', 'pch_asm.S' ], 'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ], 'xcode_settings': { 'GCC_PREFIX_HEADER': 'pch_dep.h', diff --git a/test/integration_test.py b/test/integration_test.py index bff1a792..c93c8669 100644 --- a/test/integration_test.py +++ b/test/integration_test.py @@ -84,8 +84,8 @@ def test_cmake(self) -> None: class TestGypMac(unittest.TestCase): # The prefix header of each target includes a header generated by its - # action (pch_action) or by its dependency (pch_dep), so even a gch built - # on its own must not start before that header exists. + # action (pch_action) or by its dependency (pch_dep). Each output is built + # on its own from a clean build, so it fails unless it waits for that header. pch_targets = ("pch_action", "pch_dep") pch_langs = ("c", "cc", "m", "mm") @@ -108,28 +108,28 @@ def test_make_precompiled_prefix_header(self) -> None: ) assert rc == 0 + makefile_dir = os.path.join(fixture_dir, "out") # CXXFLAGS that a C compile rejects check the C gch doesn't get them. env = {**os.environ, "CXXFLAGS": "-std=c++17"} for target in self.pch_targets: - for lang in self.pch_langs: - gch = f"out/Default/obj.target/{target}/pch-{lang}/{target}.h.gch" + obj = f"out/Default/obj.target/{target}" + gchs = [f"{obj}/pch-{lang}/{target}.h.gch" for lang in self.pch_langs] + for output in [*gchs, f"{obj}/pch_asm.o"]: + shutil.rmtree(os.path.join(makefile_dir, "out"), ignore_errors=True) subprocess.run( - ["make", "-C", os.path.join(fixture_dir, "out"), gch], - env=env, - check=True, + ["make", "-C", makefile_dir, output], env=env, check=True ) def test_ninja_precompiled_prefix_header(self) -> None: rc = gyp.main(["-f", "ninja", "--depth", fixture_dir, pch_prefix_gyp_file]) assert rc == 0 + build_dir = os.path.join(fixture_dir, "out", "Default") for target in self.pch_targets: - for lang in self.pch_langs: - gch = f"obj/{target}.{target}.h-{lang}.gch" - subprocess.run( - ["ninja", "-C", os.path.join(fixture_dir, "out/Default"), gch], - check=True, - ) + gchs = [f"obj/{target}.{target}.h-{lang}.gch" for lang in self.pch_langs] + for output in [*gchs, f"obj/{target}.pch_asm.o"]: + subprocess.run(["ninja", "-C", build_dir, "-t", "clean"], check=True) + subprocess.run(["ninja", "-C", build_dir, output], check=True) class TestGypWindows(unittest.TestCase):