diff --git a/tools/gyp/.gitignore b/tools/gyp/.gitignore index 5f71dbd435dbf4..dcf3cb43ea7ab5 100644 --- a/tools/gyp/.gitignore +++ b/tools/gyp/.gitignore @@ -144,3 +144,7 @@ static test/fixtures/out *.actual +*.sln +*.vcproj +!test/fixtures/expected-win32/**/*.sln +!test/fixtures/expected-win32/**/*.vcproj diff --git a/tools/gyp/CHANGELOG.md b/tools/gyp/CHANGELOG.md index 9a1468544752c1..89a41685244770 100644 --- a/tools/gyp/CHANGELOG.md +++ b/tools/gyp/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.22.0](https://github.com/nodejs/gyp-next/compare/v0.21.1...v0.22.0) (2026-04-02) + + +### Features + +* Windows ARM64 target architecture support ([#331](https://github.com/nodejs/gyp-next/issues/331)) ([652a346](https://github.com/nodejs/gyp-next/commit/652a346bbd3b077a4b08a3c37d48100ce200758a)) + + +### Bug Fixes + +* drop deprecated Python module pkg_resources ([#333](https://github.com/nodejs/gyp-next/issues/333)) ([5b180d5](https://github.com/nodejs/gyp-next/commit/5b180d52d03aff062bdea1ad0209b82271c7eb4a)) + ## [0.21.1](https://github.com/nodejs/gyp-next/compare/v0.21.0...v0.21.1) (2026-01-24) diff --git a/tools/gyp/pylib/gyp/MSVSVersion.py b/tools/gyp/pylib/gyp/MSVSVersion.py index 2d8e4ceab9a94c..02e6e7ed9209cf 100644 --- a/tools/gyp/pylib/gyp/MSVSVersion.py +++ b/tools/gyp/pylib/gyp/MSVSVersion.py @@ -87,7 +87,7 @@ def DefaultToolset(self): def _SetupScriptInternal(self, target_arch): """Returns a command (with arguments) to be used to set up the environment.""" - assert target_arch in ("x86", "x64"), "target_arch not supported" + assert target_arch in ("x86", "x64", "arm64"), "target_arch not supported" # If WindowsSDKDir is set and SetEnv.Cmd exists then we are using the # depot_tools build tools and should run SetEnv.Cmd to set up the # environment. The check for WindowsSDKDir alone is not sufficient because @@ -109,8 +109,16 @@ def _SetupScriptInternal(self, target_arch): ) # Always use a native executable, cross-compiling if necessary. - host_arch = "amd64" if is_host_arch_x64 else "x86" - msvc_target_arch = "amd64" if target_arch == "x64" else "x86" + host_arch = ( + "amd64" + if is_host_arch_x64 + else ( + "arm64" + if os.environ.get("PROCESSOR_ARCHITECTURE") == "ARM64" + else "x86" + ) + ) + msvc_target_arch = {"x64": "amd64"}.get(target_arch, target_arch) arg = host_arch if host_arch != msvc_target_arch: arg += "_" + msvc_target_arch diff --git a/tools/gyp/pylib/gyp/__init__.py b/tools/gyp/pylib/gyp/__init__.py index 3a70cf076c8b47..c0a2637e94770f 100755 --- a/tools/gyp/pylib/gyp/__init__.py +++ b/tools/gyp/pylib/gyp/__init__.py @@ -13,6 +13,7 @@ import shlex import sys import traceback +from importlib.metadata import version import gyp.input from gyp.common import GypError @@ -491,9 +492,7 @@ def gyp_main(args): options, build_files_arg = parser.parse_args(args) if options.version: - import pkg_resources # noqa: PLC0415 - - print(f"v{pkg_resources.get_distribution('gyp-next').version}") + print(f"v{version('gyp-next')}") return 0 build_files = build_files_arg diff --git a/tools/gyp/pylib/gyp/generator/ninja.py b/tools/gyp/pylib/gyp/generator/ninja.py index 4eac6cdb2707d7..3ceaf470cec203 100644 --- a/tools/gyp/pylib/gyp/generator/ninja.py +++ b/tools/gyp/pylib/gyp/generator/ninja.py @@ -246,7 +246,7 @@ def __init__( if flavor == "win": # See docstring of msvs_emulation.GenerateEnvironmentFiles(). self.win_env = {} - for arch in ("x86", "x64"): + for arch in ("x86", "x64", "arm64"): self.win_env[arch] = "environment." + arch # Relative path from build output dir to base dir. @@ -2339,6 +2339,7 @@ def GenerateOutputForConfig(target_list, target_dicts, data, params, config_name master_ninja.variable("rc", "rc.exe") master_ninja.variable("ml_x86", "ml.exe") master_ninja.variable("ml_x64", "ml64.exe") + master_ninja.variable("ml_arm64", "armasm64.exe") master_ninja.variable("mt", "mt.exe") else: master_ninja.variable("ld", CommandWithWrapper("LINK", wrappers, ld)) diff --git a/tools/gyp/pylib/gyp/generator/ninja_test.py b/tools/gyp/pylib/gyp/generator/ninja_test.py index 616bc7aaf015a2..5b688ae7d1207b 100644 --- a/tools/gyp/pylib/gyp/generator/ninja_test.py +++ b/tools/gyp/pylib/gyp/generator/ninja_test.py @@ -11,26 +11,36 @@ from pathlib import Path from gyp.generator import ninja +from gyp.MSVSVersion import SelectVisualStudioVersion + + +def _has_visual_studio(): + """Check if Visual Studio can be detected by gyp's registry-based detection.""" + if not sys.platform.startswith("win"): + return False + try: + SelectVisualStudioVersion("auto", allow_fallback=False) + return True + except ValueError: + return False class TestPrefixesAndSuffixes(unittest.TestCase): + @unittest.skipUnless( + _has_visual_studio(), + "requires Windows with a Visual Studio installation detected via the registry", + ) def test_BinaryNamesWindows(self): - # These cannot run on non-Windows as they require a VS installation to - # correctly handle variable expansion. - if sys.platform.startswith("win"): - writer = ninja.NinjaWriter( - "foo", "wee", ".", ".", "build.ninja", ".", "build.ninja", "win" - ) - spec = {"target_name": "wee"} - self.assertTrue( - writer.ComputeOutputFileName(spec, "executable").endswith(".exe") - ) - self.assertTrue( - writer.ComputeOutputFileName(spec, "shared_library").endswith(".dll") - ) - self.assertTrue( - writer.ComputeOutputFileName(spec, "static_library").endswith(".lib") - ) + writer = ninja.NinjaWriter( + "foo", "wee", ".", ".", "build.ninja", ".", "build.ninja", "win" + ) + spec = {"target_name": "wee"} + for key, ext in { + "executable": ".exe", + "shared_library": ".dll", + "static_library": ".lib", + }: + self.assertTrue(writer.ComputeOutputFileName(spec, key).endswith(ext)) def test_BinaryNamesLinux(self): writer = ninja.NinjaWriter( diff --git a/tools/gyp/pylib/gyp/mac_tool.py b/tools/gyp/pylib/gyp/mac_tool.py index 3710178e110ae5..4c38f0586c4c28 100755 --- a/tools/gyp/pylib/gyp/mac_tool.py +++ b/tools/gyp/pylib/gyp/mac_tool.py @@ -545,7 +545,7 @@ def _FindProvisioningProfile(self, profile, bundle_identifier): # If the user has multiple provisioning profiles installed that can be # used for ${bundle_identifier}, pick the most specific one (ie. the # provisioning profile whose pattern is the longest). - selected_key = max(valid_provisioning_profiles, key=lambda v: len(v)) + selected_key = max(valid_provisioning_profiles, key=len) return valid_provisioning_profiles[selected_key] def _LoadProvisioningProfile(self, profile_path): diff --git a/tools/gyp/pylib/gyp/msvs_emulation.py b/tools/gyp/pylib/gyp/msvs_emulation.py index 7c461a8fdf72d8..f1c1581981e3dd 100644 --- a/tools/gyp/pylib/gyp/msvs_emulation.py +++ b/tools/gyp/pylib/gyp/msvs_emulation.py @@ -1174,7 +1174,7 @@ def GenerateEnvironmentFiles( meet your requirement (e.g. for custom toolchains), you can pass "-G ninja_use_custom_environment_files" to the gyp to suppress file generation and use custom environment files prepared by yourself.""" - archs = ("x86", "x64") + archs = ("x86", "x64", "arm64") if generator_flags.get("ninja_use_custom_environment_files", 0): cl_paths = {} for arch in archs: diff --git a/tools/gyp/pyproject.toml b/tools/gyp/pyproject.toml index fa30c8cf96da6f..591ed4f5b0b6cc 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.21.1" +version = "0.22.0" authors = [ { name="Node.js contributors", email="ryzokuken@disroot.org" }, ] diff --git a/tools/gyp/test/fixtures/expected-win32/msvs/integration.sln b/tools/gyp/test/fixtures/expected-win32/msvs/integration.sln new file mode 100644 index 00000000000000..276e0693118e33 --- /dev/null +++ b/tools/gyp/test/fixtures/expected-win32/msvs/integration.sln @@ -0,0 +1,16 @@ +Microsoft Visual Studio Solution File, Format Version 9.00 +# Visual Studio 2005 +Project("{*}") = "test", "test.vcproj", "{*}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Default|Win32 = Default|Win32 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {*}.Default|Win32.ActiveCfg = Default|Win32 + {*}.Default|Win32.Build.0 = Default|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/tools/gyp/test/fixtures/expected-win32/msvs/test.vcproj b/tools/gyp/test/fixtures/expected-win32/msvs/test.vcproj new file mode 100644 index 00000000000000..981a106ce4770d --- /dev/null +++ b/tools/gyp/test/fixtures/expected-win32/msvs/test.vcproj @@ -0,0 +1 @@ + diff --git a/tools/gyp/test/integration_test.py b/tools/gyp/test/integration_test.py index b45a62ff6c74c9..26d78763078549 100644 --- a/tools/gyp/test/integration_test.py +++ b/tools/gyp/test/integration_test.py @@ -5,6 +5,7 @@ import os import re import shutil +import sys import unittest import gyp @@ -12,40 +13,48 @@ fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures") gyp_file = os.path.join(os.path.dirname(__file__), "fixtures/integration.gyp") -supported_sysnames = {"darwin", "linux"} -sysname = os.uname().sysname.lower() +if sys.platform == "win32": + sysname = sys.platform +else: + sysname = os.uname().sysname.lower() expected_dir = os.path.join(fixture_dir, f"expected-{sysname}") -class TestGyp(unittest.TestCase): - def setUp(self) -> None: - if sysname not in supported_sysnames: - self.skipTest(f"Unsupported system: {sysname}") - shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) +def assert_file(test, actual, expected) -> None: + actual_filepath = os.path.join(fixture_dir, actual) + expected_filepath = os.path.join(expected_dir, expected) + + with open(expected_filepath) as in_file: + in_bytes = in_file.read() + in_bytes = in_bytes.strip() + expected_bytes = re.escape(in_bytes) + expected_bytes = expected_bytes.replace("\\*", ".*") + expected_re = re.compile(expected_bytes) - def assert_file(self, actual, expected) -> None: - actual_filepath = os.path.join(fixture_dir, actual) - expected_filepath = os.path.join(expected_dir, expected) + with open(actual_filepath) as in_file: + actual_bytes = in_file.read() + actual_bytes = actual_bytes.strip() - with open(expected_filepath) as in_file: - expected_bytes = re.escape(in_file.read()) - expected_bytes = expected_bytes.replace("\\*", ".*") - expected_re = re.compile(expected_bytes) + try: + test.assertRegex(actual_bytes, expected_re) + except Exception: + shutil.copyfile(actual_filepath, f"{expected_filepath}.actual") + raise - with open(actual_filepath) as in_file: - actual_bytes = in_file.read() - try: - self.assertRegex(actual_bytes, expected_re) - except Exception: - shutil.copyfile(actual_filepath, f"{expected_filepath}.actual") - raise +class TestGypUnix(unittest.TestCase): + supported_sysnames = {"darwin", "linux"} + + def setUp(self) -> None: + if sysname not in TestGypUnix.supported_sysnames: + self.skipTest(f"Unsupported system: {sysname}") + shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) def test_ninja(self) -> None: rc = gyp.main(["-f", "ninja", "--depth", fixture_dir, gyp_file]) assert rc == 0 - self.assert_file("out/Default/obj/test.ninja", "ninja/test.ninja") + assert_file(self, "out/Default/obj/test.ninja", "ninja/test.ninja") def test_make(self) -> None: rc = gyp.main( @@ -61,10 +70,24 @@ def test_make(self) -> None: ) assert rc == 0 - self.assert_file("out/test.target.mk", "make/test.target.mk") + assert_file(self, "out/test.target.mk", "make/test.target.mk") def test_cmake(self) -> None: rc = gyp.main(["-f", "cmake", "--depth", fixture_dir, gyp_file]) assert rc == 0 - self.assert_file("out/Default/CMakeLists.txt", "cmake/CMakeLists.txt") + assert_file(self, "out/Default/CMakeLists.txt", "cmake/CMakeLists.txt") + + +class TestGypWindows(unittest.TestCase): + def setUp(self) -> None: + if sys.platform != "win32": + self.skipTest("Windows-only test") + shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) + + def test_msvs(self) -> None: + rc = gyp.main(["-f", "msvs", "--depth", fixture_dir, gyp_file]) + assert rc == 0 + + assert_file(self, "test.vcproj", "msvs/test.vcproj") + assert_file(self, "integration.sln", "msvs/integration.sln")