diff --git a/tools/gyp/.flake8 b/tools/gyp/.flake8 deleted file mode 100644 index ea0c7680ef87b2..00000000000000 --- a/tools/gyp/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-complexity = 101 -max-line-length = 88 -extend-ignore = E203 # whitespace before ':' to agree with psf/black diff --git a/tools/gyp/.github/workflows/Python_tests.yml b/tools/gyp/.github/workflows/Python_tests.yml index 1cfa42f563ce5f..049d5fe50c455c 100644 --- a/tools/gyp/.github/workflows/Python_tests.yml +++ b/tools/gyp/.github/workflows/Python_tests.yml @@ -2,29 +2,36 @@ # TODO: Enable pytest --doctest-modules name: Python_tests -on: [push, pull_request] +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: jobs: Python_tests: runs-on: ${{ matrix.os }} strategy: fail-fast: false - max-parallel: 8 + max-parallel: 5 matrix: os: [macos-latest, ubuntu-latest] # , windows-latest] - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements_dev.txt - - name: Lint with flake8 - run: flake8 . --ignore=E203,W503 --max-complexity=101 --max-line-length=88 --show-source --statistics - - name: Test with pytest + python -m pip install --upgrade pip setuptools + pip install --editable ".[dev]" + - run: ./gyp -V && ./gyp --version && gyp -V && gyp --version + - name: Lint with ruff # See pyproject.toml for settings + run: ruff --output-format=github . + - name: Test with pytest # See pyproject.toml for settings run: pytest # - name: Run doctests with pytest # run: pytest --doctest-modules diff --git a/tools/gyp/.github/workflows/node-gyp.yml b/tools/gyp/.github/workflows/node-gyp.yml index fc28a0b512e5a7..ebe749752175ba 100644 --- a/tools/gyp/.github/workflows/node-gyp.yml +++ b/tools/gyp/.github/workflows/node-gyp.yml @@ -1,33 +1,43 @@ name: node-gyp integration - -on: [push, pull_request] - +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: jobs: - test: + integration: strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest, windows-latest] - python: ["3.7", "3.10"] + python: ["3.8", "3.10", "3.12"] runs-on: ${{ matrix.os }} steps: - name: Clone gyp-next - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: gyp-next - name: Clone nodejs/node-gyp - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nodejs/node-gyp path: node-gyp - uses: actions/setup-node@v3 with: - node-version: 14.x - - uses: actions/setup-python@v3 + node-version: 18.x + - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - - name: Install dependencies + allow-prereleases: true + - name: Install Python dependencies + run: | + cd gyp-next + python -m pip install --upgrade pip setuptools + pip install --editable . + pip uninstall -y gyp-next + - name: Install Node.js dependencies run: | cd node-gyp npm install --no-progress @@ -36,7 +46,15 @@ jobs: run: | rm -rf node-gyp/gyp cp -r gyp-next node-gyp/gyp - - name: Run tests + - name: Run tests (macOS or Linux) + if: runner.os != 'Windows' + shell: bash + run: | + cd node-gyp + npm test --python="${pythonLocation}/python" + - name: Run tests (Windows) + if: runner.os == 'Windows' + shell: pwsh run: | cd node-gyp - npm test + npm run test --python="${env:pythonLocation}\\python.exe" diff --git a/tools/gyp/.github/workflows/nodejs-windows.yml b/tools/gyp/.github/workflows/nodejs-windows.yml index 53bd7367274c12..3f52ff9ce7138f 100644 --- a/tools/gyp/.github/workflows/nodejs-windows.yml +++ b/tools/gyp/.github/workflows/nodejs-windows.yml @@ -1,17 +1,22 @@ name: Node.js Windows integration -on: [push, pull_request] +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: jobs: build-windows: runs-on: windows-latest steps: - name: Clone gyp-next - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: gyp-next - name: Clone nodejs/node - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: nodejs/node path: node diff --git a/tools/gyp/.github/workflows/release-please.yml b/tools/gyp/.github/workflows/release-please.yml index 288afdb3b32e0c..665c4c48fed210 100644 --- a/tools/gyp/.github/workflows/release-please.yml +++ b/tools/gyp/.github/workflows/release-please.yml @@ -8,9 +8,9 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: GoogleCloudPlatform/release-please-action@v2 + - uses: google-github-actions/release-please-action@v3 with: token: ${{ secrets.GITHUB_TOKEN }} release-type: python package-name: gyp-next - bump-minor-pre-major: Yes + bump-minor-pre-major: true diff --git a/tools/gyp/AUTHORS b/tools/gyp/AUTHORS index f49a357b9ed104..c05d25b2cb1aa1 100644 --- a/tools/gyp/AUTHORS +++ b/tools/gyp/AUTHORS @@ -14,3 +14,4 @@ Tom Freudenberg
# pre-release + [-_\.]? + (?Palpha|a|beta|b|preview|pre|c|rc) + [-_\.]? + (?P [0-9]+)? + )? + (?P # post release + (?:-(?P [0-9]+)) + | + (?: + [-_\.]? + (?P post|rev|r) + [-_\.]? + (?P [0-9]+)? + ) + )? + (?P # dev release + [-_\.]? + (?P dev) + [-_\.]? + (?P [0-9]+)? + )? + ) + (?:\+(?P [a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version +""" + +VERSION_PATTERN = _VERSION_PATTERN +""" +A string containing the regular expression used to match a valid version. + +The pattern is not anchored at either end, and is intended for embedding in larger +expressions (for example, matching a version number as part of a file name). The +regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` +flags set. + +:meta hide-value: +""" + + +class Version(_BaseVersion): + """This class abstracts handling of a project's versions. + + A :class:`Version` instance is comparison aware and can be compared and + sorted using the standard Python interfaces. + + >>> v1 = Version("1.0a5") + >>> v2 = Version("1.0") + >>> v1 + + >>> v2 + + >>> v1 < v2 + True + >>> v1 == v2 + False + >>> v1 > v2 + False + >>> v1 >= v2 + False + >>> v1 <= v2 + True + """ + + _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) + _key: CmpKey + + def __init__(self, version: str) -> None: + """Initialize a Version object. + + :param version: + The string representation of a version which will be parsed and normalized + before use. + :raises InvalidVersion: + If the ``version`` does not conform to PEP 440 in any way then this + exception will be raised. + """ + + # Validate the version and parse it into pieces + match = self._regex.search(version) + if not match: + raise InvalidVersion(f"Invalid version: '{version}'") + + # Store the parsed out pieces of the version + self._version = _Version( + epoch=int(match.group("epoch")) if match.group("epoch") else 0, + release=tuple(int(i) for i in match.group("release").split(".")), + pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")), + post=_parse_letter_version( + match.group("post_l"), match.group("post_n1") or match.group("post_n2") + ), + dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")), + local=_parse_local_version(match.group("local")), + ) + + # Generate a key which will be used for sorting + self._key = _cmpkey( + self._version.epoch, + self._version.release, + self._version.pre, + self._version.post, + self._version.dev, + self._version.local, + ) + + def __repr__(self) -> str: + """A representation of the Version that shows all internal state. + + >>> Version('1.0.0') + + """ + return f" " + + def __str__(self) -> str: + """A string representation of the version that can be rounded-tripped. + + >>> str(Version("1.0a5")) + '1.0a5' + """ + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + # Pre-release + if self.pre is not None: + parts.append("".join(str(x) for x in self.pre)) + + # Post-release + if self.post is not None: + parts.append(f".post{self.post}") + + # Development release + if self.dev is not None: + parts.append(f".dev{self.dev}") + + # Local version segment + if self.local is not None: + parts.append(f"+{self.local}") + + return "".join(parts) + + @property + def epoch(self) -> int: + """The epoch of the version. + + >>> Version("2.0.0").epoch + 0 + >>> Version("1!2.0.0").epoch + 1 + """ + return self._version.epoch + + @property + def release(self) -> Tuple[int, ...]: + """The components of the "release" segment of the version. + + >>> Version("1.2.3").release + (1, 2, 3) + >>> Version("2.0.0").release + (2, 0, 0) + >>> Version("1!2.0.0.post0").release + (2, 0, 0) + + Includes trailing zeroes but not the epoch or any pre-release / development / + post-release suffixes. + """ + return self._version.release + + @property + def pre(self) -> Optional[Tuple[str, int]]: + """The pre-release segment of the version. + + >>> print(Version("1.2.3").pre) + None + >>> Version("1.2.3a1").pre + ('a', 1) + >>> Version("1.2.3b1").pre + ('b', 1) + >>> Version("1.2.3rc1").pre + ('rc', 1) + """ + return self._version.pre + + @property + def post(self) -> Optional[int]: + """The post-release number of the version. + + >>> print(Version("1.2.3").post) + None + >>> Version("1.2.3.post1").post + 1 + """ + return self._version.post[1] if self._version.post else None + + @property + def dev(self) -> Optional[int]: + """The development number of the version. + + >>> print(Version("1.2.3").dev) + None + >>> Version("1.2.3.dev1").dev + 1 + """ + return self._version.dev[1] if self._version.dev else None + + @property + def local(self) -> Optional[str]: + """The local version segment of the version. + + >>> print(Version("1.2.3").local) + None + >>> Version("1.2.3+abc").local + 'abc' + """ + if self._version.local: + return ".".join(str(x) for x in self._version.local) + else: + return None + + @property + def public(self) -> str: + """The public portion of the version. + + >>> Version("1.2.3").public + '1.2.3' + >>> Version("1.2.3+abc").public + '1.2.3' + >>> Version("1.2.3+abc.dev1").public + '1.2.3' + """ + return str(self).split("+", 1)[0] + + @property + def base_version(self) -> str: + """The "base version" of the version. + + >>> Version("1.2.3").base_version + '1.2.3' + >>> Version("1.2.3+abc").base_version + '1.2.3' + >>> Version("1!1.2.3+abc.dev1").base_version + '1!1.2.3' + + The "base version" is the public version of the project without any pre or post + release markers. + """ + parts = [] + + # Epoch + if self.epoch != 0: + parts.append(f"{self.epoch}!") + + # Release segment + parts.append(".".join(str(x) for x in self.release)) + + return "".join(parts) + + @property + def is_prerelease(self) -> bool: + """Whether this version is a pre-release. + + >>> Version("1.2.3").is_prerelease + False + >>> Version("1.2.3a1").is_prerelease + True + >>> Version("1.2.3b1").is_prerelease + True + >>> Version("1.2.3rc1").is_prerelease + True + >>> Version("1.2.3dev1").is_prerelease + True + """ + return self.dev is not None or self.pre is not None + + @property + def is_postrelease(self) -> bool: + """Whether this version is a post-release. + + >>> Version("1.2.3").is_postrelease + False + >>> Version("1.2.3.post1").is_postrelease + True + """ + return self.post is not None + + @property + def is_devrelease(self) -> bool: + """Whether this version is a development release. + + >>> Version("1.2.3").is_devrelease + False + >>> Version("1.2.3.dev1").is_devrelease + True + """ + return self.dev is not None + + @property + def major(self) -> int: + """The first item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").major + 1 + """ + return self.release[0] if len(self.release) >= 1 else 0 + + @property + def minor(self) -> int: + """The second item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").minor + 2 + >>> Version("1").minor + 0 + """ + return self.release[1] if len(self.release) >= 2 else 0 + + @property + def micro(self) -> int: + """The third item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").micro + 3 + >>> Version("1").micro + 0 + """ + return self.release[2] if len(self.release) >= 3 else 0 + + +def _parse_letter_version( + letter: Optional[str], number: Union[str, bytes, SupportsInt, None] +) -> Optional[Tuple[str, int]]: + + if letter: + # We consider there to be an implicit 0 in a pre-release if there is + # not a numeral associated with it. + if number is None: + number = 0 + + # We normalize any letters to their lower case form + letter = letter.lower() + + # We consider some words to be alternate spellings of other words and + # in those cases we want to normalize the spellings to our preferred + # spelling. + if letter == "alpha": + letter = "a" + elif letter == "beta": + letter = "b" + elif letter in ["c", "pre", "preview"]: + letter = "rc" + elif letter in ["rev", "r"]: + letter = "post" + + return letter, int(number) + if not letter and number: + # We assume if we are given a number, but we are not given a letter + # then this is using the implicit post release syntax (e.g. 1.0-1) + letter = "post" + + return letter, int(number) + + return None + + +_local_version_separators = re.compile(r"[\._-]") + + +def _parse_local_version(local: Optional[str]) -> Optional[LocalType]: + """ + Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). + """ + if local is not None: + return tuple( + part.lower() if not part.isdigit() else int(part) + for part in _local_version_separators.split(local) + ) + return None + + +def _cmpkey( + epoch: int, + release: Tuple[int, ...], + pre: Optional[Tuple[str, int]], + post: Optional[Tuple[str, int]], + dev: Optional[Tuple[str, int]], + local: Optional[LocalType], +) -> CmpKey: + + # When we compare a release version, we want to compare it with all of the + # trailing zeros removed. So we'll use a reverse the list, drop all the now + # leading zeros until we come to something non zero, then take the rest + # re-reverse it back into the correct order and make it a tuple and use + # that for our sorting key. + _release = tuple( + reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) + ) + + # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0. + # We'll do this by abusing the pre segment, but we _only_ want to do this + # if there is not a pre or a post segment. If we have one of those then + # the normal sorting rules will handle this case correctly. + if pre is None and post is None and dev is not None: + _pre: CmpPrePostDevType = NegativeInfinity + # Versions without a pre-release (except as noted above) should sort after + # those with one. + elif pre is None: + _pre = Infinity + else: + _pre = pre + + # Versions without a post segment should sort before those with one. + if post is None: + _post: CmpPrePostDevType = NegativeInfinity + + else: + _post = post + + # Versions without a development segment should sort after those with one. + if dev is None: + _dev: CmpPrePostDevType = Infinity + + else: + _dev = dev + + if local is None: + # Versions without a local segment should sort before those with one. + _local: CmpLocalType = NegativeInfinity + else: + # Versions with a local segment need that segment parsed to implement + # the sorting rules in PEP440. + # - Alpha numeric segments sort before numeric segments + # - Alpha numeric segments sort lexicographically + # - Numeric segments sort numerically + # - Shorter versions sort before longer versions when the prefixes + # match exactly + _local = tuple( + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local + ) + + return epoch, _release, _pre, _post, _dev, _local diff --git a/tools/gyp/pyproject.toml b/tools/gyp/pyproject.toml new file mode 100644 index 00000000000000..0c25d0b3c1a065 --- /dev/null +++ b/tools/gyp/pyproject.toml @@ -0,0 +1,119 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "gyp-next" +version = "0.16.1" +authors = [ + { name="Node.js contributors", email="ryzokuken@disroot.org" }, +] +description = "A fork of the GYP build system for use in the Node.js projects" +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.8" +# The Python module "packaging" is vendored in the "pylib/packaging" directory to support Python >= 3.12. +# dependencies = ["packaging>=23.1"] # Uncomment this line if the vendored version is removed. +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] + +[project.optional-dependencies] +dev = ["flake8", "ruff", "pytest"] + +[project.scripts] +gyp = "gyp:script_main" + +[project.urls] +"Homepage" = "https://github.com/nodejs/gyp-next" + +[tool.ruff] +select = [ + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DTZ", # flake8-datetimez + "E", # pycodestyle + "F", # Pyflakes + "G", # flake8-logging-format + "ICN", # flake8-import-conventions + "INT", # flake8-gettext + "PL", # Pylint + "PYI", # flake8-pyi + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "T10", # flake8-debugger + "TCH", # flake8-type-checking + "TID", # flake8-tidy-imports + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 + # "A", # flake8-builtins + # "ANN", # flake8-annotations + # "ARG", # flake8-unused-arguments + # "B", # flake8-bugbear + # "BLE", # flake8-blind-except + # "COM", # flake8-commas + # "D", # pydocstyle + # "DJ", # flake8-django + # "EM", # flake8-errmsg + # "ERA", # eradicate + # "EXE", # flake8-executable + # "FBT", # flake8-boolean-trap + # "I", # isort + # "INP", # flake8-no-pep420 + # "ISC", # flake8-implicit-str-concat + # "N", # pep8-naming + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + # "PGH", # pygrep-hooks + # "PIE", # flake8-pie + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + # "Q", # flake8-quotes + # "RET", # flake8-return + # "S", # flake8-bandit + # "SIM", # flake8-simplify + # "SLF", # flake8-self + # "T20", # flake8-print + # "TRY", # tryceratops +] +ignore = [ + "E721", + "PLC1901", + "PLR0402", + "PLR1714", + "PLR2004", + "PLR5501", + "PLW0603", + "PLW2901", + "PYI024", + "RUF005", + "RUF012", + "UP031", +] +extend-exclude = ["pylib/packaging"] +line-length = 88 +target-version = "py37" + +[tool.ruff.mccabe] +max-complexity = 101 + +[tool.ruff.pylint] +max-args = 11 +max-branches = 108 +max-returns = 10 +max-statements = 286 + +[tool.setuptools] +package-dir = {"" = "pylib"} +packages = ["gyp", "gyp.generator"] diff --git a/tools/gyp/requirements_dev.txt b/tools/gyp/requirements_dev.txt deleted file mode 100644 index 28ecacab602938..00000000000000 --- a/tools/gyp/requirements_dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -flake8 -pytest diff --git a/tools/gyp/setup.py b/tools/gyp/setup.py deleted file mode 100644 index 1bb6908deaf8ce..00000000000000 --- a/tools/gyp/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright (c) 2009 Google Inc. All rights reserved. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -from os import path - -from setuptools import setup - -here = path.abspath(path.dirname(__file__)) -# Get the long description from the README file -with open(path.join(here, "README.md")) as in_file: - long_description = in_file.read() - -setup( - name="gyp-next", - version="0.13.0", - description="A fork of the GYP build system for use in the Node.js projects", - long_description=long_description, - long_description_content_type="text/markdown", - author="Node.js contributors", - author_email="ryzokuken@disroot.org", - url="https://github.com/nodejs/gyp-next", - package_dir={"": "pylib"}, - packages=["gyp", "gyp.generator"], - entry_points={"console_scripts": ["gyp=gyp:script_main"]}, - python_requires=">=3.6", - classifiers=[ - "Development Status :: 3 - Alpha", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], -) diff --git a/tools/gyp/tools/pretty_sln.py b/tools/gyp/tools/pretty_sln.py index 6ca0cd12a7ba06..cf0638a23d635d 100755 --- a/tools/gyp/tools/pretty_sln.py +++ b/tools/gyp/tools/pretty_sln.py @@ -34,10 +34,10 @@ def BuildProject(project, built, projects, deps): def ParseSolution(solution_file): # All projects, their clsid and paths. - projects = dict() + projects = {} # A list of dependencies associated with a project. - dependencies = dict() + dependencies = {} # Regular expressions that matches the SLN format. # The first line of a project definition. diff --git a/tools/gyp/tools/pretty_vcproj.py b/tools/gyp/tools/pretty_vcproj.py index 00d32debda51f0..72c65d7fc495f9 100755 --- a/tools/gyp/tools/pretty_vcproj.py +++ b/tools/gyp/tools/pretty_vcproj.py @@ -21,7 +21,7 @@ __author__ = "nsylvain (Nicolas Sylvain)" ARGUMENTS = None -REPLACEMENTS = dict() +REPLACEMENTS = {} def cmp(x, y):