Commit 79f73b939d3 for nodejs

commit 79f73b939d3b3e167014a0f2ba575bec3ae23e0e
Author: Node.js GitHub Bot <github-bot@iojs.org>
Date:   Thu Oct 1 11:37:03 2026 -0400

    tools: update gyp-next to 0.22.3

    PR-URL: https://github.com/nodejs/node/pull/66397
    Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
    Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
    Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
    Reviewed-By: Chengzhong Wu <legendecas@gmail.com>

diff --git a/tools/gyp/CHANGELOG.md b/tools/gyp/CHANGELOG.md
index c6686c4784a..e590b6fdb8c 100644
--- a/tools/gyp/CHANGELOG.md
+++ b/tools/gyp/CHANGELOG.md
@@ -1,5 +1,19 @@
 # Changelog

+## [0.22.3](https://github.com/nodejs/gyp-next/compare/v0.22.2...v0.22.3) (2026-09-29)
+
+
+### Bug Fixes
+
+* add Python 3.15 beta to the testing ([#344](https://github.com/nodejs/gyp-next/issues/344)) ([bb6522f](https://github.com/nodejs/gyp-next/commit/bb6522fbacc620da2cc382484d75c75ee35d780c))
+* **ci:** add zizmor security linter for github actions ([#358](https://github.com/nodejs/gyp-next/issues/358)) ([3d4c42b](https://github.com/nodejs/gyp-next/commit/3d4c42b386582df54368c29fcd7c3121737451a2))
+* **ci:** enable Python 3.15 release candidate on Windows ([#357](https://github.com/nodejs/gyp-next/issues/357)) ([e2080d2](https://github.com/nodejs/gyp-next/commit/e2080d25c31a0c1f08178a52802be7fd56848376))
+* **ci:** test on npm@12 ([#359](https://github.com/nodejs/gyp-next/issues/359)) ([1729f6e](https://github.com/nodejs/gyp-next/commit/1729f6e3bf8071d95cda85129cdf4ebab0a877ef))
+* **ci:** update ruff github aactio to v4.0.0 ([#348](https://github.com/nodejs/gyp-next/issues/348)) ([8d213cd](https://github.com/nodejs/gyp-next/commit/8d213cd35cec96e91dc98359ccd247404f71f27b))
+* **cmake:** add a dummy source to libraries without sources ([#362](https://github.com/nodejs/gyp-next/issues/362)) ([391165e](https://github.com/nodejs/gyp-next/commit/391165e76f682a318216b8b5c6ca1de0ef5f8f24))
+* **cmake:** require CMake 3.10 and quote custom command comments ([#361](https://github.com/nodejs/gyp-next/issues/361)) ([b418211](https://github.com/nodejs/gyp-next/commit/b418211c49705c0fb42177b666c140cf96d0149f))
+* write pch cflags as a list instead of a map object ([#355](https://github.com/nodejs/gyp-next/issues/355)) ([fe8188c](https://github.com/nodejs/gyp-next/commit/fe8188c6af0cf53488960b0ec655b1dcc01a7c94))
+
 ## [0.22.2](https://github.com/nodejs/gyp-next/compare/v0.22.1...v0.22.2) (2026-04-22)


diff --git a/tools/gyp/README.md b/tools/gyp/README.md
index 6360a6c3342..9f8b1e6119f 100644
--- a/tools/gyp/README.md
+++ b/tools/gyp/README.md
@@ -11,7 +11,7 @@ When used as a command line utility, __gyp-next__ can also be installed with [pi
 * `uv tool install gyp-next`
 ```
 Installing to a new venv 'gyp-next'
-  installed package gyp-next 0.13.0, installed using Python 3.10.6
+  installed package gyp-next 0.13.0, installed using Python 3.14.7
   These apps are now globally available
     - gyp
 done! ✨ 🌟 ✨
diff --git a/tools/gyp/commitlint.config.mjs b/tools/gyp/commitlint.config.mjs
new file mode 100644
index 00000000000..c7a07938b92
--- /dev/null
+++ b/tools/gyp/commitlint.config.mjs
@@ -0,0 +1,6 @@
+export default {
+  extends: ['@commitlint/config-conventional'],
+  rules: {
+    'body-max-line-length': [0, 'always'],
+  },
+};
diff --git a/tools/gyp/pylib/gyp/generator/cmake.py b/tools/gyp/pylib/gyp/generator/cmake.py
index dc9ea39acb7..1028272a41c 100644
--- a/tools/gyp/pylib/gyp/generator/cmake.py
+++ b/tools/gyp/pylib/gyp/generator/cmake.py
@@ -304,12 +304,9 @@ def WriteActions(target_name, actions, extra_sources, extra_deps, path_to_gyp, o
         output.write(path_to_gyp)
         output.write("\n")

-        output.write("  COMMENT ")
-        if "message" in action:
-            output.write(action["message"])
-        else:
-            output.write(action_target_name)
-        output.write("\n")
+        output.write('  COMMENT "')
+        output.write(CMakeStringEscape(action.get("message", action_target_name)))
+        output.write('"\n')

         output.write("  VERBATIM\n")
         output.write(")\n")
@@ -415,12 +412,9 @@ def WriteRules(target_name, rules, extra_sources, extra_deps, path_to_gyp, outpu
             output.write(path_to_gyp)
             output.write("\n")

-            output.write("  COMMENT ")
-            if "message" in rule:
-                output.write(rule["message"])
-            else:
-                output.write(action_name)
-            output.write("\n")
+            output.write('  COMMENT "')
+            output.write(CMakeStringEscape(rule.get("message", action_name)))
+            output.write('"\n')

             output.write("  VERBATIM\n")
             output.write(")\n")
@@ -537,9 +531,9 @@ def WriteCopies(target_name, copies, extra_deps, path_to_gyp, output):
     output.write(path_to_gyp)
     output.write("\n")

-    output.write("COMMENT Copying for ")
-    output.write(target_name)
-    output.write("\n")
+    output.write('COMMENT "Copying for ')
+    output.write(CMakeStringEscape(target_name))
+    output.write('"\n')

     output.write("VERBATIM\n")
     output.write(")\n")
@@ -779,8 +773,10 @@ def WriteTarget(
         other_sources_name = cmake_target_name + "__other_srcs"
         SetVariableList(output, other_sources_name, other_sources)

-    # CMake gets upset when executable targets provide no sources.
+    # CMake gets upset when executable or library targets provide no sources.
     # http://www.cmake.org/pipermail/cmake/2010-July/038461.html
+    # Libraries without sources are used to aggregate dependencies; build an
+    # empty library for them, as the make generator does.
     dummy_sources_name = None
     has_sources = (
         s_sources_name
@@ -789,7 +785,7 @@ def WriteTarget(
         or linkable_sources_name
         or other_sources_name
     )
-    if target_type == "executable" and not has_sources:
+    if target_type != "none" and not has_sources:
         dummy_sources_name = cmake_target_name + "__dummy_srcs"
         SetVariable(
             output, dummy_sources_name, "${obj}.${TOOLSET}/${TARGET}/genc/dummy.c"
@@ -1160,8 +1156,7 @@ def GenerateOutputForConfig(target_list, target_dicts, data, params, config_to_u
     gyp.common.EnsureDirExists(output_file)

     output = open(output_file, "w")
-    output.write("cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR)\n")
-    output.write("cmake_policy(VERSION 2.8.8)\n")
+    output.write("cmake_minimum_required(VERSION 3.10)\n")

     gyp_file, project_target, _ = gyp.common.ParseQualifiedTarget(target_list[-1])
     output.write("project(")
diff --git a/tools/gyp/pylib/gyp/generator/cmake_test.py b/tools/gyp/pylib/gyp/generator/cmake_test.py
new file mode 100644
index 00000000000..fe45cd6d922
--- /dev/null
+++ b/tools/gyp/pylib/gyp/generator/cmake_test.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+
+"""Unit tests for the cmake.py file."""
+
+import io
+import types
+import unittest
+
+from gyp.generator import cmake
+
+
+class TestCustomCommandComment(unittest.TestCase):
+    def test_ActionMessageIsQuotedAndEscaped(self):
+        output = io.StringIO()
+        action = {
+            "action_name": "gen",
+            "inputs": ["in.txt"],
+            "outputs": ["out.txt"],
+            "action": ["python", "gen.py"],
+            "message": 'Generating "out.txt"; (see #1)',
+        }
+        cmake.WriteActions("tgt", [action], [], [], ".", output)
+        self.assertIn(
+            '  COMMENT "Generating \\"out.txt\\"\\; (see #1)"\n', output.getvalue()
+        )
+
+    def test_ActionWithoutMessageUsesTargetName(self):
+        output = io.StringIO()
+        action = {
+            "action_name": "gen",
+            "inputs": [],
+            "outputs": ["out.txt"],
+            "action": ["python", "gen.py"],
+        }
+        cmake.WriteActions("tgt", [action], [], [], ".", output)
+        self.assertIn('  COMMENT "tgt__gen"\n', output.getvalue())
+
+    def test_RuleMessageKeepsVariableReferences(self):
+        output = io.StringIO()
+        rule = {
+            "rule_name": "compile",
+            "outputs": ["${RULE_INPUT_ROOT}.o"],
+            "action": ["cc", "${RULE_INPUT_PATH}"],
+            "rule_sources": ["foo.c"],
+            "message": "Compiling ${RULE_INPUT_NAME}",
+        }
+        cmake.WriteRules("tgt", [rule], [], [], ".", output)
+        self.assertIn('  COMMENT "Compiling ${RULE_INPUT_NAME}"\n', output.getvalue())
+
+    def test_CopiesMessageIsQuoted(self):
+        output = io.StringIO()
+        copies = [{"files": ["a.txt"], "destination": "out"}]
+        cmake.WriteCopies("tgt", copies, [], ".", output)
+        self.assertIn('COMMENT "Copying for tgt"\n', output.getvalue())
+
+
+class TestTargetWithoutSources(unittest.TestCase):
+    def _WriteTarget(self, target_type, sources=()):
+        qualified_target = "foo.gyp:foo#target"
+        spec = {
+            "target_name": "foo",
+            "type": target_type,
+            "toolset": "target",
+            "sources": list(sources),
+        }
+        output = io.StringIO()
+        cmake.WriteTarget(
+            cmake.CMakeNamer([qualified_target]),
+            qualified_target,
+            {qualified_target: spec},
+            "out/Default",
+            "Default",
+            types.SimpleNamespace(toplevel_dir="."),
+            {},
+            [qualified_target],
+            "linux",
+            output,
+        )
+        return output.getvalue()
+
+    def test_DummySourceForEmptyTargets(self):
+        for target_type, add_target in (
+            ("executable", "add_executable(foo"),
+            ("static_library", "add_library(foo STATIC"),
+            ("shared_library", "add_library(foo SHARED"),
+            ("loadable_module", "add_library(foo MODULE"),
+        ):
+            with self.subTest(target_type=target_type):
+                output = self._WriteTarget(target_type)
+                self.assertIn('  file(WRITE "${foo__dummy_srcs}" "")\n', output)
+                self.assertIn(add_target + " ${foo__dummy_srcs})\n", output)
+
+    def test_NoDummySourceForLibraryWithSources(self):
+        output = self._WriteTarget("static_library", ["foo.c"])
+        self.assertIn("add_library(foo STATIC ${foo__c_srcs})\n", output)
+        self.assertNotIn("dummy", output)
+
+    def test_NoDummySourceForNoneTarget(self):
+        output = self._WriteTarget("none")
+        self.assertIn("add_custom_target(foo SOURCES)\n", output)
+        self.assertNotIn("dummy", output)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tools/gyp/pylib/gyp/msvs_emulation.py b/tools/gyp/pylib/gyp/msvs_emulation.py
index f1c1581981e..84e0b4f327a 100644
--- a/tools/gyp/pylib/gyp/msvs_emulation.py
+++ b/tools/gyp/pylib/gyp/msvs_emulation.py
@@ -1057,13 +1057,18 @@ class PrecompiledHeader:
             pch_output = ["/Yc" + self._PchHeader()]
             if command == "cxx":
                 return (
-                    [("cflags_cc", map(expand_special, cflags_cc + pch_output))],
+                    [
+                        (
+                            "cflags_cc",
+                            [expand_special(x) for x in cflags_cc + pch_output],
+                        )
+                    ],
                     self.output_obj,
                     [],
                 )
             elif command == "cc":
                 return (
-                    [("cflags_c", map(expand_special, cflags_c + pch_output))],
+                    [("cflags_c", [expand_special(x) for x in cflags_c + pch_output])],
                     self.output_obj,
                     [],
                 )
diff --git a/tools/gyp/pyproject.toml b/tools/gyp/pyproject.toml
index 487cb75002d..9ce52a6785e 100644
--- a/tools/gyp/pyproject.toml
+++ b/tools/gyp/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

 [project]
 name = "gyp-next"
-version = "0.22.2"
+version = "0.22.3"
 authors = [
   { name="Node.js contributors", email="ryzokuken@disroot.org" },
 ]
diff --git a/tools/gyp/test/fixtures/expected-darwin/cmake/CMakeLists.txt b/tools/gyp/test/fixtures/expected-darwin/cmake/CMakeLists.txt
index 90b95e75eb5..6721481cc8b 100644
--- a/tools/gyp/test/fixtures/expected-darwin/cmake/CMakeLists.txt
+++ b/tools/gyp/test/fixtures/expected-darwin/cmake/CMakeLists.txt
@@ -1,5 +1,4 @@
-cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR)
-cmake_policy(VERSION 2.8.8)
+cmake_minimum_required(VERSION 3.10)
 project(test)
 set(configuration "Default")
 enable_language(ASM)
diff --git a/tools/gyp/test/fixtures/expected-linux/cmake/CMakeLists.txt b/tools/gyp/test/fixtures/expected-linux/cmake/CMakeLists.txt
index 968642201ac..43261eb1882 100644
--- a/tools/gyp/test/fixtures/expected-linux/cmake/CMakeLists.txt
+++ b/tools/gyp/test/fixtures/expected-linux/cmake/CMakeLists.txt
@@ -1,5 +1,4 @@
-cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR)
-cmake_policy(VERSION 2.8.8)
+cmake_minimum_required(VERSION 3.10)
 project(test)
 set(configuration "Default")
 enable_language(ASM)
diff --git a/tools/gyp/test/fixtures/pch.cc b/tools/gyp/test/fixtures/pch.cc
new file mode 100644
index 00000000000..1d9f38c57d6
--- /dev/null
+++ b/tools/gyp/test/fixtures/pch.cc
@@ -0,0 +1 @@
+#include "pch.h"
diff --git a/tools/gyp/test/fixtures/pch.gyp b/tools/gyp/test/fixtures/pch.gyp
new file mode 100644
index 00000000000..628c94101dc
--- /dev/null
+++ b/tools/gyp/test/fixtures/pch.gyp
@@ -0,0 +1,13 @@
+{
+  'targets': [
+    {
+      'target_name': 'pch',
+      'type': 'executable',
+      'sources': [
+        'pch.cc',
+      ],
+      'msvs_precompiled_header': 'pch.h',
+      'msvs_precompiled_source': 'pch.cc',
+    },
+  ]
+}
diff --git a/tools/gyp/test/fixtures/pch.h b/tools/gyp/test/fixtures/pch.h
new file mode 100644
index 00000000000..6f70f09beec
--- /dev/null
+++ b/tools/gyp/test/fixtures/pch.h
@@ -0,0 +1 @@
+#pragma once
diff --git a/tools/gyp/test/integration_test.py b/tools/gyp/test/integration_test.py
index 26d78763078..8244708a9bd 100644
--- a/tools/gyp/test/integration_test.py
+++ b/tools/gyp/test/integration_test.py
@@ -11,7 +11,8 @@ import unittest
 import gyp

 fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures")
-gyp_file = os.path.join(os.path.dirname(__file__), "fixtures/integration.gyp")
+gyp_file = os.path.join(fixture_dir, "integration.gyp")
+pch_gyp_file = os.path.join(fixture_dir, "pch.gyp")

 if sys.platform == "win32":
     sysname = sys.platform
@@ -91,3 +92,16 @@ class TestGypWindows(unittest.TestCase):

         assert_file(self, "test.vcproj", "msvs/test.vcproj")
         assert_file(self, "integration.sln", "msvs/integration.sln")
+
+    def test_ninja_precompiled_header(self) -> None:
+        try:
+            rc = gyp.main(["-f", "ninja", "--depth", fixture_dir, pch_gyp_file])
+        except ValueError as exc:
+            self.skipTest(str(exc))
+        assert rc == 0
+
+        with open(os.path.join(fixture_dir, "out/Default/obj/pch.ninja")) as in_file:
+            ninja_file = in_file.read()
+
+        assert "map object at" not in ninja_file
+        assert "/Ycpch.h" in ninja_file