├── .github ├── dependabot.yml └── workflows │ ├── node-gyp.yml │ ├── nodejs.yml │ ├── python_tests.yml │ └── release-please.yml ├── .gitignore ├── .release-please-manifest.json ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── data ├── ninja │ └── build.ninja └── win │ └── large-pdb-shim.cc ├── docs ├── GypVsCMake.md ├── Hacking.md ├── InputFormatReference.md ├── LanguageSpecification.md ├── README.md ├── Testing.md └── UserDocumentation.md ├── gyp ├── gyp.bat ├── gyp_main.py ├── pylib ├── gyp │ ├── MSVSNew.py │ ├── MSVSProject.py │ ├── MSVSSettings.py │ ├── MSVSSettings_test.py │ ├── MSVSToolFile.py │ ├── MSVSUserFile.py │ ├── MSVSUtil.py │ ├── MSVSVersion.py │ ├── __init__.py │ ├── common.py │ ├── common_test.py │ ├── easy_xml.py │ ├── easy_xml_test.py │ ├── flock_tool.py │ ├── generator │ │ ├── __init__.py │ │ ├── analyzer.py │ │ ├── android.py │ │ ├── cmake.py │ │ ├── compile_commands_json.py │ │ ├── dump_dependency_json.py │ │ ├── eclipse.py │ │ ├── gypd.py │ │ ├── gypsh.py │ │ ├── make.py │ │ ├── msvs.py │ │ ├── msvs_test.py │ │ ├── ninja.py │ │ ├── ninja_test.py │ │ ├── xcode.py │ │ └── xcode_test.py │ ├── input.py │ ├── input_test.py │ ├── mac_tool.py │ ├── msvs_emulation.py │ ├── ninja_syntax.py │ ├── simple_copy.py │ ├── win_tool.py │ ├── xcode_emulation.py │ ├── xcode_emulation_test.py │ ├── xcode_ninja.py │ ├── xcodeproj_file.py │ └── xml_fix.py └── packaging │ ├── LICENSE │ ├── LICENSE.APACHE │ ├── LICENSE.BSD │ ├── __init__.py │ ├── _elffile.py │ ├── _manylinux.py │ ├── _musllinux.py │ ├── _parser.py │ ├── _structures.py │ ├── _tokenizer.py │ ├── markers.py │ ├── metadata.py │ ├── py.typed │ ├── requirements.py │ ├── specifiers.py │ ├── tags.py │ ├── utils.py │ └── version.py ├── pyproject.toml ├── release-please-config.json ├── test ├── fixtures │ ├── expected-darwin │ │ ├── cmake │ │ │ └── CMakeLists.txt │ │ ├── make │ │ │ └── test.target.mk │ │ └── ninja │ │ │ └── test.ninja │ ├── expected-linux │ │ ├── cmake │ │ │ └── CMakeLists.txt │ │ ├── make │ │ │ └── test.target.mk │ │ └── ninja │ │ │ └── test.ninja │ ├── include │ │ └── test.h │ ├── integration.gyp │ └── test.cc └── integration_test.py ├── test_gyp.py └── tools ├── README ├── Xcode ├── README └── Specifications │ ├── gyp.pbfilespec │ └── gyp.xclangspec ├── emacs ├── README ├── gyp-tests.el ├── gyp.el ├── run-unit-tests.sh └── testdata │ ├── media.gyp │ └── media.gyp.fontified ├── graphviz.py ├── pretty_gyp.py ├── pretty_sln.py └── pretty_vcproj.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Keep GitHub Actions up to date with Dependabot... 2 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | groups: 8 | GitHub_Actions: 9 | patterns: 10 | - "*" # Group all Actions updates into a single larger pull request 11 | schedule: 12 | interval: weekly 13 | - package-ecosystem: "pip" 14 | directory: "/" 15 | groups: 16 | pip: 17 | patterns: 18 | - "*" # Group all pip updates into a single larger pull request 19 | schedule: 20 | interval: weekly 21 | -------------------------------------------------------------------------------- /.github/workflows/node-gyp.yml: -------------------------------------------------------------------------------- 1 | name: node-gyp integration 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | node-gyp-integration: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: ["22"] 13 | os: [macos-latest, ubuntu-latest, windows-latest] 14 | python-version: ["3.9", "3.11", "3.13"] 15 | include: 16 | - node-version: "22" 17 | os: macos-13 # macOS on Intel 18 | python-version: "3.13" 19 | - node-version: "22" 20 | os: ubuntu-24.04-arm # Ubuntu on ARM 21 | python-version: "3.13" 22 | - node-version: "22" 23 | os: windows-11-arm # Windows on ARM 24 | python-version: "3.13" 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Clone gyp-next 28 | uses: actions/checkout@v4 29 | with: 30 | path: gyp-next 31 | - name: Clone nodejs/node-gyp 32 | uses: actions/checkout@v4 33 | with: 34 | repository: nodejs/node-gyp 35 | path: node-gyp 36 | - uses: actions/setup-node@v4 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | allow-prereleases: true 43 | - name: Install Python dependencies 44 | run: | 45 | cd gyp-next 46 | python -m pip install --upgrade pip 47 | pip install --editable . 48 | pip uninstall -y gyp-next 49 | - name: Install Node.js dependencies 50 | run: | 51 | cd node-gyp 52 | npm install --no-progress 53 | - name: Replace gyp in node-gyp 54 | shell: bash 55 | run: | 56 | rm -rf node-gyp/gyp 57 | cp -r gyp-next node-gyp/gyp 58 | - name: Run tests (macOS or Linux) 59 | if: runner.os != 'Windows' 60 | run: | 61 | cd node-gyp 62 | npm test --python="${pythonLocation}/python" 63 | - name: Run tests (Windows) 64 | if: runner.os == 'Windows' 65 | shell: pwsh 66 | run: | 67 | cd node-gyp 68 | npm run test --python="${env:pythonLocation}\\python.exe" 69 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js integration 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | nodejs-integration: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [macos-latest, ubuntu-latest, windows-latest] 13 | python: ["3.9", "3.11", "3.13"] 14 | include: 15 | - os: macos-13 # macOS on Intel 16 | python-version: "3.13" 17 | - os: ubuntu-24.04-arm # Ubuntu on ARM 18 | python-version: "3.13" 19 | - os: windows-11-arm # Windows on ARM 20 | python-version: "3.13" 21 | 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - name: Clone gyp-next 25 | uses: actions/checkout@v4 26 | with: 27 | path: gyp-next 28 | - name: Clone nodejs/node 29 | uses: actions/checkout@v4 30 | with: 31 | repository: nodejs/node 32 | path: node 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python }} 36 | allow-prereleases: true 37 | - name: Replace gyp in Node.js 38 | shell: bash 39 | run: | 40 | rm -rf node/tools/gyp 41 | cp -r gyp-next node/tools/gyp 42 | 43 | # macOS and Linux 44 | - name: Run configure 45 | if: runner.os != 'Windows' 46 | run: | 47 | cd node 48 | ./configure 49 | 50 | # Windows 51 | - name: Install deps 52 | if: runner.os == 'Windows' 53 | run: choco install nasm 54 | - name: Run configure 55 | if: runner.os == 'Windows' 56 | run: | 57 | cd node 58 | ./vcbuild.bat nobuild 59 | -------------------------------------------------------------------------------- /.github/workflows/python_tests.yml: -------------------------------------------------------------------------------- 1 | # TODO: Enable os: windows-latest 2 | # TODO: Enable pytest --doctest-modules 3 | 4 | name: Python_tests 5 | on: 6 | push: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | Python_tests: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | max-parallel: 5 16 | matrix: 17 | os: [macos-13, macos-latest, ubuntu-latest] # , windows-latest] 18 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | allow-prereleases: true 26 | - uses: seanmiddleditch/gha-setup-ninja@v6 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | pip install --editable ".[dev]" 31 | - run: ./gyp -V && ./gyp --version && gyp -V && gyp --version 32 | - name: Lint with ruff # See pyproject.toml for settings 33 | uses: astral-sh/ruff-action@v3 34 | - name: Test with pytest # See pyproject.toml for settings 35 | run: pytest 36 | # - name: Run doctests with pytest 37 | # run: pytest --doctest-modules 38 | - name: Test CLI commands on a pipx install 39 | run: | 40 | pipx run --no-cache --spec ./ gyp --help 41 | pipx run --no-cache --spec ./ gyp --version 42 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: release-please 7 | jobs: 8 | release-please: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | release_created: ${{ steps.release.outputs.release_created }} 12 | tag_name: ${{ steps.release.outputs.tag_name }} 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | steps: 17 | - uses: google-github-actions/release-please-action@v4 18 | id: release 19 | 20 | build: 21 | name: Build distribution 22 | needs: 23 | - release-please 24 | if: ${{ needs.release-please.outputs.release_created }} # only publish on release 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - name: Build a binary wheel and a source tarball 29 | run: pipx run build 30 | - name: Store the distribution packages 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: python-package-distributions 34 | path: dist/ 35 | 36 | publish-to-pypi: 37 | name: >- 38 | Publish Python distribution to PyPI 39 | needs: 40 | - release-please 41 | - build 42 | if: ${{ needs.release-please.outputs.release_created }} # only publish on release 43 | runs-on: ubuntu-latest 44 | environment: 45 | name: pypi 46 | url: https://pypi.org/p/gyp-next 47 | permissions: 48 | id-token: write # IMPORTANT: mandatory for trusted publishing 49 | steps: 50 | - name: Download all the dists 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: python-package-distributions 54 | path: dist/ 55 | - name: Publish distribution to PyPI 56 | uses: pypa/gh-action-pypi-publish@release/v1 57 | 58 | github-release: 59 | name: >- 60 | Publish Python distribution to GitHub Release 61 | needs: 62 | - release-please 63 | - build 64 | if: ${{ needs.release-please.outputs.release_created }} # only publish on release 65 | runs-on: ubuntu-latest 66 | permissions: 67 | contents: write # IMPORTANT: mandatory for making GitHub Releases 68 | id-token: write # IMPORTANT: mandatory for sigstore 69 | steps: 70 | - name: Download all the dists 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: python-package-distributions 74 | path: dist/ 75 | - name: Sign the dists with Sigstore 76 | uses: sigstore/gh-action-sigstore-python@v3.0.0 77 | with: 78 | inputs: >- 79 | ./dist/*.tar.gz 80 | ./dist/*.whl 81 | - name: Upload artifact signatures to GitHub Release 82 | env: 83 | GITHUB_TOKEN: ${{ github.token }} 84 | # Upload to GitHub Release using the `gh` CLI. 85 | # `dist/` contains the built packages, and the 86 | # sigstore-produced signatures and certificates. 87 | run: >- 88 | gh release upload 89 | ${{ needs.release-please.outputs.tag_name }} dist/** 90 | --repo '${{ github.repository }}' 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # pytype static type analyzer 136 | .pytype/ 137 | 138 | # Cython debug symbols 139 | cython_debug/ 140 | 141 | # static files generated from Django application using `collectstatic` 142 | media 143 | static 144 | 145 | test/fixtures/out 146 | *.actual 147 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "0.20.0" 3 | } 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Names should be added to this file like so: 2 | # Name or Organization 3 | 4 | Google Inc. <*@google.com> 5 | Bloomberg Finance L.P. <*@bloomberg.net> 6 | IBM Inc. <*@*.ibm.com> 7 | Yandex LLC <*@yandex-team.ru> 8 | 9 | Steven Knight 10 | Ryan Norton 11 | David J. Sankel 12 | Eric N. Vander Weele 13 | Tom Freudenberg 14 | Julien Brianceau 15 | Refael Ackermann 16 | Ujjwal Sharma 17 | Christian Clauss 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | * [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md) 4 | * [Node.js Moderation Policy](https://github.com/nodejs/admin/blob/HEAD/Moderation-Policy.md) 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to gyp-next 2 | 3 | ## Start contributing 4 | 5 | Read the docs at [`./docs/Hacking.md`](./docs/Hacking.md) to get started. 6 | 7 | ## Code of Conduct 8 | 9 | This project is bound to the [Node.js Code of Conduct](https://github.com/nodejs/admin/blob/HEAD/CODE_OF_CONDUCT.md). 10 | 11 | 12 | ## Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | * (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | * (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | * (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | * (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Node.js contributors. All rights reserved. 2 | Copyright (c) 2009 Google Inc. All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above 11 | copyright notice, this list of conditions and the following disclaimer 12 | in the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Google Inc. nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GYP can Generate Your Projects. 2 | =================================== 3 | 4 | Documents are available at [`./docs`](./docs). 5 | 6 | __gyp-next__ is [released](https://github.com/nodejs/gyp-next/releases) to the [__Python Packaging Index__](https://pypi.org/project/gyp-next) (PyPI) and can be installed with the command: 7 | * `python3 -m pip install gyp-next` 8 | 9 | When used as a command line utility, __gyp-next__ can also be installed with [pipx](https://pypa.github.io/pipx): 10 | * `pipx install gyp-next` 11 | ``` 12 | Installing to a new venv 'gyp-next' 13 | installed package gyp-next 0.13.0, installed using Python 3.10.6 14 | These apps are now globally available 15 | - gyp 16 | done! ✨ 🌟 ✨ 17 | ``` 18 | 19 | Or to run __gyp-next__ directly without installing it: 20 | * `pipx run gyp-next --help` 21 | ``` 22 | NOTE: running app 'gyp' from 'gyp-next' 23 | usage: usage: gyp [options ...] [build_file ...] 24 | 25 | options: 26 | -h, --help show this help message and exit 27 | --build CONFIGS configuration for build after project generation 28 | --check check format of gyp files 29 | [ ... ] 30 | ``` 31 | -------------------------------------------------------------------------------- /data/ninja/build.ninja: -------------------------------------------------------------------------------- 1 | rule cc 2 | command = cc $in $out 3 | 4 | build my.out: cc my.in 5 | -------------------------------------------------------------------------------- /data/win/large-pdb-shim.cc: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Google Inc. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // This file is used to generate an empty .pdb -- with a 4KB pagesize -- that is 6 | // then used during the final link for modules that have large PDBs. Otherwise, 7 | // the linker will generate a pdb with a page size of 1KB, which imposes a limit 8 | // of 1GB on the .pdb. By generating an initial empty .pdb with the compiler 9 | // (rather than the linker), this limit is avoided. With this in place PDBs may 10 | // grow to 2GB. 11 | // 12 | // This file is referenced by the msvs_large_pdb mechanism in MSVSUtil.py. 13 | -------------------------------------------------------------------------------- /docs/GypVsCMake.md: -------------------------------------------------------------------------------- 1 | # vs. CMake 2 | 3 | GYP was originally created to generate native IDE project files (Visual Studio, Xcode) for building [Chromium](http://www.chromim.org). 4 | 5 | The functionality of GYP is very similar to the [CMake](http://www.cmake.org) 6 | build tool. Bradley Nelson wrote up the following description of why the team 7 | created GYP instead of using CMake. The text below is copied from 8 | http://www.mail-archive.com/webkit-dev@lists.webkit.org/msg11029.html 9 | 10 | ``` 11 | 12 | Re: [webkit-dev] CMake as a build system? 13 | Bradley Nelson 14 | Mon, 19 Apr 2010 22:38:30 -0700 15 | 16 | Here's the innards of an email with a laundry list of stuff I came up with a 17 | while back on the gyp-developers list in response to Mike Craddick regarding 18 | what motivated gyp's development, since we were aware of cmake at the time 19 | (we'd even started a speculative port): 20 | 21 | 22 | I did an exploratory port of portions of Chromium to cmake (I think I got as 23 | far as net, base, sandbox, and part of webkit). 24 | There were a number of motivations, not all of which would apply to other 25 | projects. Also, some of the design of gyp was informed by experience at 26 | Google with large projects built wholly from source, leading to features 27 | absent from cmake, but not strictly required for Chromium. 28 | 29 | 1. Ability to incrementally transition on Windows. It took us about 6 months 30 | to switch fully to gyp. Previous attempts to move to scons had taken a long 31 | time and failed, due to the requirement to transition while in flight. For a 32 | substantial period of time, we had a hybrid of checked in vcproj and gyp generated 33 | vcproj. To this day we still have a good number of GUIDs pinned in the gyp files, 34 | because different parts of our release pipeline have leftover assumptions 35 | regarding manipulating the raw sln/vcprojs. This transition occurred from 36 | the bottom up, largely because modules like base were easier to convert, and 37 | had a lower churn rate. During early stages of the transition, the majority 38 | of the team wasn't even aware they were using gyp, as it integrated into 39 | their existing workflow, and only affected modules that had been converted. 40 | 41 | 2. Generation of a more 'normal' vcproj file. Gyp attempts, particularly on 42 | Windows, to generate vcprojs which resemble hand generated projects. It 43 | doesn't generate any Makefile type projects, but instead produces msvs 44 | Custom Build Steps and Custom Build Rules. This makes the resulting projects 45 | easier to understand from the IDE and avoids parts of the IDE that simply 46 | don't function correctly if you use Makefile projects. Our early hope with 47 | gyp was to support the least common denominator of features present in each 48 | of the platform specific project file formats, rather than falling back on 49 | generated Makefiles/shell scripts to emulate some common abstraction. CMake by 50 | comparison makes a good faith attempt to use native project features, but 51 | falls back on generated scripts in order to preserve the same semantics on 52 | each platforms. 53 | 54 | 3. Abstraction on the level of project settings, rather than command line 55 | flags. In gyp's syntax you can add nearly any option present in a hand 56 | generated xcode/vcproj file. This allows you to use abstractions built into 57 | the IDEs rather than reverse engineering them possibly incorrectly for 58 | things like: manifest generation, precompiled headers, bundle generation. 59 | When somebody wants to use a particular menu option from msvs, I'm able to 60 | do a web search on the name of the setting from the IDE and provide them 61 | with a gyp stanza that does the equivalent. In many cases, not all project 62 | file constructs correspond to command line flags. 63 | 64 | 4. Strong notion of module public/private interface. Gyp allows targets to 65 | publish a set of direct_dependent_settings, specifying things like 66 | include_dirs, defines, platforms specific settings, etc. This means that 67 | when module A depends on module B, it automatically acquires the right build 68 | settings without module A being filled with assumptions/knowledge of exactly 69 | how module B is built. Additionally, all of the transitive dependencies of 70 | module B are pulled in. This avoids their being a single top level view of 71 | the project, rather each gyp file expresses knowledge about its immediate 72 | neighbors. This keep local knowledge local. CMake effectively has a large 73 | shared global namespace. 74 | 75 | 5. Cross platform generation. CMake is not able to generate all project 76 | files on all platforms. For example xcode projects cannot be generated from 77 | windows (cmake uses mac specific libraries to do project generation). This 78 | means that for instance generating a tarball containing pregenerated 79 | projects for all platforms is hard with Cmake (requires distribution to 80 | several machine types). 81 | 82 | 6. Gyp has rudimentary cross compile support. Currently we've added enough 83 | functionality to gyp to support x86 -> arm cross compiles. Last I checked 84 | this functionality wasn't present in cmake. (This occurred later). 85 | 86 | 87 | That being said there are a number of drawbacks currently to gyp: 88 | 89 | 1. Because platform specific settings are expressed at the project file 90 | level (rather than the command line level). Settings which might otherwise 91 | be shared in common between platforms (flags to gcc on mac/linux), end up 92 | being repeated twice. Though in fairness there is actually less sharing here 93 | than you'd think. include_dirs and defines actually represent 90% of what 94 | can be typically shared. 95 | 96 | 2. CMake may be more mature, having been applied to a broader range of 97 | projects. There a number of 'tool modules' for cmake, which are shared in a 98 | common community. 99 | 100 | 3. gyp currently makes some nasty assumptions about the availability of 101 | chromium's hermetic copy of cygwin on windows. This causes you to either 102 | have to special case a number of rules, or swallow this copy of cygwin as a 103 | build time dependency. 104 | 105 | 4. CMake includes a fairly readable imperative language. Currently Gyp has a 106 | somewhat poorly specified declarative language (variable expansion happens 107 | in sometimes weird and counter-intuitive ways). In fairness though, gyp assumes 108 | that external python scripts can be used as an escape hatch. Also gyp avoids 109 | a lot of the things you'd need imperative code for, by having a nice target 110 | settings publication mechanism. 111 | 112 | 5. (Feature/drawback depending on personal preference). Gyp's syntax is 113 | DEEPLY nested. It suffers from all of Lisp's advantages and drawbacks. 114 | 115 | -BradN 116 | ``` 117 | -------------------------------------------------------------------------------- /docs/Hacking.md: -------------------------------------------------------------------------------- 1 | # Hacking 2 | 3 | ## Getting the sources 4 | 5 | Git is required to hack on anything, you can set up a git clone of GYP 6 | as follows: 7 | 8 | ``` 9 | mkdir foo 10 | cd foo 11 | git clone git@github.com:nodejs/gyp-next.git 12 | cd gyp 13 | ``` 14 | 15 | (this will clone gyp underneath it into `foo/gyp`. 16 | `foo` can be any directory name you want. Once you've done that, 17 | you can use the repo like anything other Git repo. 18 | 19 | ## Testing your change 20 | 21 | GYP has a suite of tests which you can run with the provided test driver 22 | to make sure your changes aren't breaking anything important. 23 | 24 | You run the test driver with e.g. 25 | 26 | ``` sh 27 | $ python -m pip install --upgrade pip 28 | $ pip install --editable ".[dev]" 29 | $ python -m pytest 30 | ``` 31 | 32 | See [Testing](Testing.md) for more details on the test framework. 33 | 34 | Note that it can be handy to look at the project files output by the tests 35 | to diagnose problems. The easiest way to do that is by kindly asking the 36 | test driver to leave the temporary directories it creates in-place. 37 | This is done by setting the environment variable "PRESERVE", e.g. 38 | 39 | ``` 40 | set PRESERVE=all # On Windows 41 | export PRESERVE=all # On saner platforms. 42 | ``` 43 | 44 | ## Reviewing your change 45 | 46 | All changes to GYP must be code reviewed before submission. 47 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Generate Your Projects (gyp-next) 2 | 3 | GYP is a Meta-Build system: a build system that generates other build systems. 4 | 5 | * [User documentation](./UserDocumentation.md) 6 | * [Input Format Reference](./InputFormatReference.md) 7 | * [Language specification](./LanguageSpecification.md) 8 | * [Hacking](./Hacking.md) 9 | * [Testing](./Testing.md) 10 | * [GYP vs. CMake](./GypVsCMake.md) 11 | 12 | GYP is intended to support large projects that need to be built on multiple 13 | platforms (e.g., Mac, Windows, Linux), and where it is important that 14 | the project can be built using the IDEs that are popular on each platform 15 | as if the project is a "native" one. 16 | 17 | It can be used to generate XCode projects, Visual Studio projects, Ninja 18 | build files, and Makefiles. In each case GYP's goal is to replicate as 19 | closely as possible the way one would set up a native build of the project 20 | using the IDE. 21 | 22 | GYP can also be used to generate "hybrid" projects that provide the IDE 23 | scaffolding for a nice user experience but call out to Ninja to do the actual 24 | building (which is usually much faster than the native build systems of the 25 | IDEs). 26 | 27 | For more information on GYP, click on the links above. 28 | -------------------------------------------------------------------------------- /gyp: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Copyright 2013 The Chromium Authors. All rights reserved. 3 | # Use of this source code is governed by a BSD-style license that can be 4 | # found in the LICENSE file. 5 | 6 | set -e 7 | base=$(dirname "$0") 8 | exec python "${base}/gyp_main.py" "$@" 9 | -------------------------------------------------------------------------------- /gyp.bat: -------------------------------------------------------------------------------- 1 | @rem Copyright (c) 2009 Google Inc. All rights reserved. 2 | @rem Use of this source code is governed by a BSD-style license that can be 3 | @rem found in the LICENSE file. 4 | 5 | @python "%~dp0gyp_main.py" %* 6 | -------------------------------------------------------------------------------- /gyp_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2009 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | import os 8 | import subprocess 9 | import sys 10 | 11 | 12 | def IsCygwin(): 13 | # Function copied from pylib/gyp/common.py 14 | try: 15 | out = subprocess.Popen( 16 | "uname", stdout=subprocess.PIPE, stderr=subprocess.STDOUT 17 | ) 18 | stdout, _ = out.communicate() 19 | return "CYGWIN" in stdout.decode("utf-8") 20 | except Exception: 21 | return False 22 | 23 | 24 | def UnixifyPath(path): 25 | try: 26 | if not IsCygwin(): 27 | return path 28 | out = subprocess.Popen( 29 | ["cygpath", "-u", path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT 30 | ) 31 | stdout, _ = out.communicate() 32 | return stdout.decode("utf-8") 33 | except Exception: 34 | return path 35 | 36 | 37 | # Make sure we're using the version of pylib in this repo, not one installed 38 | # elsewhere on the system. Also convert to Unix style path on Cygwin systems, 39 | # else the 'gyp' library will not be found 40 | path = UnixifyPath(sys.argv[0]) 41 | sys.path.insert(0, os.path.join(os.path.dirname(path), "pylib")) 42 | import gyp # noqa: E402 43 | 44 | if __name__ == "__main__": 45 | sys.exit(gyp.script_main()) 46 | -------------------------------------------------------------------------------- /pylib/gyp/MSVSProject.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """Visual Studio project reader/writer.""" 6 | 7 | from gyp import easy_xml 8 | 9 | # ------------------------------------------------------------------------------ 10 | 11 | 12 | class Tool: 13 | """Visual Studio tool.""" 14 | 15 | def __init__(self, name, attrs=None): 16 | """Initializes the tool. 17 | 18 | Args: 19 | name: Tool name. 20 | attrs: Dict of tool attributes; may be None. 21 | """ 22 | self._attrs = attrs or {} 23 | self._attrs["Name"] = name 24 | 25 | def _GetSpecification(self): 26 | """Creates an element for the tool. 27 | 28 | Returns: 29 | A new xml.dom.Element for the tool. 30 | """ 31 | return ["Tool", self._attrs] 32 | 33 | 34 | class Filter: 35 | """Visual Studio filter - that is, a virtual folder.""" 36 | 37 | def __init__(self, name, contents=None): 38 | """Initializes the folder. 39 | 40 | Args: 41 | name: Filter (folder) name. 42 | contents: List of filenames and/or Filter objects contained. 43 | """ 44 | self.name = name 45 | self.contents = list(contents or []) 46 | 47 | 48 | # ------------------------------------------------------------------------------ 49 | 50 | 51 | class Writer: 52 | """Visual Studio XML project writer.""" 53 | 54 | def __init__(self, project_path, version, name, guid=None, platforms=None): 55 | """Initializes the project. 56 | 57 | Args: 58 | project_path: Path to the project file. 59 | version: Format version to emit. 60 | name: Name of the project. 61 | guid: GUID to use for project, if not None. 62 | platforms: Array of string, the supported platforms. If null, ['Win32'] 63 | """ 64 | self.project_path = project_path 65 | self.version = version 66 | self.name = name 67 | self.guid = guid 68 | 69 | # Default to Win32 for platforms. 70 | if not platforms: 71 | platforms = ["Win32"] 72 | 73 | # Initialize the specifications of the various sections. 74 | self.platform_section = ["Platforms"] 75 | for platform in platforms: 76 | self.platform_section.append(["Platform", {"Name": platform}]) 77 | self.tool_files_section = ["ToolFiles"] 78 | self.configurations_section = ["Configurations"] 79 | self.files_section = ["Files"] 80 | 81 | # Keep a dict keyed on filename to speed up access. 82 | self.files_dict = {} 83 | 84 | def AddToolFile(self, path): 85 | """Adds a tool file to the project. 86 | 87 | Args: 88 | path: Relative path from project to tool file. 89 | """ 90 | self.tool_files_section.append(["ToolFile", {"RelativePath": path}]) 91 | 92 | def _GetSpecForConfiguration(self, config_type, config_name, attrs, tools): 93 | """Returns the specification for a configuration. 94 | 95 | Args: 96 | config_type: Type of configuration node. 97 | config_name: Configuration name. 98 | attrs: Dict of configuration attributes; may be None. 99 | tools: List of tools (strings or Tool objects); may be None. 100 | Returns: 101 | """ 102 | # Handle defaults 103 | if not attrs: 104 | attrs = {} 105 | if not tools: 106 | tools = [] 107 | 108 | # Add configuration node and its attributes 109 | node_attrs = attrs.copy() 110 | node_attrs["Name"] = config_name 111 | specification = [config_type, node_attrs] 112 | 113 | # Add tool nodes and their attributes 114 | if tools: 115 | for t in tools: 116 | if isinstance(t, Tool): 117 | specification.append(t._GetSpecification()) 118 | else: 119 | specification.append(Tool(t)._GetSpecification()) 120 | return specification 121 | 122 | def AddConfig(self, name, attrs=None, tools=None): 123 | """Adds a configuration to the project. 124 | 125 | Args: 126 | name: Configuration name. 127 | attrs: Dict of configuration attributes; may be None. 128 | tools: List of tools (strings or Tool objects); may be None. 129 | """ 130 | spec = self._GetSpecForConfiguration("Configuration", name, attrs, tools) 131 | self.configurations_section.append(spec) 132 | 133 | def _AddFilesToNode(self, parent, files): 134 | """Adds files and/or filters to the parent node. 135 | 136 | Args: 137 | parent: Destination node 138 | files: A list of Filter objects and/or relative paths to files. 139 | 140 | Will call itself recursively, if the files list contains Filter objects. 141 | """ 142 | for f in files: 143 | if isinstance(f, Filter): 144 | node = ["Filter", {"Name": f.name}] 145 | self._AddFilesToNode(node, f.contents) 146 | else: 147 | node = ["File", {"RelativePath": f}] 148 | self.files_dict[f] = node 149 | parent.append(node) 150 | 151 | def AddFiles(self, files): 152 | """Adds files to the project. 153 | 154 | Args: 155 | files: A list of Filter objects and/or relative paths to files. 156 | 157 | This makes a copy of the file/filter tree at the time of this call. If you 158 | later add files to a Filter object which was passed into a previous call 159 | to AddFiles(), it will not be reflected in this project. 160 | """ 161 | self._AddFilesToNode(self.files_section, files) 162 | # TODO(rspangler) This also doesn't handle adding files to an existing 163 | # filter. That is, it doesn't merge the trees. 164 | 165 | def AddFileConfig(self, path, config, attrs=None, tools=None): 166 | """Adds a configuration to a file. 167 | 168 | Args: 169 | path: Relative path to the file. 170 | config: Name of configuration to add. 171 | attrs: Dict of configuration attributes; may be None. 172 | tools: List of tools (strings or Tool objects); may be None. 173 | 174 | Raises: 175 | ValueError: Relative path does not match any file added via AddFiles(). 176 | """ 177 | # Find the file node with the right relative path 178 | parent = self.files_dict.get(path) 179 | if not parent: 180 | raise ValueError('AddFileConfig: file "%s" not in project.' % path) 181 | 182 | # Add the config to the file node 183 | spec = self._GetSpecForConfiguration("FileConfiguration", config, attrs, tools) 184 | parent.append(spec) 185 | 186 | def WriteIfChanged(self): 187 | """Writes the project file.""" 188 | # First create XML content definition 189 | content = [ 190 | "VisualStudioProject", 191 | { 192 | "ProjectType": "Visual C++", 193 | "Version": self.version.ProjectVersion(), 194 | "Name": self.name, 195 | "ProjectGUID": self.guid, 196 | "RootNamespace": self.name, 197 | "Keyword": "Win32Proj", 198 | }, 199 | self.platform_section, 200 | self.tool_files_section, 201 | self.configurations_section, 202 | ["References"], # empty section 203 | self.files_section, 204 | ["Globals"], # empty section 205 | ] 206 | easy_xml.WriteXmlIfChanged(content, self.project_path, encoding="Windows-1252") 207 | -------------------------------------------------------------------------------- /pylib/gyp/MSVSToolFile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """Visual Studio project reader/writer.""" 6 | 7 | from gyp import easy_xml 8 | 9 | 10 | class Writer: 11 | """Visual Studio XML tool file writer.""" 12 | 13 | def __init__(self, tool_file_path, name): 14 | """Initializes the tool file. 15 | 16 | Args: 17 | tool_file_path: Path to the tool file. 18 | name: Name of the tool file. 19 | """ 20 | self.tool_file_path = tool_file_path 21 | self.name = name 22 | self.rules_section = ["Rules"] 23 | 24 | def AddCustomBuildRule( 25 | self, name, cmd, description, additional_dependencies, outputs, extensions 26 | ): 27 | """Adds a rule to the tool file. 28 | 29 | Args: 30 | name: Name of the rule. 31 | description: Description of the rule. 32 | cmd: Command line of the rule. 33 | additional_dependencies: other files which may trigger the rule. 34 | outputs: outputs of the rule. 35 | extensions: extensions handled by the rule. 36 | """ 37 | rule = [ 38 | "CustomBuildRule", 39 | { 40 | "Name": name, 41 | "ExecutionDescription": description, 42 | "CommandLine": cmd, 43 | "Outputs": ";".join(outputs), 44 | "FileExtensions": ";".join(extensions), 45 | "AdditionalDependencies": ";".join(additional_dependencies), 46 | }, 47 | ] 48 | self.rules_section.append(rule) 49 | 50 | def WriteIfChanged(self): 51 | """Writes the tool file.""" 52 | content = [ 53 | "VisualStudioToolFile", 54 | {"Version": "8.00", "Name": self.name}, 55 | self.rules_section, 56 | ] 57 | easy_xml.WriteXmlIfChanged( 58 | content, self.tool_file_path, encoding="Windows-1252" 59 | ) 60 | -------------------------------------------------------------------------------- /pylib/gyp/MSVSUserFile.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """Visual Studio user preferences file writer.""" 6 | 7 | import os 8 | import re 9 | import socket # for gethostname 10 | 11 | from gyp import easy_xml 12 | 13 | # ------------------------------------------------------------------------------ 14 | 15 | 16 | def _FindCommandInPath(command): 17 | """If there are no slashes in the command given, this function 18 | searches the PATH env to find the given command, and converts it 19 | to an absolute path. We have to do this because MSVS is looking 20 | for an actual file to launch a debugger on, not just a command 21 | line. Note that this happens at GYP time, so anything needing to 22 | be built needs to have a full path.""" 23 | if "/" in command or "\\" in command: 24 | # If the command already has path elements (either relative or 25 | # absolute), then assume it is constructed properly. 26 | return command 27 | else: 28 | # Search through the path list and find an existing file that 29 | # we can access. 30 | paths = os.environ.get("PATH", "").split(os.pathsep) 31 | for path in paths: 32 | item = os.path.join(path, command) 33 | if os.path.isfile(item) and os.access(item, os.X_OK): 34 | return item 35 | return command 36 | 37 | 38 | def _QuoteWin32CommandLineArgs(args): 39 | new_args = [] 40 | for arg in args: 41 | # Replace all double-quotes with double-double-quotes to escape 42 | # them for cmd shell, and then quote the whole thing if there 43 | # are any. 44 | if arg.find('"') != -1: 45 | arg = '""'.join(arg.split('"')) 46 | arg = '"%s"' % arg 47 | 48 | # Otherwise, if there are any spaces, quote the whole arg. 49 | elif re.search(r"[ \t\n]", arg): 50 | arg = '"%s"' % arg 51 | new_args.append(arg) 52 | return new_args 53 | 54 | 55 | class Writer: 56 | """Visual Studio XML user user file writer.""" 57 | 58 | def __init__(self, user_file_path, version, name): 59 | """Initializes the user file. 60 | 61 | Args: 62 | user_file_path: Path to the user file. 63 | version: Version info. 64 | name: Name of the user file. 65 | """ 66 | self.user_file_path = user_file_path 67 | self.version = version 68 | self.name = name 69 | self.configurations = {} 70 | 71 | def AddConfig(self, name): 72 | """Adds a configuration to the project. 73 | 74 | Args: 75 | name: Configuration name. 76 | """ 77 | self.configurations[name] = ["Configuration", {"Name": name}] 78 | 79 | def AddDebugSettings( 80 | self, config_name, command, environment={}, working_directory="" 81 | ): 82 | """Adds a DebugSettings node to the user file for a particular config. 83 | 84 | Args: 85 | command: command line to run. First element in the list is the 86 | executable. All elements of the command will be quoted if 87 | necessary. 88 | working_directory: other files which may trigger the rule. (optional) 89 | """ 90 | command = _QuoteWin32CommandLineArgs(command) 91 | 92 | abs_command = _FindCommandInPath(command[0]) 93 | 94 | if environment and isinstance(environment, dict): 95 | env_list = [f'{key}="{val}"' for (key, val) in environment.items()] 96 | environment = " ".join(env_list) 97 | else: 98 | environment = "" 99 | 100 | n_cmd = [ 101 | "DebugSettings", 102 | { 103 | "Command": abs_command, 104 | "WorkingDirectory": working_directory, 105 | "CommandArguments": " ".join(command[1:]), 106 | "RemoteMachine": socket.gethostname(), 107 | "Environment": environment, 108 | "EnvironmentMerge": "true", 109 | # Currently these are all "dummy" values that we're just setting 110 | # in the default manner that MSVS does it. We could use some of 111 | # these to add additional capabilities, I suppose, but they might 112 | # not have parity with other platforms then. 113 | "Attach": "false", 114 | "DebuggerType": "3", # 'auto' debugger 115 | "Remote": "1", 116 | "RemoteCommand": "", 117 | "HttpUrl": "", 118 | "PDBPath": "", 119 | "SQLDebugging": "", 120 | "DebuggerFlavor": "0", 121 | "MPIRunCommand": "", 122 | "MPIRunArguments": "", 123 | "MPIRunWorkingDirectory": "", 124 | "ApplicationCommand": "", 125 | "ApplicationArguments": "", 126 | "ShimCommand": "", 127 | "MPIAcceptMode": "", 128 | "MPIAcceptFilter": "", 129 | }, 130 | ] 131 | 132 | # Find the config, and add it if it doesn't exist. 133 | if config_name not in self.configurations: 134 | self.AddConfig(config_name) 135 | 136 | # Add the DebugSettings onto the appropriate config. 137 | self.configurations[config_name].append(n_cmd) 138 | 139 | def WriteIfChanged(self): 140 | """Writes the user file.""" 141 | configs = ["Configurations"] 142 | for config, spec in sorted(self.configurations.items()): 143 | configs.append(spec) 144 | 145 | content = [ 146 | "VisualStudioUserFile", 147 | {"Version": self.version.ProjectVersion(), "Name": self.name}, 148 | configs, 149 | ] 150 | easy_xml.WriteXmlIfChanged( 151 | content, self.user_file_path, encoding="Windows-1252" 152 | ) 153 | -------------------------------------------------------------------------------- /pylib/gyp/MSVSUtil.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """Utility functions shared amongst the Windows generators.""" 6 | 7 | import copy 8 | import os 9 | 10 | # A dictionary mapping supported target types to extensions. 11 | TARGET_TYPE_EXT = { 12 | "executable": "exe", 13 | "loadable_module": "dll", 14 | "shared_library": "dll", 15 | "static_library": "lib", 16 | "windows_driver": "sys", 17 | } 18 | 19 | 20 | def _GetLargePdbShimCcPath(): 21 | """Returns the path of the large_pdb_shim.cc file.""" 22 | this_dir = os.path.abspath(os.path.dirname(__file__)) 23 | src_dir = os.path.abspath(os.path.join(this_dir, "..", "..")) 24 | win_data_dir = os.path.join(src_dir, "data", "win") 25 | large_pdb_shim_cc = os.path.join(win_data_dir, "large-pdb-shim.cc") 26 | return large_pdb_shim_cc 27 | 28 | 29 | def _DeepCopySomeKeys(in_dict, keys): 30 | """Performs a partial deep-copy on |in_dict|, only copying the keys in |keys|. 31 | 32 | Arguments: 33 | in_dict: The dictionary to copy. 34 | keys: The keys to be copied. If a key is in this list and doesn't exist in 35 | |in_dict| this is not an error. 36 | Returns: 37 | The partially deep-copied dictionary. 38 | """ 39 | d = {} 40 | for key in keys: 41 | if key not in in_dict: 42 | continue 43 | d[key] = copy.deepcopy(in_dict[key]) 44 | return d 45 | 46 | 47 | def _SuffixName(name, suffix): 48 | """Add a suffix to the end of a target. 49 | 50 | Arguments: 51 | name: name of the target (foo#target) 52 | suffix: the suffix to be added 53 | Returns: 54 | Target name with suffix added (foo_suffix#target) 55 | """ 56 | parts = name.rsplit("#", 1) 57 | parts[0] = f"{parts[0]}_{suffix}" 58 | return "#".join(parts) 59 | 60 | 61 | def _ShardName(name, number): 62 | """Add a shard number to the end of a target. 63 | 64 | Arguments: 65 | name: name of the target (foo#target) 66 | number: shard number 67 | Returns: 68 | Target name with shard added (foo_1#target) 69 | """ 70 | return _SuffixName(name, str(number)) 71 | 72 | 73 | def ShardTargets(target_list, target_dicts): 74 | """Shard some targets apart to work around the linkers limits. 75 | 76 | Arguments: 77 | target_list: List of target pairs: 'base/base.gyp:base'. 78 | target_dicts: Dict of target properties keyed on target pair. 79 | Returns: 80 | Tuple of the new sharded versions of the inputs. 81 | """ 82 | # Gather the targets to shard, and how many pieces. 83 | targets_to_shard = {} 84 | for t in target_dicts: 85 | shards = int(target_dicts[t].get("msvs_shard", 0)) 86 | if shards: 87 | targets_to_shard[t] = shards 88 | # Shard target_list. 89 | new_target_list = [] 90 | for t in target_list: 91 | if t in targets_to_shard: 92 | for i in range(targets_to_shard[t]): 93 | new_target_list.append(_ShardName(t, i)) 94 | else: 95 | new_target_list.append(t) 96 | # Shard target_dict. 97 | new_target_dicts = {} 98 | for t in target_dicts: 99 | if t in targets_to_shard: 100 | for i in range(targets_to_shard[t]): 101 | name = _ShardName(t, i) 102 | new_target_dicts[name] = copy.copy(target_dicts[t]) 103 | new_target_dicts[name]["target_name"] = _ShardName( 104 | new_target_dicts[name]["target_name"], i 105 | ) 106 | sources = new_target_dicts[name].get("sources", []) 107 | new_sources = [] 108 | for pos in range(i, len(sources), targets_to_shard[t]): 109 | new_sources.append(sources[pos]) 110 | new_target_dicts[name]["sources"] = new_sources 111 | else: 112 | new_target_dicts[t] = target_dicts[t] 113 | # Shard dependencies. 114 | for t in sorted(new_target_dicts): 115 | for deptype in ("dependencies", "dependencies_original"): 116 | dependencies = copy.copy(new_target_dicts[t].get(deptype, [])) 117 | new_dependencies = [] 118 | for d in dependencies: 119 | if d in targets_to_shard: 120 | for i in range(targets_to_shard[d]): 121 | new_dependencies.append(_ShardName(d, i)) 122 | else: 123 | new_dependencies.append(d) 124 | new_target_dicts[t][deptype] = new_dependencies 125 | 126 | return (new_target_list, new_target_dicts) 127 | 128 | 129 | def _GetPdbPath(target_dict, config_name, vars): 130 | """Returns the path to the PDB file that will be generated by a given 131 | configuration. 132 | 133 | The lookup proceeds as follows: 134 | - Look for an explicit path in the VCLinkerTool configuration block. 135 | - Look for an 'msvs_large_pdb_path' variable. 136 | - Use '<(PRODUCT_DIR)/<(product_name).(exe|dll).pdb' if 'product_name' is 137 | specified. 138 | - Use '<(PRODUCT_DIR)/<(target_name).(exe|dll).pdb'. 139 | 140 | Arguments: 141 | target_dict: The target dictionary to be searched. 142 | config_name: The name of the configuration of interest. 143 | vars: A dictionary of common GYP variables with generator-specific values. 144 | Returns: 145 | The path of the corresponding PDB file. 146 | """ 147 | config = target_dict["configurations"][config_name] 148 | msvs = config.setdefault("msvs_settings", {}) 149 | 150 | linker = msvs.get("VCLinkerTool", {}) 151 | 152 | pdb_path = linker.get("ProgramDatabaseFile") 153 | if pdb_path: 154 | return pdb_path 155 | 156 | variables = target_dict.get("variables", {}) 157 | pdb_path = variables.get("msvs_large_pdb_path", None) 158 | if pdb_path: 159 | return pdb_path 160 | 161 | pdb_base = target_dict.get("product_name", target_dict["target_name"]) 162 | pdb_base = "{}.{}.pdb".format(pdb_base, TARGET_TYPE_EXT[target_dict["type"]]) 163 | pdb_path = vars["PRODUCT_DIR"] + "/" + pdb_base 164 | 165 | return pdb_path 166 | 167 | 168 | def InsertLargePdbShims(target_list, target_dicts, vars): 169 | """Insert a shim target that forces the linker to use 4KB pagesize PDBs. 170 | 171 | This is a workaround for targets with PDBs greater than 1GB in size, the 172 | limit for the 1KB pagesize PDBs created by the linker by default. 173 | 174 | Arguments: 175 | target_list: List of target pairs: 'base/base.gyp:base'. 176 | target_dicts: Dict of target properties keyed on target pair. 177 | vars: A dictionary of common GYP variables with generator-specific values. 178 | Returns: 179 | Tuple of the shimmed version of the inputs. 180 | """ 181 | # Determine which targets need shimming. 182 | targets_to_shim = [] 183 | for t in target_dicts: 184 | target_dict = target_dicts[t] 185 | 186 | # We only want to shim targets that have msvs_large_pdb enabled. 187 | if not int(target_dict.get("msvs_large_pdb", 0)): 188 | continue 189 | # This is intended for executable, shared_library and loadable_module 190 | # targets where every configuration is set up to produce a PDB output. 191 | # If any of these conditions is not true then the shim logic will fail 192 | # below. 193 | targets_to_shim.append(t) 194 | 195 | large_pdb_shim_cc = _GetLargePdbShimCcPath() 196 | 197 | for t in targets_to_shim: 198 | target_dict = target_dicts[t] 199 | target_name = target_dict.get("target_name") 200 | 201 | base_dict = _DeepCopySomeKeys( 202 | target_dict, ["configurations", "default_configuration", "toolset"] 203 | ) 204 | 205 | # This is the dict for copying the source file (part of the GYP tree) 206 | # to the intermediate directory of the project. This is necessary because 207 | # we can't always build a relative path to the shim source file (on Windows 208 | # GYP and the project may be on different drives), and Ninja hates absolute 209 | # paths (it ends up generating the .obj and .obj.d alongside the source 210 | # file, polluting GYPs tree). 211 | copy_suffix = "large_pdb_copy" 212 | copy_target_name = target_name + "_" + copy_suffix 213 | full_copy_target_name = _SuffixName(t, copy_suffix) 214 | shim_cc_basename = os.path.basename(large_pdb_shim_cc) 215 | shim_cc_dir = vars["SHARED_INTERMEDIATE_DIR"] + "/" + copy_target_name 216 | shim_cc_path = shim_cc_dir + "/" + shim_cc_basename 217 | copy_dict = copy.deepcopy(base_dict) 218 | copy_dict["target_name"] = copy_target_name 219 | copy_dict["type"] = "none" 220 | copy_dict["sources"] = [large_pdb_shim_cc] 221 | copy_dict["copies"] = [ 222 | {"destination": shim_cc_dir, "files": [large_pdb_shim_cc]} 223 | ] 224 | 225 | # This is the dict for the PDB generating shim target. It depends on the 226 | # copy target. 227 | shim_suffix = "large_pdb_shim" 228 | shim_target_name = target_name + "_" + shim_suffix 229 | full_shim_target_name = _SuffixName(t, shim_suffix) 230 | shim_dict = copy.deepcopy(base_dict) 231 | shim_dict["target_name"] = shim_target_name 232 | shim_dict["type"] = "static_library" 233 | shim_dict["sources"] = [shim_cc_path] 234 | shim_dict["dependencies"] = [full_copy_target_name] 235 | 236 | # Set up the shim to output its PDB to the same location as the final linker 237 | # target. 238 | for config_name, config in shim_dict.get("configurations").items(): 239 | pdb_path = _GetPdbPath(target_dict, config_name, vars) 240 | 241 | # A few keys that we don't want to propagate. 242 | for key in ["msvs_precompiled_header", "msvs_precompiled_source", "test"]: 243 | config.pop(key, None) 244 | 245 | msvs = config.setdefault("msvs_settings", {}) 246 | 247 | # Update the compiler directives in the shim target. 248 | compiler = msvs.setdefault("VCCLCompilerTool", {}) 249 | compiler["DebugInformationFormat"] = "3" 250 | compiler["ProgramDataBaseFileName"] = pdb_path 251 | 252 | # Set the explicit PDB path in the appropriate configuration of the 253 | # original target. 254 | config = target_dict["configurations"][config_name] 255 | msvs = config.setdefault("msvs_settings", {}) 256 | linker = msvs.setdefault("VCLinkerTool", {}) 257 | linker["GenerateDebugInformation"] = "true" 258 | linker["ProgramDatabaseFile"] = pdb_path 259 | 260 | # Add the new targets. They must go to the beginning of the list so that 261 | # the dependency generation works as expected in ninja. 262 | target_list.insert(0, full_copy_target_name) 263 | target_list.insert(0, full_shim_target_name) 264 | target_dicts[full_copy_target_name] = copy_dict 265 | target_dicts[full_shim_target_name] = shim_dict 266 | 267 | # Update the original target to depend on the shim target. 268 | target_dict.setdefault("dependencies", []).append(full_shim_target_name) 269 | 270 | return (target_list, target_dicts) 271 | -------------------------------------------------------------------------------- /pylib/gyp/common_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2012 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """Unit tests for the common.py file.""" 8 | 9 | import os 10 | import subprocess 11 | import sys 12 | import unittest 13 | from unittest.mock import MagicMock, patch 14 | 15 | import gyp.common 16 | 17 | 18 | class TestTopologicallySorted(unittest.TestCase): 19 | def test_Valid(self): 20 | """Test that sorting works on a valid graph with one possible order.""" 21 | graph = { 22 | "a": ["b", "c"], 23 | "b": [], 24 | "c": ["d"], 25 | "d": ["b"], 26 | } 27 | 28 | def GetEdge(node): 29 | return tuple(graph[node]) 30 | 31 | assert gyp.common.TopologicallySorted( 32 | graph.keys(), GetEdge) == ["a", "c", "d", "b"] 33 | 34 | def test_Cycle(self): 35 | """Test that an exception is thrown on a cyclic graph.""" 36 | graph = { 37 | "a": ["b"], 38 | "b": ["c"], 39 | "c": ["d"], 40 | "d": ["a"], 41 | } 42 | 43 | def GetEdge(node): 44 | return tuple(graph[node]) 45 | 46 | self.assertRaises( 47 | gyp.common.CycleError, gyp.common.TopologicallySorted, graph.keys(), GetEdge 48 | ) 49 | 50 | 51 | class TestGetFlavor(unittest.TestCase): 52 | """Test that gyp.common.GetFlavor works as intended""" 53 | 54 | original_platform = "" 55 | 56 | def setUp(self): 57 | self.original_platform = sys.platform 58 | 59 | def tearDown(self): 60 | sys.platform = self.original_platform 61 | 62 | def assertFlavor(self, expected, argument, param): 63 | sys.platform = argument 64 | assert expected == gyp.common.GetFlavor(param) 65 | 66 | def test_platform_default(self): 67 | self.assertFlavor("freebsd", "freebsd9", {}) 68 | self.assertFlavor("freebsd", "freebsd10", {}) 69 | self.assertFlavor("openbsd", "openbsd5", {}) 70 | self.assertFlavor("solaris", "sunos5", {}) 71 | self.assertFlavor("solaris", "sunos", {}) 72 | self.assertFlavor("linux", "linux2", {}) 73 | self.assertFlavor("linux", "linux3", {}) 74 | self.assertFlavor("linux", "linux", {}) 75 | 76 | def test_param(self): 77 | self.assertFlavor("foobar", "linux2", {"flavor": "foobar"}) 78 | 79 | class MockCommunicate: 80 | def __init__(self, stdout): 81 | self.stdout = stdout 82 | 83 | def decode(self, encoding): 84 | return self.stdout 85 | 86 | @patch("os.close") 87 | @patch("os.unlink") 88 | @patch("tempfile.mkstemp") 89 | def test_GetCompilerPredefines(self, mock_mkstemp, mock_unlink, mock_close): 90 | mock_close.return_value = None 91 | mock_unlink.return_value = None 92 | mock_mkstemp.return_value = (0, "temp.c") 93 | 94 | def mock_run(env, defines_stdout, expected_cmd, throws=False): 95 | with patch("subprocess.run") as mock_run: 96 | expected_input = "temp.c" if sys.platform == "win32" else "/dev/null" 97 | if throws: 98 | mock_run.side_effect = subprocess.CalledProcessError( 99 | returncode=1, 100 | cmd=[ 101 | *expected_cmd, 102 | "-dM", "-E", "-x", "c", expected_input 103 | ] 104 | ) 105 | else: 106 | mock_process = MagicMock() 107 | mock_process.returncode = 0 108 | mock_process.stdout = TestGetFlavor.MockCommunicate(defines_stdout) 109 | mock_run.return_value = mock_process 110 | with patch.dict(os.environ, env): 111 | try: 112 | defines = gyp.common.GetCompilerPredefines() 113 | except Exception as e: 114 | self.fail(f"GetCompilerPredefines raised an exception: {e}") 115 | flavor = gyp.common.GetFlavor({}) 116 | if env.get("CC_target") or env.get("CC"): 117 | mock_run.assert_called_with( 118 | [ 119 | *expected_cmd, 120 | "-dM", "-E", "-x", "c", expected_input 121 | ], 122 | shell=sys.platform == "win32", 123 | capture_output=True, check=True) 124 | return [defines, flavor] 125 | 126 | [defines0, _] = mock_run({ "CC": "cl.exe" }, "", ["cl.exe"], True) 127 | assert defines0 == {} 128 | 129 | [defines1, _] = mock_run({}, "", []) 130 | assert defines1 == {} 131 | 132 | [defines2, flavor2] = mock_run( 133 | { "CC_target": "/opt/wasi-sdk/bin/clang" }, 134 | "#define __wasm__ 1\n#define __wasi__ 1\n", 135 | ["/opt/wasi-sdk/bin/clang"] 136 | ) 137 | assert defines2 == { "__wasm__": "1", "__wasi__": "1" } 138 | assert flavor2 == "wasi" 139 | 140 | [defines3, flavor3] = mock_run( 141 | { "CC_target": "/opt/wasi-sdk/bin/clang --target=wasm32" }, 142 | "#define __wasm__ 1\n", 143 | ["/opt/wasi-sdk/bin/clang", "--target=wasm32"] 144 | ) 145 | assert defines3 == { "__wasm__": "1" } 146 | assert flavor3 == "wasm" 147 | 148 | [defines4, flavor4] = mock_run( 149 | { "CC_target": "/emsdk/upstream/emscripten/emcc" }, 150 | "#define __EMSCRIPTEN__ 1\n", 151 | ["/emsdk/upstream/emscripten/emcc"] 152 | ) 153 | assert defines4 == { "__EMSCRIPTEN__": "1" } 154 | assert flavor4 == "emscripten" 155 | 156 | # Test path which include white space 157 | [defines5, flavor5] = mock_run( 158 | { 159 | "CC_target": "\"/Users/Toyo Li/wasi-sdk/bin/clang\" -O3", 160 | "CFLAGS": "--target=wasm32-wasi-threads -pthread" 161 | }, 162 | "#define __wasm__ 1\n#define __wasi__ 1\n#define _REENTRANT 1\n", 163 | [ 164 | "/Users/Toyo Li/wasi-sdk/bin/clang", 165 | "-O3", 166 | "--target=wasm32-wasi-threads", 167 | "-pthread" 168 | ] 169 | ) 170 | assert defines5 == { 171 | "__wasm__": "1", 172 | "__wasi__": "1", 173 | "_REENTRANT": "1" 174 | } 175 | assert flavor5 == "wasi" 176 | 177 | original_sep = os.sep 178 | os.sep = "\\" 179 | [defines6, flavor6] = mock_run( 180 | { "CC_target": "\"C:\\Program Files\\wasi-sdk\\clang.exe\"" }, 181 | "#define __wasm__ 1\n#define __wasi__ 1\n", 182 | ["C:/Program Files/wasi-sdk/clang.exe"] 183 | ) 184 | os.sep = original_sep 185 | assert defines6 == { "__wasm__": "1", "__wasi__": "1" } 186 | assert flavor6 == "wasi" 187 | 188 | if __name__ == "__main__": 189 | unittest.main() 190 | -------------------------------------------------------------------------------- /pylib/gyp/easy_xml.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | import locale 6 | import os 7 | import re 8 | import sys 9 | from functools import reduce 10 | 11 | 12 | def XmlToString(content, encoding="utf-8", pretty=False): 13 | """ Writes the XML content to disk, touching the file only if it has changed. 14 | 15 | Visual Studio files have a lot of pre-defined structures. This function makes 16 | it easy to represent these structures as Python data structures, instead of 17 | having to create a lot of function calls. 18 | 19 | Each XML element of the content is represented as a list composed of: 20 | 1. The name of the element, a string, 21 | 2. The attributes of the element, a dictionary (optional), and 22 | 3+. The content of the element, if any. Strings are simple text nodes and 23 | lists are child elements. 24 | 25 | Example 1: 26 | 27 | becomes 28 | ['test'] 29 | 30 | Example 2: 31 | 32 | This is 33 | it! 34 | 35 | 36 | becomes 37 | ['myelement', {'a':'value1', 'b':'value2'}, 38 | ['childtype', 'This is'], 39 | ['childtype', 'it!'], 40 | ] 41 | 42 | Args: 43 | content: The structured content to be converted. 44 | encoding: The encoding to report on the first XML line. 45 | pretty: True if we want pretty printing with indents and new lines. 46 | 47 | Returns: 48 | The XML content as a string. 49 | """ 50 | # We create a huge list of all the elements of the file. 51 | xml_parts = ['' % encoding] 52 | if pretty: 53 | xml_parts.append("\n") 54 | _ConstructContentList(xml_parts, content, pretty) 55 | 56 | # Convert it to a string 57 | return "".join(xml_parts) 58 | 59 | 60 | def _ConstructContentList(xml_parts, specification, pretty, level=0): 61 | """ Appends the XML parts corresponding to the specification. 62 | 63 | Args: 64 | xml_parts: A list of XML parts to be appended to. 65 | specification: The specification of the element. See EasyXml docs. 66 | pretty: True if we want pretty printing with indents and new lines. 67 | level: Indentation level. 68 | """ 69 | # The first item in a specification is the name of the element. 70 | if pretty: 71 | indentation = " " * level 72 | new_line = "\n" 73 | else: 74 | indentation = "" 75 | new_line = "" 76 | name = specification[0] 77 | if not isinstance(name, str): 78 | raise Exception( 79 | "The first item of an EasyXml specification should be " 80 | "a string. Specification was " + str(specification) 81 | ) 82 | xml_parts.append(indentation + "<" + name) 83 | 84 | # Optionally in second position is a dictionary of the attributes. 85 | rest = specification[1:] 86 | if rest and isinstance(rest[0], dict): 87 | for at, val in sorted(rest[0].items()): 88 | xml_parts.append(f' {at}="{_XmlEscape(val, attr=True)}"') 89 | rest = rest[1:] 90 | if rest: 91 | xml_parts.append(">") 92 | all_strings = reduce(lambda x, y: x and isinstance(y, str), rest, True) 93 | multi_line = not all_strings 94 | if multi_line and new_line: 95 | xml_parts.append(new_line) 96 | for child_spec in rest: 97 | # If it's a string, append a text node. 98 | # Otherwise recurse over that child definition 99 | if isinstance(child_spec, str): 100 | xml_parts.append(_XmlEscape(child_spec)) 101 | else: 102 | _ConstructContentList(xml_parts, child_spec, pretty, level + 1) 103 | if multi_line and indentation: 104 | xml_parts.append(indentation) 105 | xml_parts.append(f"{new_line}") 106 | else: 107 | xml_parts.append("/>%s" % new_line) 108 | 109 | 110 | def WriteXmlIfChanged(content, path, encoding="utf-8", pretty=False, 111 | win32=(sys.platform == "win32")): 112 | """ Writes the XML content to disk, touching the file only if it has changed. 113 | 114 | Args: 115 | content: The structured content to be written. 116 | path: Location of the file. 117 | encoding: The encoding to report on the first line of the XML file. 118 | pretty: True if we want pretty printing with indents and new lines. 119 | """ 120 | xml_string = XmlToString(content, encoding, pretty) 121 | if win32 and os.linesep != "\r\n": 122 | xml_string = xml_string.replace("\n", "\r\n") 123 | 124 | try: # getdefaultlocale() was removed in Python 3.11 125 | default_encoding = locale.getdefaultlocale()[1] 126 | except AttributeError: 127 | default_encoding = locale.getencoding() 128 | 129 | if default_encoding and default_encoding.upper() != encoding.upper(): 130 | xml_string = xml_string.encode(encoding) 131 | 132 | # Get the old content 133 | try: 134 | with open(path) as file: 135 | existing = file.read() 136 | except OSError: 137 | existing = None 138 | 139 | # It has changed, write it 140 | if existing != xml_string: 141 | with open(path, "wb") as file: 142 | file.write(xml_string) 143 | 144 | 145 | _xml_escape_map = { 146 | '"': """, 147 | "'": "'", 148 | "<": "<", 149 | ">": ">", 150 | "&": "&", 151 | "\n": " ", 152 | "\r": " ", 153 | } 154 | 155 | 156 | _xml_escape_re = re.compile("(%s)" % "|".join(map(re.escape, _xml_escape_map.keys()))) 157 | 158 | 159 | def _XmlEscape(value, attr=False): 160 | """ Escape a string for inclusion in XML.""" 161 | 162 | def replace(match): 163 | m = match.string[match.start() : match.end()] 164 | # don't replace single quotes in attrs 165 | if attr and m == "'": 166 | return m 167 | return _xml_escape_map[m] 168 | 169 | return _xml_escape_re.sub(replace, value) 170 | -------------------------------------------------------------------------------- /pylib/gyp/easy_xml_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2011 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """ Unit tests for the easy_xml.py file. """ 8 | 9 | import unittest 10 | from io import StringIO 11 | 12 | from gyp import easy_xml 13 | 14 | 15 | class TestSequenceFunctions(unittest.TestCase): 16 | def setUp(self): 17 | self.stderr = StringIO() 18 | 19 | def test_EasyXml_simple(self): 20 | self.assertEqual( 21 | easy_xml.XmlToString(["test"]), 22 | '', 23 | ) 24 | 25 | self.assertEqual( 26 | easy_xml.XmlToString(["test"], encoding="Windows-1252"), 27 | '', 28 | ) 29 | 30 | def test_EasyXml_simple_with_attributes(self): 31 | self.assertEqual( 32 | easy_xml.XmlToString(["test2", {"a": "value1", "b": "value2"}]), 33 | '', 34 | ) 35 | 36 | def test_EasyXml_escaping(self): 37 | original = "'\"\r&\nfoo" 38 | converted = "<test>'" & foo" 39 | converted_apos = converted.replace("'", "'") 40 | self.assertEqual( 41 | easy_xml.XmlToString(["test3", {"a": original}, original]), 42 | '%s' 43 | % (converted, converted_apos), 44 | ) 45 | 46 | def test_EasyXml_pretty(self): 47 | self.assertEqual( 48 | easy_xml.XmlToString( 49 | ["test3", ["GrandParent", ["Parent1", ["Child"]], ["Parent2"]]], 50 | pretty=True, 51 | ), 52 | '\n' 53 | "\n" 54 | " \n" 55 | " \n" 56 | " \n" 57 | " \n" 58 | " \n" 59 | " \n" 60 | "\n", 61 | ) 62 | 63 | def test_EasyXml_complex(self): 64 | # We want to create: 65 | target = ( 66 | '' 67 | "" 68 | '' 69 | "{D2250C20-3A94-4FB9-AF73-11BC5B73884B}" 70 | "Win32Proj" 71 | "automated_ui_tests" 72 | "" 73 | '' 74 | "' 77 | "Application" 78 | "Unicode" 79 | "SpectreLoadCF" 80 | "14.36.32532" 81 | "" 82 | "" 83 | ) 84 | 85 | xml = easy_xml.XmlToString( 86 | [ 87 | "Project", 88 | [ 89 | "PropertyGroup", 90 | {"Label": "Globals"}, 91 | ["ProjectGuid", "{D2250C20-3A94-4FB9-AF73-11BC5B73884B}"], 92 | ["Keyword", "Win32Proj"], 93 | ["RootNamespace", "automated_ui_tests"], 94 | ], 95 | ["Import", {"Project": "$(VCTargetsPath)\\Microsoft.Cpp.props"}], 96 | [ 97 | "PropertyGroup", 98 | { 99 | "Condition": "'$(Configuration)|$(Platform)'=='Debug|Win32'", 100 | "Label": "Configuration", 101 | }, 102 | ["ConfigurationType", "Application"], 103 | ["CharacterSet", "Unicode"], 104 | ["SpectreMitigation", "SpectreLoadCF"], 105 | ["VCToolsVersion", "14.36.32532"], 106 | ], 107 | ] 108 | ) 109 | self.assertEqual(xml, target) 110 | 111 | 112 | if __name__ == "__main__": 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /pylib/gyp/flock_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2011 Google Inc. All rights reserved. 3 | # Use of this source code is governed by a BSD-style license that can be 4 | # found in the LICENSE file. 5 | 6 | """These functions are executed via gyp-flock-tool when using the Makefile 7 | generator. Used on systems that don't have a built-in flock.""" 8 | 9 | import fcntl 10 | import os 11 | import struct 12 | import subprocess 13 | import sys 14 | 15 | 16 | def main(args): 17 | executor = FlockTool() 18 | executor.Dispatch(args) 19 | 20 | 21 | class FlockTool: 22 | """This class emulates the 'flock' command.""" 23 | 24 | def Dispatch(self, args): 25 | """Dispatches a string command to a method.""" 26 | if len(args) < 1: 27 | raise Exception("Not enough arguments") 28 | 29 | method = "Exec%s" % self._CommandifyName(args[0]) 30 | getattr(self, method)(*args[1:]) 31 | 32 | def _CommandifyName(self, name_string): 33 | """Transforms a tool name like copy-info-plist to CopyInfoPlist""" 34 | return name_string.title().replace("-", "") 35 | 36 | def ExecFlock(self, lockfile, *cmd_list): 37 | """Emulates the most basic behavior of Linux's flock(1).""" 38 | # Rely on exception handling to report errors. 39 | # Note that the stock python on SunOS has a bug 40 | # where fcntl.flock(fd, LOCK_EX) always fails 41 | # with EBADF, that's why we use this F_SETLK 42 | # hack instead. 43 | fd = os.open(lockfile, os.O_WRONLY | os.O_NOCTTY | os.O_CREAT, 0o666) 44 | if sys.platform.startswith("aix") or sys.platform == "os400": 45 | # Python on AIX is compiled with LARGEFILE support, which changes the 46 | # struct size. 47 | op = struct.pack("hhIllqq", fcntl.F_WRLCK, 0, 0, 0, 0, 0, 0) 48 | else: 49 | op = struct.pack("hhllhhl", fcntl.F_WRLCK, 0, 0, 0, 0, 0, 0) 50 | fcntl.fcntl(fd, fcntl.F_SETLK, op) 51 | return subprocess.call(cmd_list) 52 | 53 | 54 | if __name__ == "__main__": 55 | sys.exit(main(sys.argv[1:])) 56 | -------------------------------------------------------------------------------- /pylib/gyp/generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodejs/gyp-next/0eaea297f0fbb0869597aa162f66f78eb2468fad/pylib/gyp/generator/__init__.py -------------------------------------------------------------------------------- /pylib/gyp/generator/compile_commands_json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016 Ben Noordhuis . All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | import json 6 | import os 7 | 8 | import gyp.common 9 | import gyp.xcode_emulation 10 | 11 | generator_additional_non_configuration_keys = [] 12 | generator_additional_path_sections = [] 13 | generator_extra_sources_for_rules = [] 14 | generator_filelist_paths = None 15 | generator_supports_multiple_toolsets = True 16 | generator_wants_sorted_dependencies = False 17 | 18 | # Lifted from make.py. The actual values don't matter much. 19 | generator_default_variables = { 20 | "CONFIGURATION_NAME": "$(BUILDTYPE)", 21 | "EXECUTABLE_PREFIX": "", 22 | "EXECUTABLE_SUFFIX": "", 23 | "INTERMEDIATE_DIR": "$(obj).$(TOOLSET)/$(TARGET)/geni", 24 | "PRODUCT_DIR": "$(builddir)", 25 | "RULE_INPUT_DIRNAME": "%(INPUT_DIRNAME)s", 26 | "RULE_INPUT_EXT": "$(suffix $<)", 27 | "RULE_INPUT_NAME": "$(notdir $<)", 28 | "RULE_INPUT_PATH": "$(abspath $<)", 29 | "RULE_INPUT_ROOT": "%(INPUT_ROOT)s", 30 | "SHARED_INTERMEDIATE_DIR": "$(obj)/gen", 31 | "SHARED_LIB_PREFIX": "lib", 32 | "STATIC_LIB_PREFIX": "lib", 33 | "STATIC_LIB_SUFFIX": ".a", 34 | } 35 | 36 | 37 | def IsMac(params): 38 | return gyp.common.GetFlavor(params) == "mac" 39 | 40 | 41 | def CalculateVariables(default_variables, params): 42 | default_variables.setdefault("OS", gyp.common.GetFlavor(params)) 43 | 44 | 45 | def AddCommandsForTarget(cwd, target, params, per_config_commands): 46 | output_dir = params["generator_flags"].get("output_dir", "out") 47 | for configuration_name, configuration in target["configurations"].items(): 48 | if IsMac(params): 49 | xcode_settings = gyp.xcode_emulation.XcodeSettings(target) 50 | cflags = xcode_settings.GetCflags(configuration_name) 51 | cflags_c = xcode_settings.GetCflagsC(configuration_name) 52 | cflags_cc = xcode_settings.GetCflagsCC(configuration_name) 53 | else: 54 | cflags = configuration.get("cflags", []) 55 | cflags_c = configuration.get("cflags_c", []) 56 | cflags_cc = configuration.get("cflags_cc", []) 57 | 58 | cflags_c = cflags + cflags_c 59 | cflags_cc = cflags + cflags_cc 60 | 61 | defines = configuration.get("defines", []) 62 | defines = ["-D" + s for s in defines] 63 | 64 | # TODO(bnoordhuis) Handle generated source files. 65 | extensions = (".c", ".cc", ".cpp", ".cxx") 66 | sources = [s for s in target.get("sources", []) if s.endswith(extensions)] 67 | 68 | def resolve(filename): 69 | return os.path.abspath(os.path.join(cwd, filename)) 70 | 71 | # TODO(bnoordhuis) Handle generated header files. 72 | include_dirs = configuration.get("include_dirs", []) 73 | include_dirs = [s for s in include_dirs if not s.startswith("$(obj)")] 74 | includes = ["-I" + resolve(s) for s in include_dirs] 75 | 76 | defines = gyp.common.EncodePOSIXShellList(defines) 77 | includes = gyp.common.EncodePOSIXShellList(includes) 78 | cflags_c = gyp.common.EncodePOSIXShellList(cflags_c) 79 | cflags_cc = gyp.common.EncodePOSIXShellList(cflags_cc) 80 | 81 | commands = per_config_commands.setdefault(configuration_name, []) 82 | for source in sources: 83 | file = resolve(source) 84 | isc = source.endswith(".c") 85 | cc = "cc" if isc else "c++" 86 | cflags = cflags_c if isc else cflags_cc 87 | command = " ".join( 88 | ( 89 | cc, 90 | defines, 91 | includes, 92 | cflags, 93 | "-c", 94 | gyp.common.EncodePOSIXShellArgument(file), 95 | ) 96 | ) 97 | commands.append({"command": command, "directory": output_dir, "file": file}) 98 | 99 | 100 | def GenerateOutput(target_list, target_dicts, data, params): 101 | per_config_commands = {} 102 | for qualified_target, target in target_dicts.items(): 103 | build_file, target_name, toolset = gyp.common.ParseQualifiedTarget( 104 | qualified_target 105 | ) 106 | if IsMac(params): 107 | settings = data[build_file] 108 | gyp.xcode_emulation.MergeGlobalXcodeSettingsToSpec(settings, target) 109 | cwd = os.path.dirname(build_file) 110 | AddCommandsForTarget(cwd, target, params, per_config_commands) 111 | 112 | output_dir = None 113 | try: 114 | # generator_output can be `None` on Windows machines, or even not 115 | # defined in other cases 116 | output_dir = params.get("options").generator_output 117 | except AttributeError: 118 | pass 119 | output_dir = output_dir or params["generator_flags"].get("output_dir", "out") 120 | for configuration_name, commands in per_config_commands.items(): 121 | filename = os.path.join(output_dir, configuration_name, "compile_commands.json") 122 | gyp.common.EnsureDirExists(filename) 123 | fp = open(filename, "w") 124 | json.dump(commands, fp=fp, indent=0, check_circular=False) 125 | 126 | 127 | def PerformBuild(data, configurations, params): 128 | pass 129 | -------------------------------------------------------------------------------- /pylib/gyp/generator/dump_dependency_json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | 6 | import json 7 | import os 8 | 9 | import gyp 10 | import gyp.common 11 | import gyp.msvs_emulation 12 | 13 | generator_supports_multiple_toolsets = True 14 | 15 | generator_wants_static_library_dependencies_adjusted = False 16 | 17 | generator_filelist_paths = {} 18 | 19 | generator_default_variables = {} 20 | for dirname in [ 21 | "INTERMEDIATE_DIR", 22 | "SHARED_INTERMEDIATE_DIR", 23 | "PRODUCT_DIR", 24 | "LIB_DIR", 25 | "SHARED_LIB_DIR", 26 | ]: 27 | # Some gyp steps fail if these are empty(!). 28 | generator_default_variables[dirname] = "dir" 29 | for unused in [ 30 | "RULE_INPUT_PATH", 31 | "RULE_INPUT_ROOT", 32 | "RULE_INPUT_NAME", 33 | "RULE_INPUT_DIRNAME", 34 | "RULE_INPUT_EXT", 35 | "EXECUTABLE_PREFIX", 36 | "EXECUTABLE_SUFFIX", 37 | "STATIC_LIB_PREFIX", 38 | "STATIC_LIB_SUFFIX", 39 | "SHARED_LIB_PREFIX", 40 | "SHARED_LIB_SUFFIX", 41 | "CONFIGURATION_NAME", 42 | ]: 43 | generator_default_variables[unused] = "" 44 | 45 | 46 | def CalculateVariables(default_variables, params): 47 | generator_flags = params.get("generator_flags", {}) 48 | for key, val in generator_flags.items(): 49 | default_variables.setdefault(key, val) 50 | default_variables.setdefault("OS", gyp.common.GetFlavor(params)) 51 | 52 | flavor = gyp.common.GetFlavor(params) 53 | if flavor == "win": 54 | gyp.msvs_emulation.CalculateCommonVariables(default_variables, params) 55 | 56 | 57 | def CalculateGeneratorInputInfo(params): 58 | """Calculate the generator specific info that gets fed to input (called by 59 | gyp).""" 60 | generator_flags = params.get("generator_flags", {}) 61 | if generator_flags.get("adjust_static_libraries", False): 62 | global generator_wants_static_library_dependencies_adjusted 63 | generator_wants_static_library_dependencies_adjusted = True 64 | 65 | toplevel = params["options"].toplevel_dir 66 | generator_dir = os.path.relpath(params["options"].generator_output or ".") 67 | # output_dir: relative path from generator_dir to the build directory. 68 | output_dir = generator_flags.get("output_dir", "out") 69 | qualified_out_dir = os.path.normpath( 70 | os.path.join(toplevel, generator_dir, output_dir, "gypfiles") 71 | ) 72 | global generator_filelist_paths 73 | generator_filelist_paths = { 74 | "toplevel": toplevel, 75 | "qualified_out_dir": qualified_out_dir, 76 | } 77 | 78 | 79 | def GenerateOutput(target_list, target_dicts, data, params): 80 | # Map of target -> list of targets it depends on. 81 | edges = {} 82 | 83 | # Queue of targets to visit. 84 | targets_to_visit = target_list[:] 85 | 86 | while len(targets_to_visit) > 0: 87 | target = targets_to_visit.pop() 88 | if target in edges: 89 | continue 90 | edges[target] = [] 91 | 92 | for dep in target_dicts[target].get("dependencies", []): 93 | edges[target].append(dep) 94 | targets_to_visit.append(dep) 95 | 96 | try: 97 | filepath = params["generator_flags"]["output_dir"] 98 | except KeyError: 99 | filepath = "." 100 | filename = os.path.join(filepath, "dump.json") 101 | f = open(filename, "w") 102 | json.dump(edges, f) 103 | f.close() 104 | print("Wrote json to %s." % filename) 105 | -------------------------------------------------------------------------------- /pylib/gyp/generator/gypd.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """gypd output module 6 | 7 | This module produces gyp input as its output. Output files are given the 8 | .gypd extension to avoid overwriting the .gyp files that they are generated 9 | from. Internal references to .gyp files (such as those found in 10 | "dependencies" sections) are not adjusted to point to .gypd files instead; 11 | unlike other paths, which are relative to the .gyp or .gypd file, such paths 12 | are relative to the directory from which gyp was run to create the .gypd file. 13 | 14 | This generator module is intended to be a sample and a debugging aid, hence 15 | the "d" for "debug" in .gypd. It is useful to inspect the results of the 16 | various merges, expansions, and conditional evaluations performed by gyp 17 | and to see a representation of what would be fed to a generator module. 18 | 19 | It's not advisable to rename .gypd files produced by this module to .gyp, 20 | because they will have all merges, expansions, and evaluations already 21 | performed and the relevant constructs not present in the output; paths to 22 | dependencies may be wrong; and various sections that do not belong in .gyp 23 | files such as such as "included_files" and "*_excluded" will be present. 24 | Output will also be stripped of comments. This is not intended to be a 25 | general-purpose gyp pretty-printer; for that, you probably just want to 26 | run "pprint.pprint(eval(open('source.gyp').read()))", which will still strip 27 | comments but won't do all of the other things done to this module's output. 28 | 29 | The specific formatting of the output generated by this module is subject 30 | to change. 31 | """ 32 | 33 | 34 | import pprint 35 | 36 | import gyp.common 37 | 38 | # These variables should just be spit back out as variable references. 39 | _generator_identity_variables = [ 40 | "CONFIGURATION_NAME", 41 | "EXECUTABLE_PREFIX", 42 | "EXECUTABLE_SUFFIX", 43 | "INTERMEDIATE_DIR", 44 | "LIB_DIR", 45 | "PRODUCT_DIR", 46 | "RULE_INPUT_ROOT", 47 | "RULE_INPUT_DIRNAME", 48 | "RULE_INPUT_EXT", 49 | "RULE_INPUT_NAME", 50 | "RULE_INPUT_PATH", 51 | "SHARED_INTERMEDIATE_DIR", 52 | "SHARED_LIB_DIR", 53 | "SHARED_LIB_PREFIX", 54 | "SHARED_LIB_SUFFIX", 55 | "STATIC_LIB_PREFIX", 56 | "STATIC_LIB_SUFFIX", 57 | ] 58 | 59 | # gypd doesn't define a default value for OS like many other generator 60 | # modules. Specify "-D OS=whatever" on the command line to provide a value. 61 | generator_default_variables = {} 62 | 63 | # gypd supports multiple toolsets 64 | generator_supports_multiple_toolsets = True 65 | 66 | # TODO(mark): This always uses <, which isn't right. The input module should 67 | # notify the generator to tell it which phase it is operating in, and this 68 | # module should use < for the early phase and then switch to > for the late 69 | # phase. Bonus points for carrying @ back into the output too. 70 | for v in _generator_identity_variables: 71 | generator_default_variables[v] = "<(%s)" % v 72 | 73 | 74 | def GenerateOutput(target_list, target_dicts, data, params): 75 | output_files = {} 76 | for qualified_target in target_list: 77 | [input_file, target] = gyp.common.ParseQualifiedTarget(qualified_target)[0:2] 78 | 79 | if input_file[-4:] != ".gyp": 80 | continue 81 | input_file_stem = input_file[:-4] 82 | output_file = input_file_stem + params["options"].suffix + ".gypd" 83 | 84 | output_files[output_file] = output_files.get(output_file, input_file) 85 | 86 | for output_file, input_file in output_files.items(): 87 | output = open(output_file, "w") 88 | pprint.pprint(data[input_file], output) 89 | output.close() 90 | -------------------------------------------------------------------------------- /pylib/gyp/generator/gypsh.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """gypsh output module 6 | 7 | gypsh is a GYP shell. It's not really a generator per se. All it does is 8 | fire up an interactive Python session with a few local variables set to the 9 | variables passed to the generator. Like gypd, it's intended as a debugging 10 | aid, to facilitate the exploration of .gyp structures after being processed 11 | by the input module. 12 | 13 | The expected usage is "gyp -f gypsh -D OS=desired_os". 14 | """ 15 | 16 | 17 | import code 18 | import sys 19 | 20 | # All of this stuff about generator variables was lovingly ripped from gypd.py. 21 | # That module has a much better description of what's going on and why. 22 | _generator_identity_variables = [ 23 | "EXECUTABLE_PREFIX", 24 | "EXECUTABLE_SUFFIX", 25 | "INTERMEDIATE_DIR", 26 | "PRODUCT_DIR", 27 | "RULE_INPUT_ROOT", 28 | "RULE_INPUT_DIRNAME", 29 | "RULE_INPUT_EXT", 30 | "RULE_INPUT_NAME", 31 | "RULE_INPUT_PATH", 32 | "SHARED_INTERMEDIATE_DIR", 33 | ] 34 | 35 | generator_default_variables = {} 36 | 37 | for v in _generator_identity_variables: 38 | generator_default_variables[v] = "<(%s)" % v 39 | 40 | 41 | def GenerateOutput(target_list, target_dicts, data, params): 42 | locals = { 43 | "target_list": target_list, 44 | "target_dicts": target_dicts, 45 | "data": data, 46 | } 47 | 48 | # Use a banner that looks like the stock Python one and like what 49 | # code.interact uses by default, but tack on something to indicate what 50 | # locals are available, and identify gypsh. 51 | banner = ( 52 | f"Python {sys.version} on {sys.platform}\nlocals.keys() = " 53 | f"{sorted(locals.keys())!r}\ngypsh" 54 | ) 55 | 56 | code.interact(banner, local=locals) 57 | -------------------------------------------------------------------------------- /pylib/gyp/generator/msvs_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2012 Google Inc. All rights reserved. 3 | # Use of this source code is governed by a BSD-style license that can be 4 | # found in the LICENSE file. 5 | 6 | """ Unit tests for the msvs.py file. """ 7 | 8 | import unittest 9 | from io import StringIO 10 | 11 | from gyp.generator import msvs 12 | 13 | 14 | class TestSequenceFunctions(unittest.TestCase): 15 | def setUp(self): 16 | self.stderr = StringIO() 17 | 18 | def test_GetLibraries(self): 19 | self.assertEqual(msvs._GetLibraries({}), []) 20 | self.assertEqual(msvs._GetLibraries({"libraries": []}), []) 21 | self.assertEqual( 22 | msvs._GetLibraries({"other": "foo", "libraries": ["a.lib"]}), ["a.lib"] 23 | ) 24 | self.assertEqual(msvs._GetLibraries({"libraries": ["-la"]}), ["a.lib"]) 25 | self.assertEqual( 26 | msvs._GetLibraries( 27 | { 28 | "libraries": [ 29 | "a.lib", 30 | "b.lib", 31 | "c.lib", 32 | "-lb.lib", 33 | "-lb.lib", 34 | "d.lib", 35 | "a.lib", 36 | ] 37 | } 38 | ), 39 | ["c.lib", "b.lib", "d.lib", "a.lib"], 40 | ) 41 | 42 | 43 | if __name__ == "__main__": 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /pylib/gyp/generator/ninja_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2012 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """ Unit tests for the ninja.py file. """ 8 | 9 | import sys 10 | import unittest 11 | from pathlib import Path 12 | 13 | from gyp.generator import ninja 14 | 15 | 16 | class TestPrefixesAndSuffixes(unittest.TestCase): 17 | def test_BinaryNamesWindows(self): 18 | # These cannot run on non-Windows as they require a VS installation to 19 | # correctly handle variable expansion. 20 | if sys.platform.startswith("win"): 21 | writer = ninja.NinjaWriter( 22 | "foo", "wee", ".", ".", "build.ninja", ".", "build.ninja", "win" 23 | ) 24 | spec = {"target_name": "wee"} 25 | self.assertTrue( 26 | writer.ComputeOutputFileName(spec, "executable").endswith(".exe") 27 | ) 28 | self.assertTrue( 29 | writer.ComputeOutputFileName(spec, "shared_library").endswith(".dll") 30 | ) 31 | self.assertTrue( 32 | writer.ComputeOutputFileName(spec, "static_library").endswith(".lib") 33 | ) 34 | 35 | def test_BinaryNamesLinux(self): 36 | writer = ninja.NinjaWriter( 37 | "foo", "wee", ".", ".", "build.ninja", ".", "build.ninja", "linux" 38 | ) 39 | spec = {"target_name": "wee"} 40 | self.assertTrue("." not in writer.ComputeOutputFileName(spec, "executable")) 41 | self.assertTrue( 42 | writer.ComputeOutputFileName(spec, "shared_library").startswith("lib") 43 | ) 44 | self.assertTrue( 45 | writer.ComputeOutputFileName(spec, "static_library").startswith("lib") 46 | ) 47 | self.assertTrue( 48 | writer.ComputeOutputFileName(spec, "shared_library").endswith(".so") 49 | ) 50 | self.assertTrue( 51 | writer.ComputeOutputFileName(spec, "static_library").endswith(".a") 52 | ) 53 | 54 | def test_GenerateCompileDBWithNinja(self): 55 | build_dir = ( 56 | Path(__file__).resolve().parent.parent.parent.parent / "data" / "ninja" 57 | ) 58 | compile_db = ninja.GenerateCompileDBWithNinja(build_dir) 59 | assert len(compile_db) == 1 60 | assert compile_db[0]["directory"] == str(build_dir) 61 | assert compile_db[0]["command"] == "cc my.in my.out" 62 | assert compile_db[0]["file"] == "my.in" 63 | assert compile_db[0]["output"] == "my.out" 64 | 65 | 66 | if __name__ == "__main__": 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /pylib/gyp/generator/xcode_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2013 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """ Unit tests for the xcode.py file. """ 8 | 9 | import sys 10 | import unittest 11 | 12 | from gyp.generator import xcode 13 | 14 | 15 | class TestEscapeXcodeDefine(unittest.TestCase): 16 | if sys.platform == "darwin": 17 | 18 | def test_InheritedRemainsUnescaped(self): 19 | self.assertEqual(xcode.EscapeXcodeDefine("$(inherited)"), "$(inherited)") 20 | 21 | def test_Escaping(self): 22 | self.assertEqual(xcode.EscapeXcodeDefine('a b"c\\'), 'a\\ b\\"c\\\\') 23 | 24 | 25 | if __name__ == "__main__": 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /pylib/gyp/input_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2013 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """Unit tests for the input.py file.""" 8 | 9 | import unittest 10 | 11 | import gyp.input 12 | 13 | 14 | class TestFindCycles(unittest.TestCase): 15 | def setUp(self): 16 | self.nodes = {} 17 | for x in ("a", "b", "c", "d", "e"): 18 | self.nodes[x] = gyp.input.DependencyGraphNode(x) 19 | 20 | def _create_dependency(self, dependent, dependency): 21 | dependent.dependencies.append(dependency) 22 | dependency.dependents.append(dependent) 23 | 24 | def test_no_cycle_empty_graph(self): 25 | for label, node in self.nodes.items(): 26 | self.assertEqual([], node.FindCycles()) 27 | 28 | def test_no_cycle_line(self): 29 | self._create_dependency(self.nodes["a"], self.nodes["b"]) 30 | self._create_dependency(self.nodes["b"], self.nodes["c"]) 31 | self._create_dependency(self.nodes["c"], self.nodes["d"]) 32 | 33 | for label, node in self.nodes.items(): 34 | self.assertEqual([], node.FindCycles()) 35 | 36 | def test_no_cycle_dag(self): 37 | self._create_dependency(self.nodes["a"], self.nodes["b"]) 38 | self._create_dependency(self.nodes["a"], self.nodes["c"]) 39 | self._create_dependency(self.nodes["b"], self.nodes["c"]) 40 | 41 | for label, node in self.nodes.items(): 42 | self.assertEqual([], node.FindCycles()) 43 | 44 | def test_cycle_self_reference(self): 45 | self._create_dependency(self.nodes["a"], self.nodes["a"]) 46 | 47 | self.assertEqual( 48 | [[self.nodes["a"], self.nodes["a"]]], self.nodes["a"].FindCycles() 49 | ) 50 | 51 | def test_cycle_two_nodes(self): 52 | self._create_dependency(self.nodes["a"], self.nodes["b"]) 53 | self._create_dependency(self.nodes["b"], self.nodes["a"]) 54 | 55 | self.assertEqual( 56 | [[self.nodes["a"], self.nodes["b"], self.nodes["a"]]], 57 | self.nodes["a"].FindCycles(), 58 | ) 59 | self.assertEqual( 60 | [[self.nodes["b"], self.nodes["a"], self.nodes["b"]]], 61 | self.nodes["b"].FindCycles(), 62 | ) 63 | 64 | def test_two_cycles(self): 65 | self._create_dependency(self.nodes["a"], self.nodes["b"]) 66 | self._create_dependency(self.nodes["b"], self.nodes["a"]) 67 | 68 | self._create_dependency(self.nodes["b"], self.nodes["c"]) 69 | self._create_dependency(self.nodes["c"], self.nodes["b"]) 70 | 71 | cycles = self.nodes["a"].FindCycles() 72 | self.assertTrue([self.nodes["a"], self.nodes["b"], self.nodes["a"]] in cycles) 73 | self.assertTrue([self.nodes["b"], self.nodes["c"], self.nodes["b"]] in cycles) 74 | self.assertEqual(2, len(cycles)) 75 | 76 | def test_big_cycle(self): 77 | self._create_dependency(self.nodes["a"], self.nodes["b"]) 78 | self._create_dependency(self.nodes["b"], self.nodes["c"]) 79 | self._create_dependency(self.nodes["c"], self.nodes["d"]) 80 | self._create_dependency(self.nodes["d"], self.nodes["e"]) 81 | self._create_dependency(self.nodes["e"], self.nodes["a"]) 82 | 83 | self.assertEqual( 84 | [ 85 | [ 86 | self.nodes["a"], 87 | self.nodes["b"], 88 | self.nodes["c"], 89 | self.nodes["d"], 90 | self.nodes["e"], 91 | self.nodes["a"], 92 | ] 93 | ], 94 | self.nodes["a"].FindCycles(), 95 | ) 96 | 97 | 98 | if __name__ == "__main__": 99 | unittest.main() 100 | -------------------------------------------------------------------------------- /pylib/gyp/ninja_syntax.py: -------------------------------------------------------------------------------- 1 | # This file comes from 2 | # https://github.com/martine/ninja/blob/master/misc/ninja_syntax.py 3 | # Do not edit! Edit the upstream one instead. 4 | 5 | """Python module for generating .ninja files. 6 | 7 | Note that this is emphatically not a required piece of Ninja; it's 8 | just a helpful utility for build-file-generation systems that already 9 | use Python. 10 | """ 11 | 12 | import textwrap 13 | 14 | 15 | def escape_path(word): 16 | return word.replace("$ ", "$$ ").replace(" ", "$ ").replace(":", "$:") 17 | 18 | 19 | class Writer: 20 | def __init__(self, output, width=78): 21 | self.output = output 22 | self.width = width 23 | 24 | def newline(self): 25 | self.output.write("\n") 26 | 27 | def comment(self, text): 28 | for line in textwrap.wrap(text, self.width - 2): 29 | self.output.write("# " + line + "\n") 30 | 31 | def variable(self, key, value, indent=0): 32 | if value is None: 33 | return 34 | if isinstance(value, list): 35 | value = " ".join(filter(None, value)) # Filter out empty strings. 36 | self._line(f"{key} = {value}", indent) 37 | 38 | def pool(self, name, depth): 39 | self._line("pool %s" % name) 40 | self.variable("depth", depth, indent=1) 41 | 42 | def rule( 43 | self, 44 | name, 45 | command, 46 | description=None, 47 | depfile=None, 48 | generator=False, 49 | pool=None, 50 | restat=False, 51 | rspfile=None, 52 | rspfile_content=None, 53 | deps=None, 54 | ): 55 | self._line("rule %s" % name) 56 | self.variable("command", command, indent=1) 57 | if description: 58 | self.variable("description", description, indent=1) 59 | if depfile: 60 | self.variable("depfile", depfile, indent=1) 61 | if generator: 62 | self.variable("generator", "1", indent=1) 63 | if pool: 64 | self.variable("pool", pool, indent=1) 65 | if restat: 66 | self.variable("restat", "1", indent=1) 67 | if rspfile: 68 | self.variable("rspfile", rspfile, indent=1) 69 | if rspfile_content: 70 | self.variable("rspfile_content", rspfile_content, indent=1) 71 | if deps: 72 | self.variable("deps", deps, indent=1) 73 | 74 | def build( 75 | self, outputs, rule, inputs=None, implicit=None, order_only=None, variables=None 76 | ): 77 | outputs = self._as_list(outputs) 78 | all_inputs = self._as_list(inputs)[:] 79 | out_outputs = list(map(escape_path, outputs)) 80 | all_inputs = list(map(escape_path, all_inputs)) 81 | 82 | if implicit: 83 | implicit = map(escape_path, self._as_list(implicit)) 84 | all_inputs.append("|") 85 | all_inputs.extend(implicit) 86 | if order_only: 87 | order_only = map(escape_path, self._as_list(order_only)) 88 | all_inputs.append("||") 89 | all_inputs.extend(order_only) 90 | 91 | self._line( 92 | "build {}: {}".format(" ".join(out_outputs), " ".join([rule] + all_inputs)) 93 | ) 94 | 95 | if variables: 96 | if isinstance(variables, dict): 97 | iterator = iter(variables.items()) 98 | else: 99 | iterator = iter(variables) 100 | 101 | for key, val in iterator: 102 | self.variable(key, val, indent=1) 103 | 104 | return outputs 105 | 106 | def include(self, path): 107 | self._line("include %s" % path) 108 | 109 | def subninja(self, path): 110 | self._line("subninja %s" % path) 111 | 112 | def default(self, paths): 113 | self._line("default %s" % " ".join(self._as_list(paths))) 114 | 115 | def _count_dollars_before_index(self, s, i): 116 | """Returns the number of '$' characters right in front of s[i].""" 117 | dollar_count = 0 118 | dollar_index = i - 1 119 | while dollar_index > 0 and s[dollar_index] == "$": 120 | dollar_count += 1 121 | dollar_index -= 1 122 | return dollar_count 123 | 124 | def _line(self, text, indent=0): 125 | """Write 'text' word-wrapped at self.width characters.""" 126 | leading_space = " " * indent 127 | while len(leading_space) + len(text) > self.width: 128 | # The text is too wide; wrap if possible. 129 | 130 | # Find the rightmost space that would obey our width constraint and 131 | # that's not an escaped space. 132 | available_space = self.width - len(leading_space) - len(" $") 133 | space = available_space 134 | while True: 135 | space = text.rfind(" ", 0, space) 136 | if space < 0 or self._count_dollars_before_index(text, space) % 2 == 0: 137 | break 138 | 139 | if space < 0: 140 | # No such space; just use the first unescaped space we can find. 141 | space = available_space - 1 142 | while True: 143 | space = text.find(" ", space + 1) 144 | if ( 145 | space < 0 146 | or self._count_dollars_before_index(text, space) % 2 == 0 147 | ): 148 | break 149 | if space < 0: 150 | # Give up on breaking. 151 | break 152 | 153 | self.output.write(leading_space + text[0:space] + " $\n") 154 | text = text[space + 1 :] 155 | 156 | # Subsequent lines are continuations, so indent them. 157 | leading_space = " " * (indent + 2) 158 | 159 | self.output.write(leading_space + text + "\n") 160 | 161 | def _as_list(self, input): 162 | if input is None: 163 | return [] 164 | if isinstance(input, list): 165 | return input 166 | return [input] 167 | 168 | 169 | def escape(string): 170 | """Escape a string such that it can be embedded into a Ninja file without 171 | further interpretation.""" 172 | assert "\n" not in string, "Ninja syntax does not allow newlines" 173 | # We only have one special metacharacter: '$'. 174 | return string.replace("$", "$$") 175 | -------------------------------------------------------------------------------- /pylib/gyp/simple_copy.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """A clone of the default copy.deepcopy that doesn't handle cyclic 6 | structures or complex types except for dicts and lists. This is 7 | because gyp copies so large structure that small copy overhead ends up 8 | taking seconds in a project the size of Chromium.""" 9 | 10 | 11 | class Error(Exception): 12 | pass 13 | 14 | 15 | __all__ = ["Error", "deepcopy"] 16 | 17 | 18 | def deepcopy(x): 19 | """Deep copy operation on gyp objects such as strings, ints, dicts 20 | and lists. More than twice as fast as copy.deepcopy but much less 21 | generic.""" 22 | 23 | try: 24 | return _deepcopy_dispatch[type(x)](x) 25 | except KeyError: 26 | raise Error( 27 | "Unsupported type %s for deepcopy. Use copy.deepcopy " 28 | + "or expand simple_copy support." % type(x) 29 | ) 30 | 31 | 32 | _deepcopy_dispatch = d = {} 33 | 34 | 35 | def _deepcopy_atomic(x): 36 | return x 37 | 38 | 39 | types = bool, float, int, str, type, type(None) 40 | 41 | for x in types: 42 | d[x] = _deepcopy_atomic 43 | 44 | 45 | def _deepcopy_list(x): 46 | return [deepcopy(a) for a in x] 47 | 48 | 49 | d[list] = _deepcopy_list 50 | 51 | 52 | def _deepcopy_dict(x): 53 | y = {} 54 | for key, value in x.items(): 55 | y[deepcopy(key)] = deepcopy(value) 56 | return y 57 | 58 | 59 | d[dict] = _deepcopy_dict 60 | 61 | del d 62 | -------------------------------------------------------------------------------- /pylib/gyp/xcode_emulation_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Unit tests for the xcode_emulation.py file.""" 4 | 5 | import sys 6 | import unittest 7 | 8 | from gyp.xcode_emulation import XcodeSettings 9 | 10 | 11 | class TestXcodeSettings(unittest.TestCase): 12 | def setUp(self): 13 | if sys.platform != "darwin": 14 | self.skipTest("This test only runs on macOS") 15 | 16 | def test_GetCflags(self): 17 | target = { 18 | "type": "static_library", 19 | "configurations": { 20 | "Release": {}, 21 | }, 22 | } 23 | configuration_name = "Release" 24 | xcode_settings = XcodeSettings(target) 25 | cflags = xcode_settings.GetCflags(configuration_name, "arm64") 26 | 27 | # Do not quote `-arch arm64` with spaces in one string. 28 | self.assertEqual( 29 | cflags, 30 | ["-fasm-blocks", "-mpascal-strings", "-Os", "-gdwarf-2", "-arch", "arm64"], 31 | ) 32 | 33 | def GypToBuildPath(self, path): 34 | return path 35 | 36 | def test_GetLdflags(self): 37 | target = { 38 | "type": "static_library", 39 | "configurations": { 40 | "Release": {}, 41 | }, 42 | } 43 | configuration_name = "Release" 44 | xcode_settings = XcodeSettings(target) 45 | ldflags = xcode_settings.GetLdflags( 46 | configuration_name, "PRODUCT_DIR", self.GypToBuildPath, "arm64" 47 | ) 48 | 49 | # Do not quote `-arch arm64` with spaces in one string. 50 | self.assertEqual(ldflags, ["-arch", "arm64", "-LPRODUCT_DIR"]) 51 | 52 | 53 | if __name__ == "__main__": 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /pylib/gyp/xml_fix.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011 Google Inc. All rights reserved. 2 | # Use of this source code is governed by a BSD-style license that can be 3 | # found in the LICENSE file. 4 | 5 | """Applies a fix to CR LF TAB handling in xml.dom. 6 | 7 | Fixes this: http://code.google.com/p/chromium/issues/detail?id=76293 8 | Working around this: http://bugs.python.org/issue5752 9 | TODO(bradnelson): Consider dropping this when we drop XP support. 10 | """ 11 | 12 | 13 | import xml.dom.minidom 14 | 15 | 16 | def _Replacement_write_data(writer, data, is_attrib=False): 17 | """Writes datachars to writer.""" 18 | data = data.replace("&", "&").replace("<", "<") 19 | data = data.replace('"', """).replace(">", ">") 20 | if is_attrib: 21 | data = data.replace("\r", " ").replace("\n", " ").replace("\t", " ") 22 | writer.write(data) 23 | 24 | 25 | def _Replacement_writexml(self, writer, indent="", addindent="", newl=""): 26 | # indent = current indentation 27 | # addindent = indentation to add to higher levels 28 | # newl = newline string 29 | writer.write(indent + "<" + self.tagName) 30 | 31 | attrs = self._get_attributes() 32 | a_names = sorted(attrs.keys()) 33 | 34 | for a_name in a_names: 35 | writer.write(' %s="' % a_name) 36 | _Replacement_write_data(writer, attrs[a_name].value, is_attrib=True) 37 | writer.write('"') 38 | if self.childNodes: 39 | writer.write(">%s" % newl) 40 | for node in self.childNodes: 41 | node.writexml(writer, indent + addindent, addindent, newl) 42 | writer.write(f"{indent}{newl}") 43 | else: 44 | writer.write("/>%s" % newl) 45 | 46 | 47 | class XmlFix: 48 | """Object to manage temporary patching of xml.dom.minidom.""" 49 | 50 | def __init__(self): 51 | # Preserve current xml.dom.minidom functions. 52 | self.write_data = xml.dom.minidom._write_data 53 | self.writexml = xml.dom.minidom.Element.writexml 54 | # Inject replacement versions of a function and a method. 55 | xml.dom.minidom._write_data = _Replacement_write_data 56 | xml.dom.minidom.Element.writexml = _Replacement_writexml 57 | 58 | def Cleanup(self): 59 | if self.write_data: 60 | xml.dom.minidom._write_data = self.write_data 61 | xml.dom.minidom.Element.writexml = self.writexml 62 | self.write_data = None 63 | 64 | def __del__(self): 65 | self.Cleanup() 66 | -------------------------------------------------------------------------------- /pylib/packaging/LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the licenses 2 | found in LICENSE.APACHE or LICENSE.BSD. Contributions to this software is made 3 | under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /pylib/packaging/LICENSE.APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /pylib/packaging/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) Donald Stufft and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /pylib/packaging/__init__.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | __title__ = "packaging" 6 | __summary__ = "Core utilities for Python packages" 7 | __uri__ = "https://github.com/pypa/packaging" 8 | 9 | __version__ = "23.3.dev0" 10 | 11 | __author__ = "Donald Stufft and individual contributors" 12 | __email__ = "donald@stufft.io" 13 | 14 | __license__ = "BSD-2-Clause or Apache-2.0" 15 | __copyright__ = "2014 %s" % __author__ 16 | -------------------------------------------------------------------------------- /pylib/packaging/_elffile.py: -------------------------------------------------------------------------------- 1 | """ 2 | ELF file parser. 3 | 4 | This provides a class ``ELFFile`` that parses an ELF executable in a similar 5 | interface to ``ZipFile``. Only the read interface is implemented. 6 | 7 | Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca 8 | ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html 9 | """ 10 | 11 | import enum 12 | import os 13 | import struct 14 | from typing import IO, Optional, Tuple 15 | 16 | 17 | class ELFInvalid(ValueError): 18 | pass 19 | 20 | 21 | class EIClass(enum.IntEnum): 22 | C32 = 1 23 | C64 = 2 24 | 25 | 26 | class EIData(enum.IntEnum): 27 | Lsb = 1 28 | Msb = 2 29 | 30 | 31 | class EMachine(enum.IntEnum): 32 | I386 = 3 33 | S390 = 22 34 | Arm = 40 35 | X8664 = 62 36 | AArc64 = 183 37 | 38 | 39 | class ELFFile: 40 | """ 41 | Representation of an ELF executable. 42 | """ 43 | 44 | def __init__(self, f: IO[bytes]) -> None: 45 | self._f = f 46 | 47 | try: 48 | ident = self._read("16B") 49 | except struct.error: 50 | raise ELFInvalid("unable to parse identification") 51 | if (magic := bytes(ident[:4])) != b"\x7fELF": 52 | raise ELFInvalid(f"invalid magic: {magic!r}") 53 | 54 | self.capacity = ident[4] # Format for program header (bitness). 55 | self.encoding = ident[5] # Data structure encoding (endianness). 56 | 57 | try: 58 | # e_fmt: Format for program header. 59 | # p_fmt: Format for section header. 60 | # p_idx: Indexes to find p_type, p_offset, and p_filesz. 61 | e_fmt, self._p_fmt, self._p_idx = { 62 | (1, 1): ("HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. 64 | (2, 1): ("HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. 66 | }[(self.capacity, self.encoding)] 67 | except KeyError: 68 | raise ELFInvalid( 69 | f"unrecognized capacity ({self.capacity}) or " 70 | f"encoding ({self.encoding})" 71 | ) 72 | 73 | try: 74 | ( 75 | _, 76 | self.machine, # Architecture type. 77 | _, 78 | _, 79 | self._e_phoff, # Offset of program header. 80 | _, 81 | self.flags, # Processor-specific flags. 82 | _, 83 | self._e_phentsize, # Size of section. 84 | self._e_phnum, # Number of sections. 85 | ) = self._read(e_fmt) 86 | except struct.error as e: 87 | raise ELFInvalid("unable to parse machine and section information") from e 88 | 89 | def _read(self, fmt: str) -> Tuple[int, ...]: 90 | return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) 91 | 92 | @property 93 | def interpreter(self) -> Optional[str]: 94 | """ 95 | The path recorded in the ``PT_INTERP`` section header. 96 | """ 97 | for index in range(self._e_phnum): 98 | self._f.seek(self._e_phoff + self._e_phentsize * index) 99 | try: 100 | data = self._read(self._p_fmt) 101 | except struct.error: 102 | continue 103 | if data[self._p_idx[0]] != 3: # Not PT_INTERP. 104 | continue 105 | self._f.seek(data[self._p_idx[1]]) 106 | return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") 107 | return None 108 | -------------------------------------------------------------------------------- /pylib/packaging/_manylinux.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import contextlib 3 | import functools 4 | import os 5 | import re 6 | import sys 7 | import warnings 8 | from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple 9 | 10 | from ._elffile import EIClass, EIData, ELFFile, EMachine 11 | 12 | EF_ARM_ABIMASK = 0xFF000000 13 | EF_ARM_ABI_VER5 = 0x05000000 14 | EF_ARM_ABI_FLOAT_HARD = 0x00000400 15 | 16 | 17 | # `os.PathLike` not a generic type until Python 3.9, so sticking with `str` 18 | # as the type for `path` until then. 19 | @contextlib.contextmanager 20 | def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: 21 | try: 22 | with open(path, "rb") as f: 23 | yield ELFFile(f) 24 | except (OSError, TypeError, ValueError): 25 | yield None 26 | 27 | 28 | def _is_linux_armhf(executable: str) -> bool: 29 | # hard-float ABI can be detected from the ELF header of the running 30 | # process 31 | # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf 32 | with _parse_elf(executable) as f: 33 | return ( 34 | f is not None 35 | and f.capacity == EIClass.C32 36 | and f.encoding == EIData.Lsb 37 | and f.machine == EMachine.Arm 38 | and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 39 | and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD 40 | ) 41 | 42 | 43 | def _is_linux_i686(executable: str) -> bool: 44 | with _parse_elf(executable) as f: 45 | return ( 46 | f is not None 47 | and f.capacity == EIClass.C32 48 | and f.encoding == EIData.Lsb 49 | and f.machine == EMachine.I386 50 | ) 51 | 52 | 53 | def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: 54 | if "armv7l" in archs: 55 | return _is_linux_armhf(executable) 56 | if "i686" in archs: 57 | return _is_linux_i686(executable) 58 | allowed_archs = {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x", "loongarch64"} 59 | return any(arch in allowed_archs for arch in archs) 60 | 61 | 62 | # If glibc ever changes its major version, we need to know what the last 63 | # minor version was, so we can build the complete list of all versions. 64 | # For now, guess what the highest minor version might be, assume it will 65 | # be 50 for testing. Once this actually happens, update the dictionary 66 | # with the actual value. 67 | _LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) 68 | 69 | 70 | class _GLibCVersion(NamedTuple): 71 | major: int 72 | minor: int 73 | 74 | 75 | def _glibc_version_string_confstr() -> Optional[str]: 76 | """ 77 | Primary implementation of glibc_version_string using os.confstr. 78 | """ 79 | # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely 80 | # to be broken or missing. This strategy is used in the standard library 81 | # platform module. 82 | # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 83 | try: 84 | # Should be a string like "glibc 2.17". 85 | version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION") 86 | assert version_string is not None 87 | _, version = version_string.rsplit() 88 | except (AssertionError, AttributeError, OSError, ValueError): 89 | # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... 90 | return None 91 | return version 92 | 93 | 94 | def _glibc_version_string_ctypes() -> Optional[str]: 95 | """ 96 | Fallback implementation of glibc_version_string using ctypes. 97 | """ 98 | try: 99 | import ctypes 100 | except ImportError: 101 | return None 102 | 103 | # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen 104 | # manpage says, "If filename is NULL, then the returned handle is for the 105 | # main program". This way we can let the linker do the work to figure out 106 | # which libc our process is actually using. 107 | # 108 | # We must also handle the special case where the executable is not a 109 | # dynamically linked executable. This can occur when using musl libc, 110 | # for example. In this situation, dlopen() will error, leading to an 111 | # OSError. Interestingly, at least in the case of musl, there is no 112 | # errno set on the OSError. The single string argument used to construct 113 | # OSError comes from libc itself and is therefore not portable to 114 | # hard code here. In any case, failure to call dlopen() means we 115 | # can proceed, so we bail on our attempt. 116 | try: 117 | process_namespace = ctypes.CDLL(None) 118 | except OSError: 119 | return None 120 | 121 | try: 122 | gnu_get_libc_version = process_namespace.gnu_get_libc_version 123 | except AttributeError: 124 | # Symbol doesn't exist -> therefore, we are not linked to 125 | # glibc. 126 | return None 127 | 128 | # Call gnu_get_libc_version, which returns a string like "2.5" 129 | gnu_get_libc_version.restype = ctypes.c_char_p 130 | version_str: str = gnu_get_libc_version() 131 | # py2 / py3 compatibility: 132 | if not isinstance(version_str, str): 133 | version_str = version_str.decode("ascii") 134 | 135 | return version_str 136 | 137 | 138 | def _glibc_version_string() -> Optional[str]: 139 | """Returns glibc version string, or None if not using glibc.""" 140 | return _glibc_version_string_confstr() or _glibc_version_string_ctypes() 141 | 142 | 143 | def _parse_glibc_version(version_str: str) -> Tuple[int, int]: 144 | """Parse glibc version. 145 | 146 | We use a regexp instead of str.split because we want to discard any 147 | random junk that might come after the minor version -- this might happen 148 | in patched/forked versions of glibc (e.g. Linaro's version of glibc 149 | uses version strings like "2.20-2014.11"). See gh-3588. 150 | """ 151 | m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) 152 | if not m: 153 | warnings.warn( 154 | f"Expected glibc version with 2 components major.minor," 155 | f" got: {version_str}", 156 | RuntimeWarning, 157 | ) 158 | return -1, -1 159 | return int(m.group("major")), int(m.group("minor")) 160 | 161 | 162 | @functools.lru_cache() 163 | def _get_glibc_version() -> Tuple[int, int]: 164 | version_str = _glibc_version_string() 165 | if version_str is None: 166 | return (-1, -1) 167 | return _parse_glibc_version(version_str) 168 | 169 | 170 | # From PEP 513, PEP 600 171 | def _is_compatible(arch: str, version: _GLibCVersion) -> bool: 172 | sys_glibc = _get_glibc_version() 173 | if sys_glibc < version: 174 | return False 175 | # Check for presence of _manylinux module. 176 | try: 177 | import _manylinux # noqa 178 | except ImportError: 179 | return True 180 | if hasattr(_manylinux, "manylinux_compatible"): 181 | result = _manylinux.manylinux_compatible(version[0], version[1], arch) 182 | if result is not None: 183 | return bool(result) 184 | return True 185 | if version == _GLibCVersion(2, 5): 186 | if hasattr(_manylinux, "manylinux1_compatible"): 187 | return bool(_manylinux.manylinux1_compatible) 188 | if version == _GLibCVersion(2, 12): 189 | if hasattr(_manylinux, "manylinux2010_compatible"): 190 | return bool(_manylinux.manylinux2010_compatible) 191 | if version == _GLibCVersion(2, 17): 192 | if hasattr(_manylinux, "manylinux2014_compatible"): 193 | return bool(_manylinux.manylinux2014_compatible) 194 | return True 195 | 196 | 197 | _LEGACY_MANYLINUX_MAP = { 198 | # CentOS 7 w/ glibc 2.17 (PEP 599) 199 | (2, 17): "manylinux2014", 200 | # CentOS 6 w/ glibc 2.12 (PEP 571) 201 | (2, 12): "manylinux2010", 202 | # CentOS 5 w/ glibc 2.5 (PEP 513) 203 | (2, 5): "manylinux1", 204 | } 205 | 206 | 207 | def platform_tags(archs: Sequence[str]) -> Iterator[str]: 208 | """Generate manylinux tags compatible to the current platform. 209 | 210 | :param archs: Sequence of compatible architectures. 211 | The first one shall be the closest to the actual architecture and be the part of 212 | platform tag after the ``linux_`` prefix, e.g. ``x86_64``. 213 | The ``linux_`` prefix is assumed as a prerequisite for the current platform to 214 | be manylinux-compatible. 215 | 216 | :returns: An iterator of compatible manylinux tags. 217 | """ 218 | if not _have_compatible_abi(sys.executable, archs): 219 | return 220 | # Oldest glibc to be supported regardless of architecture is (2, 17). 221 | too_old_glibc2 = _GLibCVersion(2, 16) 222 | if set(archs) & {"x86_64", "i686"}: 223 | # On x86/i686 also oldest glibc to be supported is (2, 5). 224 | too_old_glibc2 = _GLibCVersion(2, 4) 225 | current_glibc = _GLibCVersion(*_get_glibc_version()) 226 | glibc_max_list = [current_glibc] 227 | # We can assume compatibility across glibc major versions. 228 | # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 229 | # 230 | # Build a list of maximum glibc versions so that we can 231 | # output the canonical list of all glibc from current_glibc 232 | # down to too_old_glibc2, including all intermediary versions. 233 | for glibc_major in range(current_glibc.major - 1, 1, -1): 234 | glibc_minor = _LAST_GLIBC_MINOR[glibc_major] 235 | glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) 236 | for arch in archs: 237 | for glibc_max in glibc_max_list: 238 | if glibc_max.major == too_old_glibc2.major: 239 | min_minor = too_old_glibc2.minor 240 | else: 241 | # For other glibc major versions oldest supported is (x, 0). 242 | min_minor = -1 243 | for glibc_minor in range(glibc_max.minor, min_minor, -1): 244 | glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) 245 | tag = "manylinux_{}_{}".format(*glibc_version) 246 | if _is_compatible(arch, glibc_version): 247 | yield f"{tag}_{arch}" 248 | # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. 249 | if glibc_version in _LEGACY_MANYLINUX_MAP: 250 | legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] 251 | if _is_compatible(arch, glibc_version): 252 | yield f"{legacy_tag}_{arch}" 253 | -------------------------------------------------------------------------------- /pylib/packaging/_musllinux.py: -------------------------------------------------------------------------------- 1 | """PEP 656 support. 2 | 3 | This module implements logic to detect if the currently running Python is 4 | linked against musl, and what musl version is used. 5 | """ 6 | 7 | import functools 8 | import re 9 | import subprocess 10 | import sys 11 | from typing import Iterator, NamedTuple, Optional, Sequence 12 | 13 | from ._elffile import ELFFile 14 | 15 | 16 | class _MuslVersion(NamedTuple): 17 | major: int 18 | minor: int 19 | 20 | 21 | def _parse_musl_version(output: str) -> Optional[_MuslVersion]: 22 | lines = [n for n in (n.strip() for n in output.splitlines()) if n] 23 | if len(lines) < 2 or lines[0][:4] != "musl": 24 | return None 25 | m = re.match(r"Version (\d+)\.(\d+)", lines[1]) 26 | if not m: 27 | return None 28 | return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) 29 | 30 | 31 | @functools.lru_cache() 32 | def _get_musl_version(executable: str) -> Optional[_MuslVersion]: 33 | """Detect currently-running musl runtime version. 34 | 35 | This is done by checking the specified executable's dynamic linking 36 | information, and invoking the loader to parse its output for a version 37 | string. If the loader is musl, the output would be something like:: 38 | 39 | musl libc (x86_64) 40 | Version 1.2.2 41 | Dynamic Program Loader 42 | """ 43 | try: 44 | with open(executable, "rb") as f: 45 | ld = ELFFile(f).interpreter 46 | except (OSError, TypeError, ValueError): 47 | return None 48 | if ld is None or "musl" not in ld: 49 | return None 50 | proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) 51 | return _parse_musl_version(proc.stderr) 52 | 53 | 54 | def platform_tags(archs: Sequence[str]) -> Iterator[str]: 55 | """Generate musllinux tags compatible to the current platform. 56 | 57 | :param archs: Sequence of compatible architectures. 58 | The first one shall be the closest to the actual architecture and be the part of 59 | platform tag after the ``linux_`` prefix, e.g. ``x86_64``. 60 | The ``linux_`` prefix is assumed as a prerequisite for the current platform to 61 | be musllinux-compatible. 62 | 63 | :returns: An iterator of compatible musllinux tags. 64 | """ 65 | sys_musl = _get_musl_version(sys.executable) 66 | if sys_musl is None: # Python not dynamically linked against musl. 67 | return 68 | for arch in archs: 69 | for minor in range(sys_musl.minor, -1, -1): 70 | yield f"musllinux_{sys_musl.major}_{minor}_{arch}" 71 | 72 | 73 | if __name__ == "__main__": # pragma: no cover 74 | import sysconfig 75 | 76 | plat = sysconfig.get_platform() 77 | assert plat.startswith("linux-"), "not linux" 78 | 79 | print("plat:", plat) 80 | print("musl:", _get_musl_version(sys.executable)) 81 | print("tags:", end=" ") 82 | for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): 83 | print(t, end="\n ") 84 | -------------------------------------------------------------------------------- /pylib/packaging/_structures.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | 6 | class InfinityType: 7 | def __repr__(self) -> str: 8 | return "Infinity" 9 | 10 | def __hash__(self) -> int: 11 | return hash(repr(self)) 12 | 13 | def __lt__(self, other: object) -> bool: 14 | return False 15 | 16 | def __le__(self, other: object) -> bool: 17 | return False 18 | 19 | def __eq__(self, other: object) -> bool: 20 | return isinstance(other, self.__class__) 21 | 22 | def __gt__(self, other: object) -> bool: 23 | return True 24 | 25 | def __ge__(self, other: object) -> bool: 26 | return True 27 | 28 | def __neg__(self: object) -> "NegativeInfinityType": 29 | return NegativeInfinity 30 | 31 | 32 | Infinity = InfinityType() 33 | 34 | 35 | class NegativeInfinityType: 36 | def __repr__(self) -> str: 37 | return "-Infinity" 38 | 39 | def __hash__(self) -> int: 40 | return hash(repr(self)) 41 | 42 | def __lt__(self, other: object) -> bool: 43 | return True 44 | 45 | def __le__(self, other: object) -> bool: 46 | return True 47 | 48 | def __eq__(self, other: object) -> bool: 49 | return isinstance(other, self.__class__) 50 | 51 | def __gt__(self, other: object) -> bool: 52 | return False 53 | 54 | def __ge__(self, other: object) -> bool: 55 | return False 56 | 57 | def __neg__(self: object) -> InfinityType: 58 | return Infinity 59 | 60 | 61 | NegativeInfinity = NegativeInfinityType() 62 | -------------------------------------------------------------------------------- /pylib/packaging/_tokenizer.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import re 3 | from dataclasses import dataclass 4 | from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union 5 | 6 | from .specifiers import Specifier 7 | 8 | 9 | @dataclass 10 | class Token: 11 | name: str 12 | text: str 13 | position: int 14 | 15 | 16 | class ParserSyntaxError(Exception): 17 | """The provided source text could not be parsed correctly.""" 18 | 19 | def __init__( 20 | self, 21 | message: str, 22 | *, 23 | source: str, 24 | span: Tuple[int, int], 25 | ) -> None: 26 | self.span = span 27 | self.message = message 28 | self.source = source 29 | 30 | super().__init__() 31 | 32 | def __str__(self) -> str: 33 | marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" 34 | return "\n ".join([self.message, self.source, marker]) 35 | 36 | 37 | DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { 38 | "LEFT_PARENTHESIS": r"\(", 39 | "RIGHT_PARENTHESIS": r"\)", 40 | "LEFT_BRACKET": r"\[", 41 | "RIGHT_BRACKET": r"\]", 42 | "SEMICOLON": r";", 43 | "COMMA": r",", 44 | "QUOTED_STRING": re.compile( 45 | r""" 46 | ( 47 | ('[^']*') 48 | | 49 | ("[^"]*") 50 | ) 51 | """, 52 | re.VERBOSE, 53 | ), 54 | "OP": r"(===|==|~=|!=|<=|>=|<|>)", 55 | "BOOLOP": r"\b(or|and)\b", 56 | "IN": r"\bin\b", 57 | "NOT": r"\bnot\b", 58 | "VARIABLE": re.compile( 59 | r""" 60 | \b( 61 | python_version 62 | |python_full_version 63 | |os[._]name 64 | |sys[._]platform 65 | |platform_(release|system) 66 | |platform[._](version|machine|python_implementation) 67 | |python_implementation 68 | |implementation_(name|version) 69 | |extra 70 | )\b 71 | """, 72 | re.VERBOSE, 73 | ), 74 | "SPECIFIER": re.compile( 75 | Specifier._operator_regex_str + Specifier._version_regex_str, 76 | re.VERBOSE | re.IGNORECASE, 77 | ), 78 | "AT": r"\@", 79 | "URL": r"[^ \t]+", 80 | "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", 81 | "VERSION_PREFIX_TRAIL": r"\.\*", 82 | "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", 83 | "WS": r"[ \t]+", 84 | "END": r"$", 85 | } 86 | 87 | 88 | class Tokenizer: 89 | """Context-sensitive token parsing. 90 | 91 | Provides methods to examine the input stream to check whether the next token 92 | matches. 93 | """ 94 | 95 | def __init__( 96 | self, 97 | source: str, 98 | *, 99 | rules: "Dict[str, Union[str, re.Pattern[str]]]", 100 | ) -> None: 101 | self.source = source 102 | self.rules: Dict[str, re.Pattern[str]] = { 103 | name: re.compile(pattern) for name, pattern in rules.items() 104 | } 105 | self.next_token: Optional[Token] = None 106 | self.position = 0 107 | 108 | def consume(self, name: str) -> None: 109 | """Move beyond provided token name, if at current position.""" 110 | if self.check(name): 111 | self.read() 112 | 113 | def check(self, name: str, *, peek: bool = False) -> bool: 114 | """Check whether the next token has the provided name. 115 | 116 | By default, if the check succeeds, the token *must* be read before 117 | another check. If `peek` is set to `True`, the token is not loaded and 118 | would need to be checked again. 119 | """ 120 | assert ( 121 | self.next_token is None 122 | ), f"Cannot check for {name!r}, already have {self.next_token!r}" 123 | assert name in self.rules, f"Unknown token name: {name!r}" 124 | 125 | expression = self.rules[name] 126 | 127 | match = expression.match(self.source, self.position) 128 | if match is None: 129 | return False 130 | if not peek: 131 | self.next_token = Token(name, match[0], self.position) 132 | return True 133 | 134 | def expect(self, name: str, *, expected: str) -> Token: 135 | """Expect a certain token name next, failing with a syntax error otherwise. 136 | 137 | The token is *not* read. 138 | """ 139 | if not self.check(name): 140 | raise self.raise_syntax_error(f"Expected {expected}") 141 | return self.read() 142 | 143 | def read(self) -> Token: 144 | """Consume the next token and return it.""" 145 | token = self.next_token 146 | assert token is not None 147 | 148 | self.position += len(token.text) 149 | self.next_token = None 150 | 151 | return token 152 | 153 | def raise_syntax_error( 154 | self, 155 | message: str, 156 | *, 157 | span_start: Optional[int] = None, 158 | span_end: Optional[int] = None, 159 | ) -> NoReturn: 160 | """Raise ParserSyntaxError at the given position.""" 161 | span = ( 162 | self.position if span_start is None else span_start, 163 | self.position if span_end is None else span_end, 164 | ) 165 | raise ParserSyntaxError( 166 | message, 167 | source=self.source, 168 | span=span, 169 | ) 170 | 171 | @contextlib.contextmanager 172 | def enclosing_tokens( 173 | self, open_token: str, close_token: str, *, around: str 174 | ) -> Iterator[None]: 175 | if self.check(open_token): 176 | open_position = self.position 177 | self.read() 178 | else: 179 | open_position = None 180 | 181 | yield 182 | 183 | if open_position is None: 184 | return 185 | 186 | if not self.check(close_token): 187 | self.raise_syntax_error( 188 | f"Expected matching {close_token} for {open_token}, after {around}", 189 | span_start=open_position, 190 | ) 191 | 192 | self.read() 193 | -------------------------------------------------------------------------------- /pylib/packaging/markers.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | import operator 6 | import os 7 | import platform 8 | import sys 9 | from typing import Any, Callable, Dict, List, Optional, Tuple, Union 10 | 11 | from ._parser import ( 12 | MarkerAtom, 13 | MarkerList, 14 | Op, 15 | Value, 16 | Variable, 17 | parse_marker as _parse_marker, 18 | ) 19 | from ._tokenizer import ParserSyntaxError 20 | from .specifiers import InvalidSpecifier, Specifier 21 | from .utils import canonicalize_name 22 | 23 | __all__ = [ 24 | "InvalidMarker", 25 | "UndefinedComparison", 26 | "UndefinedEnvironmentName", 27 | "Marker", 28 | "default_environment", 29 | ] 30 | 31 | Operator = Callable[[str, str], bool] 32 | 33 | 34 | class InvalidMarker(ValueError): 35 | """ 36 | An invalid marker was found, users should refer to PEP 508. 37 | """ 38 | 39 | 40 | class UndefinedComparison(ValueError): 41 | """ 42 | An invalid operation was attempted on a value that doesn't support it. 43 | """ 44 | 45 | 46 | class UndefinedEnvironmentName(ValueError): 47 | """ 48 | A name was attempted to be used that does not exist inside of the 49 | environment. 50 | """ 51 | 52 | 53 | def _normalize_extra_values(results: Any) -> Any: 54 | """ 55 | Normalize extra values. 56 | """ 57 | if isinstance(results[0], tuple): 58 | lhs, op, rhs = results[0] 59 | if isinstance(lhs, Variable) and lhs.value == "extra": 60 | normalized_extra = canonicalize_name(rhs.value) 61 | rhs = Value(normalized_extra) 62 | elif isinstance(rhs, Variable) and rhs.value == "extra": 63 | normalized_extra = canonicalize_name(lhs.value) 64 | lhs = Value(normalized_extra) 65 | results[0] = lhs, op, rhs 66 | return results 67 | 68 | 69 | def _format_marker( 70 | marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True 71 | ) -> str: 72 | 73 | assert isinstance(marker, (list, tuple, str)) 74 | 75 | # Sometimes we have a structure like [[...]] which is a single item list 76 | # where the single item is itself it's own list. In that case we want skip 77 | # the rest of this function so that we don't get extraneous () on the 78 | # outside. 79 | if ( 80 | isinstance(marker, list) 81 | and len(marker) == 1 82 | and isinstance(marker[0], (list, tuple)) 83 | ): 84 | return _format_marker(marker[0]) 85 | 86 | if isinstance(marker, list): 87 | inner = (_format_marker(m, first=False) for m in marker) 88 | if first: 89 | return " ".join(inner) 90 | else: 91 | return "(" + " ".join(inner) + ")" 92 | elif isinstance(marker, tuple): 93 | return " ".join([m.serialize() for m in marker]) 94 | else: 95 | return marker 96 | 97 | 98 | _operators: Dict[str, Operator] = { 99 | "in": lambda lhs, rhs: lhs in rhs, 100 | "not in": lambda lhs, rhs: lhs not in rhs, 101 | "<": operator.lt, 102 | "<=": operator.le, 103 | "==": operator.eq, 104 | "!=": operator.ne, 105 | ">=": operator.ge, 106 | ">": operator.gt, 107 | } 108 | 109 | 110 | def _eval_op(lhs: str, op: Op, rhs: str) -> bool: 111 | try: 112 | spec = Specifier("".join([op.serialize(), rhs])) 113 | except InvalidSpecifier: 114 | pass 115 | else: 116 | return spec.contains(lhs, prereleases=True) 117 | 118 | oper: Optional[Operator] = _operators.get(op.serialize()) 119 | if oper is None: 120 | raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") 121 | 122 | return oper(lhs, rhs) 123 | 124 | 125 | def _normalize(*values: str, key: str) -> Tuple[str, ...]: 126 | # PEP 685 – Comparison of extra names for optional distribution dependencies 127 | # https://peps.python.org/pep-0685/ 128 | # > When comparing extra names, tools MUST normalize the names being 129 | # > compared using the semantics outlined in PEP 503 for names 130 | if key == "extra": 131 | return tuple(canonicalize_name(v) for v in values) 132 | 133 | # other environment markers don't have such standards 134 | return values 135 | 136 | 137 | def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: 138 | groups: List[List[bool]] = [[]] 139 | 140 | for marker in markers: 141 | assert isinstance(marker, (list, tuple, str)) 142 | 143 | if isinstance(marker, list): 144 | groups[-1].append(_evaluate_markers(marker, environment)) 145 | elif isinstance(marker, tuple): 146 | lhs, op, rhs = marker 147 | 148 | if isinstance(lhs, Variable): 149 | environment_key = lhs.value 150 | lhs_value = environment[environment_key] 151 | rhs_value = rhs.value 152 | else: 153 | lhs_value = lhs.value 154 | environment_key = rhs.value 155 | rhs_value = environment[environment_key] 156 | 157 | lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) 158 | groups[-1].append(_eval_op(lhs_value, op, rhs_value)) 159 | else: 160 | assert marker in ["and", "or"] 161 | if marker == "or": 162 | groups.append([]) 163 | 164 | return any(all(item) for item in groups) 165 | 166 | 167 | def format_full_version(info: "sys._version_info") -> str: 168 | version = "{0.major}.{0.minor}.{0.micro}".format(info) 169 | if (kind := info.releaselevel) != "final": 170 | version += kind[0] + str(info.serial) 171 | return version 172 | 173 | 174 | def default_environment() -> Dict[str, str]: 175 | iver = format_full_version(sys.implementation.version) 176 | implementation_name = sys.implementation.name 177 | return { 178 | "implementation_name": implementation_name, 179 | "implementation_version": iver, 180 | "os_name": os.name, 181 | "platform_machine": platform.machine(), 182 | "platform_release": platform.release(), 183 | "platform_system": platform.system(), 184 | "platform_version": platform.version(), 185 | "python_full_version": platform.python_version(), 186 | "platform_python_implementation": platform.python_implementation(), 187 | "python_version": ".".join(platform.python_version_tuple()[:2]), 188 | "sys_platform": sys.platform, 189 | } 190 | 191 | 192 | class Marker: 193 | def __init__(self, marker: str) -> None: 194 | # Note: We create a Marker object without calling this constructor in 195 | # packaging.requirements.Requirement. If any additional logic is 196 | # added here, make sure to mirror/adapt Requirement. 197 | try: 198 | self._markers = _normalize_extra_values(_parse_marker(marker)) 199 | # The attribute `_markers` can be described in terms of a recursive type: 200 | # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] 201 | # 202 | # For example, the following expression: 203 | # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") 204 | # 205 | # is parsed into: 206 | # [ 207 | # (, ')>, ), 208 | # 'and', 209 | # [ 210 | # (, , ), 211 | # 'or', 212 | # (, , ) 213 | # ] 214 | # ] 215 | except ParserSyntaxError as e: 216 | raise InvalidMarker(str(e)) from e 217 | 218 | def __str__(self) -> str: 219 | return _format_marker(self._markers) 220 | 221 | def __repr__(self) -> str: 222 | return f"" 223 | 224 | def __hash__(self) -> int: 225 | return hash((self.__class__.__name__, str(self))) 226 | 227 | def __eq__(self, other: Any) -> bool: 228 | if not isinstance(other, Marker): 229 | return NotImplemented 230 | 231 | return str(self) == str(other) 232 | 233 | def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: 234 | """Evaluate a marker. 235 | 236 | Return the boolean from evaluating the given marker against the 237 | environment. environment is an optional argument to override all or 238 | part of the determined environment. 239 | 240 | The environment is determined from the current Python process. 241 | """ 242 | current_environment = default_environment() 243 | current_environment["extra"] = "" 244 | if environment is not None: 245 | current_environment.update(environment) 246 | # The API used to allow setting extra to None. We need to handle this 247 | # case for backwards compatibility. 248 | if current_environment["extra"] is None: 249 | current_environment["extra"] = "" 250 | 251 | return _evaluate_markers(self._markers, current_environment) 252 | -------------------------------------------------------------------------------- /pylib/packaging/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nodejs/gyp-next/0eaea297f0fbb0869597aa162f66f78eb2468fad/pylib/packaging/py.typed -------------------------------------------------------------------------------- /pylib/packaging/requirements.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | from typing import Any, Iterator, Optional, Set 6 | 7 | from ._parser import parse_requirement as _parse_requirement 8 | from ._tokenizer import ParserSyntaxError 9 | from .markers import Marker, _normalize_extra_values 10 | from .specifiers import SpecifierSet 11 | from .utils import canonicalize_name 12 | 13 | 14 | class InvalidRequirement(ValueError): 15 | """ 16 | An invalid requirement was found, users should refer to PEP 508. 17 | """ 18 | 19 | 20 | class Requirement: 21 | """Parse a requirement. 22 | 23 | Parse a given requirement string into its parts, such as name, specifier, 24 | URL, and extras. Raises InvalidRequirement on a badly-formed requirement 25 | string. 26 | """ 27 | 28 | # TODO: Can we test whether something is contained within a requirement? 29 | # If so how do we do that? Do we need to test against the _name_ of 30 | # the thing as well as the version? What about the markers? 31 | # TODO: Can we normalize the name and extra name? 32 | 33 | def __init__(self, requirement_string: str) -> None: 34 | try: 35 | parsed = _parse_requirement(requirement_string) 36 | except ParserSyntaxError as e: 37 | raise InvalidRequirement(str(e)) from e 38 | 39 | self.name: str = parsed.name 40 | self.url: Optional[str] = parsed.url or None 41 | self.extras: Set[str] = set(parsed.extras if parsed.extras else []) 42 | self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) 43 | self.marker: Optional[Marker] = None 44 | if parsed.marker is not None: 45 | self.marker = Marker.__new__(Marker) 46 | self.marker._markers = _normalize_extra_values(parsed.marker) 47 | 48 | def _iter_parts(self, name: str) -> Iterator[str]: 49 | yield name 50 | 51 | if self.extras: 52 | formatted_extras = ",".join(sorted(self.extras)) 53 | yield f"[{formatted_extras}]" 54 | 55 | if self.specifier: 56 | yield str(self.specifier) 57 | 58 | if self.url: 59 | yield f"@ {self.url}" 60 | if self.marker: 61 | yield " " 62 | 63 | if self.marker: 64 | yield f"; {self.marker}" 65 | 66 | def __str__(self) -> str: 67 | return "".join(self._iter_parts(self.name)) 68 | 69 | def __repr__(self) -> str: 70 | return f"" 71 | 72 | def __hash__(self) -> int: 73 | return hash( 74 | ( 75 | self.__class__.__name__, 76 | *self._iter_parts(canonicalize_name(self.name)), 77 | ) 78 | ) 79 | 80 | def __eq__(self, other: Any) -> bool: 81 | if not isinstance(other, Requirement): 82 | return NotImplemented 83 | 84 | return ( 85 | canonicalize_name(self.name) == canonicalize_name(other.name) 86 | and self.extras == other.extras 87 | and self.specifier == other.specifier 88 | and self.url == other.url 89 | and self.marker == other.marker 90 | ) 91 | -------------------------------------------------------------------------------- /pylib/packaging/utils.py: -------------------------------------------------------------------------------- 1 | # This file is dual licensed under the terms of the Apache License, Version 2 | # 2.0, and the BSD License. See the LICENSE file in the root of this repository 3 | # for complete details. 4 | 5 | import re 6 | from typing import FrozenSet, NewType, Tuple, Union, cast 7 | 8 | from .tags import Tag, parse_tag 9 | from .version import InvalidVersion, Version 10 | 11 | BuildTag = Union[Tuple[()], Tuple[int, str]] 12 | NormalizedName = NewType("NormalizedName", str) 13 | 14 | 15 | class InvalidName(ValueError): 16 | """ 17 | An invalid distribution name; users should refer to the packaging user guide. 18 | """ 19 | 20 | 21 | class InvalidWheelFilename(ValueError): 22 | """ 23 | An invalid wheel filename was found, users should refer to PEP 427. 24 | """ 25 | 26 | 27 | class InvalidSdistFilename(ValueError): 28 | """ 29 | An invalid sdist filename was found, users should refer to the packaging user guide. 30 | """ 31 | 32 | 33 | # Core metadata spec for `Name` 34 | _validate_regex = re.compile( 35 | r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE 36 | ) 37 | _canonicalize_regex = re.compile(r"[-_.]+") 38 | _normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") 39 | # PEP 427: The build number must start with a digit. 40 | _build_tag_regex = re.compile(r"(\d+)(.*)") 41 | 42 | 43 | def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: 44 | if validate and not _validate_regex.match(name): 45 | raise InvalidName(f"name is invalid: {name!r}") 46 | # This is taken from PEP 503. 47 | value = _canonicalize_regex.sub("-", name).lower() 48 | return cast(NormalizedName, value) 49 | 50 | 51 | def is_normalized_name(name: str) -> bool: 52 | return _normalized_regex.match(name) is not None 53 | 54 | 55 | def canonicalize_version( 56 | version: Union[Version, str], *, strip_trailing_zero: bool = True 57 | ) -> str: 58 | """ 59 | This is very similar to Version.__str__, but has one subtle difference 60 | with the way it handles the release segment. 61 | """ 62 | if isinstance(version, str): 63 | try: 64 | parsed = Version(version) 65 | except InvalidVersion: 66 | # Legacy versions cannot be normalized 67 | return version 68 | else: 69 | parsed = version 70 | 71 | parts = [] 72 | 73 | # Epoch 74 | if parsed.epoch != 0: 75 | parts.append(f"{parsed.epoch}!") 76 | 77 | # Release segment 78 | release_segment = ".".join(str(x) for x in parsed.release) 79 | if strip_trailing_zero: 80 | # NB: This strips trailing '.0's to normalize 81 | release_segment = re.sub(r"(\.0)+$", "", release_segment) 82 | parts.append(release_segment) 83 | 84 | # Pre-release 85 | if parsed.pre is not None: 86 | parts.append("".join(str(x) for x in parsed.pre)) 87 | 88 | # Post-release 89 | if parsed.post is not None: 90 | parts.append(f".post{parsed.post}") 91 | 92 | # Development release 93 | if parsed.dev is not None: 94 | parts.append(f".dev{parsed.dev}") 95 | 96 | # Local version segment 97 | if parsed.local is not None: 98 | parts.append(f"+{parsed.local}") 99 | 100 | return "".join(parts) 101 | 102 | 103 | def parse_wheel_filename( 104 | filename: str, 105 | ) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: 106 | if not filename.endswith(".whl"): 107 | raise InvalidWheelFilename( 108 | f"Invalid wheel filename (extension must be '.whl'): {filename}" 109 | ) 110 | 111 | filename = filename[:-4] 112 | dashes = filename.count("-") 113 | if dashes not in (4, 5): 114 | raise InvalidWheelFilename( 115 | f"Invalid wheel filename (wrong number of parts): {filename}" 116 | ) 117 | 118 | parts = filename.split("-", dashes - 2) 119 | name_part = parts[0] 120 | # See PEP 427 for the rules on escaping the project name. 121 | if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: 122 | raise InvalidWheelFilename(f"Invalid project name: {filename}") 123 | name = canonicalize_name(name_part) 124 | 125 | try: 126 | version = Version(parts[1]) 127 | except InvalidVersion as e: 128 | raise InvalidWheelFilename( 129 | f"Invalid wheel filename (invalid version): {filename}" 130 | ) from e 131 | 132 | if dashes == 5: 133 | build_part = parts[2] 134 | build_match = _build_tag_regex.match(build_part) 135 | if build_match is None: 136 | raise InvalidWheelFilename( 137 | f"Invalid build number: {build_part} in '{filename}'" 138 | ) 139 | build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) 140 | else: 141 | build = () 142 | tags = parse_tag(parts[-1]) 143 | return (name, version, build, tags) 144 | 145 | 146 | def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: 147 | if filename.endswith(".tar.gz"): 148 | file_stem = filename[: -len(".tar.gz")] 149 | elif filename.endswith(".zip"): 150 | file_stem = filename[: -len(".zip")] 151 | else: 152 | raise InvalidSdistFilename( 153 | f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" 154 | f" {filename}" 155 | ) 156 | 157 | # We are requiring a PEP 440 version, which cannot contain dashes, 158 | # so we split on the last dash. 159 | name_part, sep, version_part = file_stem.rpartition("-") 160 | if not sep: 161 | raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") 162 | 163 | name = canonicalize_name(name_part) 164 | 165 | try: 166 | version = Version(version_part) 167 | except InvalidVersion as e: 168 | raise InvalidSdistFilename( 169 | f"Invalid sdist filename (invalid version): {filename}" 170 | ) from e 171 | 172 | return (name, version) 173 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "gyp-next" 7 | version = "0.20.0" 8 | authors = [ 9 | { name="Node.js contributors", email="ryzokuken@disroot.org" }, 10 | ] 11 | description = "A fork of the GYP build system for use in the Node.js projects" 12 | readme = "README.md" 13 | license = { file="LICENSE" } 14 | requires-python = ">=3.8" 15 | dependencies = ["packaging>=24.0", "setuptools>=69.5.1"] 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", 18 | "Environment :: Console", 19 | "Intended Audience :: Developers", 20 | "License :: OSI Approved :: BSD License", 21 | "Natural Language :: English", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.8", 25 | "Programming Language :: Python :: 3.9", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | ] 29 | 30 | [project.optional-dependencies] 31 | dev = ["pytest", "ruff"] 32 | 33 | [project.scripts] 34 | gyp = "gyp:script_main" 35 | 36 | [project.urls] 37 | "Homepage" = "https://github.com/nodejs/gyp-next" 38 | 39 | [tool.ruff] 40 | extend-exclude = ["pylib/packaging"] 41 | line-length = 88 42 | target-version = "py37" 43 | 44 | [tool.ruff.lint] 45 | select = [ 46 | "C4", # flake8-comprehensions 47 | "C90", # McCabe cyclomatic complexity 48 | "DTZ", # flake8-datetimez 49 | "E", # pycodestyle 50 | "F", # Pyflakes 51 | "G", # flake8-logging-format 52 | "ICN", # flake8-import-conventions 53 | "INT", # flake8-gettext 54 | "PL", # Pylint 55 | "PYI", # flake8-pyi 56 | "RSE", # flake8-raise 57 | "RUF", # Ruff-specific rules 58 | "T10", # flake8-debugger 59 | "TCH", # flake8-type-checking 60 | "TID", # flake8-tidy-imports 61 | "UP", # pyupgrade 62 | "W", # pycodestyle 63 | "YTT", # flake8-2020 64 | # "A", # flake8-builtins 65 | # "ANN", # flake8-annotations 66 | # "ARG", # flake8-unused-arguments 67 | # "B", # flake8-bugbear 68 | # "BLE", # flake8-blind-except 69 | # "COM", # flake8-commas 70 | # "D", # pydocstyle 71 | # "DJ", # flake8-django 72 | # "EM", # flake8-errmsg 73 | # "ERA", # eradicate 74 | # "EXE", # flake8-executable 75 | # "FBT", # flake8-boolean-trap 76 | # "I", # isort 77 | # "INP", # flake8-no-pep420 78 | # "ISC", # flake8-implicit-str-concat 79 | # "N", # pep8-naming 80 | # "NPY", # NumPy-specific rules 81 | # "PD", # pandas-vet 82 | # "PGH", # pygrep-hooks 83 | # "PIE", # flake8-pie 84 | # "PT", # flake8-pytest-style 85 | # "PTH", # flake8-use-pathlib 86 | # "Q", # flake8-quotes 87 | # "RET", # flake8-return 88 | # "S", # flake8-bandit 89 | # "SIM", # flake8-simplify 90 | # "SLF", # flake8-self 91 | # "T20", # flake8-print 92 | # "TRY", # tryceratops 93 | ] 94 | ignore = [ 95 | "PLR1714", 96 | "PLW0603", 97 | "PLW2901", 98 | "RUF005", 99 | "RUF012", 100 | "UP031", 101 | ] 102 | 103 | [tool.ruff.lint.mccabe] 104 | max-complexity = 101 105 | 106 | [tool.ruff.lint.pylint] 107 | allow-magic-value-types = ["float", "int", "str"] 108 | max-args = 11 109 | max-branches = 108 110 | max-returns = 10 111 | max-statements = 286 112 | 113 | [tool.setuptools] 114 | package-dir = {"" = "pylib"} 115 | packages = ["gyp", "gyp.generator"] 116 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "last-release-sha": "78756421b0d7bb335992a9c7d26ba3cc8b619708", 3 | "packages": { 4 | ".": { 5 | "release-type": "python", 6 | "package-name": "gyp-next", 7 | "bump-minor-pre-major": true, 8 | "include-component-in-tag": false 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/expected-darwin/cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR) 2 | cmake_policy(VERSION 2.8.8) 3 | project(test) 4 | set(configuration "Default") 5 | enable_language(ASM) 6 | set(builddir "${CMAKE_CURRENT_BINARY_DIR}") 7 | set(obj "${builddir}/obj") 8 | 9 | set(CMAKE_C_OUTPUT_EXTENSION_REPLACE 1) 10 | set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE 1) 11 | 12 | 13 | 14 | #*/gyp-next/test/fixtures/integration.gyp:test#target 15 | set(TARGET "test") 16 | set(TOOLSET "target") 17 | set(test__cxx_srcs "../../test.cc") 18 | link_directories( ../../mylib 19 | ) 20 | add_executable(test ${test__cxx_srcs}) 21 | set_target_properties(test PROPERTIES EXCLUDE_FROM_ALL "FALSE") 22 | set_target_properties(test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${builddir}") 23 | set_target_properties(test PROPERTIES PREFIX "") 24 | set_target_properties(test PROPERTIES RUNTIME_OUTPUT_NAME "test") 25 | set_target_properties(test PROPERTIES SUFFIX "") 26 | set_source_files_properties(${builddir}/test PROPERTIES GENERATED "TRUE") 27 | set(test__include_dirs "${CMAKE_CURRENT_LIST_DIR}/../../include") 28 | set_property(TARGET test APPEND PROPERTY INCLUDE_DIRECTORIES ${test__include_dirs}) 29 | set_target_properties(test PROPERTIES COMPILE_FLAGS "-fasm-blocks -mpascal-strings -Os -gdwarf-2 -arch x86_64 ") 30 | unset(TOOLSET) 31 | unset(TARGET) 32 | -------------------------------------------------------------------------------- /test/fixtures/expected-darwin/make/test.target.mk: -------------------------------------------------------------------------------- 1 | # This file is generated by gyp; do not edit. 2 | 3 | TOOLSET := target 4 | TARGET := test 5 | DEFS_Default := 6 | 7 | # Flags passed to all source files. 8 | CFLAGS_Default := \ 9 | -fasm-blocks \ 10 | -mpascal-strings \ 11 | -Os \ 12 | -gdwarf-2 \ 13 | -arch \ 14 | x86_64 15 | 16 | # Flags passed to only C files. 17 | CFLAGS_C_Default := 18 | 19 | # Flags passed to only C++ files. 20 | CFLAGS_CC_Default := 21 | 22 | # Flags passed to only ObjC files. 23 | CFLAGS_OBJC_Default := 24 | 25 | # Flags passed to only ObjC++ files. 26 | CFLAGS_OBJCC_Default := 27 | 28 | INCS_Default := \ 29 | -I$(srcdir)/include 30 | 31 | OBJS := \ 32 | $(obj).target/$(TARGET)/test.o 33 | 34 | # Add to the list of files we specially track dependencies for. 35 | all_deps += $(OBJS) 36 | 37 | # CFLAGS et al overrides must be target-local. 38 | # See "Target-specific Variable Values" in the GNU Make manual. 39 | $(OBJS): TOOLSET := $(TOOLSET) 40 | $(OBJS): GYP_CFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_C_$(BUILDTYPE)) 41 | $(OBJS): GYP_CXXFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_CC_$(BUILDTYPE)) 42 | $(OBJS): GYP_OBJCFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_C_$(BUILDTYPE)) $(CFLAGS_OBJC_$(BUILDTYPE)) 43 | $(OBJS): GYP_OBJCXXFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_CC_$(BUILDTYPE)) $(CFLAGS_OBJCC_$(BUILDTYPE)) 44 | 45 | # Suffix rules, putting all outputs into $(obj). 46 | 47 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD 48 | @$(call do_cmd,cxx,1) 49 | 50 | # Try building from generated source, too. 51 | 52 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD 53 | @$(call do_cmd,cxx,1) 54 | 55 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj)/%.cc FORCE_DO_CMD 56 | @$(call do_cmd,cxx,1) 57 | 58 | # End of this set of suffix rules 59 | ### Rules for final target. 60 | LDFLAGS_Default := \ 61 | -arch \ 62 | x86_64 \ 63 | -L$(builddir) \ 64 | -L$(srcdir)/mylib 65 | 66 | LIBTOOLFLAGS_Default := 67 | 68 | LIBS := 69 | 70 | $(builddir)/test: GYP_LDFLAGS := $(LDFLAGS_$(BUILDTYPE)) 71 | $(builddir)/test: LIBS := $(LIBS) 72 | $(builddir)/test: GYP_LIBTOOLFLAGS := $(LIBTOOLFLAGS_$(BUILDTYPE)) 73 | $(builddir)/test: LD_INPUTS := $(OBJS) 74 | $(builddir)/test: TOOLSET := $(TOOLSET) 75 | $(builddir)/test: $(OBJS) FORCE_DO_CMD 76 | $(call do_cmd,link) 77 | 78 | all_deps += $(builddir)/test 79 | # Add target alias 80 | .PHONY: test 81 | test: $(builddir)/test 82 | 83 | # Add executable to "all" target. 84 | .PHONY: all 85 | all: $(builddir)/test 86 | 87 | -------------------------------------------------------------------------------- /test/fixtures/expected-darwin/ninja/test.ninja: -------------------------------------------------------------------------------- 1 | defines = 2 | includes = -I../../include 3 | cflags = -fasm-blocks -mpascal-strings -Os -gdwarf-2 -arch x86_64 4 | cflags_c = 5 | cflags_cc = 6 | cflags_objc = $cflags_c 7 | cflags_objcc = $cflags_cc 8 | arflags = 9 | 10 | build obj/test.test.o: cxx ../../test.cc 11 | 12 | ldflags = -arch x86_64 -L./ 13 | libs = -L../../mylib 14 | build test: link obj/test.test.o 15 | ld = $ldxx 16 | -------------------------------------------------------------------------------- /test/fixtures/expected-linux/cmake/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.8 FATAL_ERROR) 2 | cmake_policy(VERSION 2.8.8) 3 | project(test) 4 | set(configuration "Default") 5 | enable_language(ASM) 6 | set(builddir "${CMAKE_CURRENT_BINARY_DIR}") 7 | set(obj "${builddir}/obj") 8 | 9 | set(CMAKE_C_OUTPUT_EXTENSION_REPLACE 1) 10 | set(CMAKE_CXX_OUTPUT_EXTENSION_REPLACE 1) 11 | 12 | set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1) 13 | 14 | 15 | #*/test/fixtures/integration.gyp:test#target 16 | set(TARGET "test") 17 | set(TOOLSET "target") 18 | set(test__cxx_srcs "../../test.cc") 19 | link_directories( ../../mylib 20 | ) 21 | add_executable(test ${test__cxx_srcs}) 22 | set_target_properties(test PROPERTIES EXCLUDE_FROM_ALL "FALSE") 23 | set_target_properties(test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${builddir}") 24 | set_target_properties(test PROPERTIES PREFIX "") 25 | set_target_properties(test PROPERTIES RUNTIME_OUTPUT_NAME "test") 26 | set_target_properties(test PROPERTIES SUFFIX "") 27 | set_source_files_properties(${builddir}/test PROPERTIES GENERATED "TRUE") 28 | set(test__include_dirs "${CMAKE_CURRENT_LIST_DIR}/../../include") 29 | set_property(TARGET test APPEND PROPERTY INCLUDE_DIRECTORIES ${test__include_dirs}) 30 | set_target_properties(test PROPERTIES COMPILE_FLAGS "") 31 | unset(TOOLSET) 32 | unset(TARGET) 33 | -------------------------------------------------------------------------------- /test/fixtures/expected-linux/make/test.target.mk: -------------------------------------------------------------------------------- 1 | # This file is generated by gyp; do not edit. 2 | 3 | TOOLSET := target 4 | TARGET := test 5 | DEFS_Default := 6 | 7 | # Flags passed to all source files. 8 | CFLAGS_Default := 9 | 10 | # Flags passed to only C files. 11 | CFLAGS_C_Default := 12 | 13 | # Flags passed to only C++ files. 14 | CFLAGS_CC_Default := 15 | 16 | INCS_Default := \ 17 | -I$(srcdir)/include 18 | 19 | OBJS := \ 20 | $(obj).target/$(TARGET)/test.o 21 | 22 | # Add to the list of files we specially track dependencies for. 23 | all_deps += $(OBJS) 24 | 25 | # CFLAGS et al overrides must be target-local. 26 | # See "Target-specific Variable Values" in the GNU Make manual. 27 | $(OBJS): TOOLSET := $(TOOLSET) 28 | $(OBJS): GYP_CFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_C_$(BUILDTYPE)) 29 | $(OBJS): GYP_CXXFLAGS := $(DEFS_$(BUILDTYPE)) $(INCS_$(BUILDTYPE)) $(CFLAGS_$(BUILDTYPE)) $(CFLAGS_CC_$(BUILDTYPE)) 30 | 31 | # Suffix rules, putting all outputs into $(obj). 32 | 33 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(srcdir)/%.cc FORCE_DO_CMD 34 | @$(call do_cmd,cxx,1) 35 | 36 | # Try building from generated source, too. 37 | 38 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj).$(TOOLSET)/%.cc FORCE_DO_CMD 39 | @$(call do_cmd,cxx,1) 40 | 41 | $(obj).$(TOOLSET)/$(TARGET)/%.o: $(obj)/%.cc FORCE_DO_CMD 42 | @$(call do_cmd,cxx,1) 43 | 44 | # End of this set of suffix rules 45 | ### Rules for final target. 46 | LDFLAGS_Default := \ 47 | -L$(srcdir)/mylib 48 | 49 | LIBS := 50 | 51 | $(builddir)/test: GYP_LDFLAGS := $(LDFLAGS_$(BUILDTYPE)) 52 | $(builddir)/test: LIBS := $(LIBS) 53 | $(builddir)/test: LD_INPUTS := $(OBJS) 54 | $(builddir)/test: TOOLSET := $(TOOLSET) 55 | $(builddir)/test: $(OBJS) FORCE_DO_CMD 56 | $(call do_cmd,link) 57 | 58 | all_deps += $(builddir)/test 59 | # Add target alias 60 | .PHONY: test 61 | test: $(builddir)/test 62 | 63 | # Add executable to "all" target. 64 | .PHONY: all 65 | all: $(builddir)/test 66 | 67 | -------------------------------------------------------------------------------- /test/fixtures/expected-linux/ninja/test.ninja: -------------------------------------------------------------------------------- 1 | defines = 2 | includes = -I../../include 3 | cflags = 4 | cflags_c = 5 | cflags_cc = 6 | arflags = 7 | 8 | build obj/test.test.o: cxx ../../test.cc 9 | 10 | ldflags = 11 | libs = -L../../mylib 12 | build test: link obj/test.test.o 13 | ld = $ldxx 14 | -------------------------------------------------------------------------------- /test/fixtures/include/test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | int foo(); 4 | -------------------------------------------------------------------------------- /test/fixtures/integration.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'test', 5 | 'type': 'executable', 6 | 'sources': [ 7 | 'test.cc', 8 | ], 9 | 'include_dirs': [ 10 | 'include', 11 | ], 12 | 'library_dirs': [ 13 | 'mylib' 14 | ], 15 | }, 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/test.cc: -------------------------------------------------------------------------------- 1 | #include "test.h" 2 | 3 | int main() { 4 | return foo(); 5 | } 6 | 7 | int foo() { 8 | return 0; 9 | } 10 | -------------------------------------------------------------------------------- /test/integration_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Integration test""" 4 | 5 | import os 6 | import re 7 | import shutil 8 | import unittest 9 | 10 | import gyp 11 | 12 | fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures") 13 | gyp_file = os.path.join(os.path.dirname(__file__), "fixtures/integration.gyp") 14 | 15 | supported_sysnames = {"darwin", "linux"} 16 | sysname = os.uname().sysname.lower() 17 | expected_dir = os.path.join(fixture_dir, f"expected-{sysname}") 18 | 19 | 20 | class TestGyp(unittest.TestCase): 21 | def setUp(self) -> None: 22 | if sysname not in supported_sysnames: 23 | self.skipTest(f"Unsupported system: {sysname}") 24 | shutil.rmtree(os.path.join(fixture_dir, "out"), ignore_errors=True) 25 | 26 | def assert_file(self, actual, expected) -> None: 27 | actual_filepath = os.path.join(fixture_dir, actual) 28 | expected_filepath = os.path.join(expected_dir, expected) 29 | 30 | with open(expected_filepath) as in_file: 31 | expected_bytes = re.escape(in_file.read()) 32 | expected_bytes = expected_bytes.replace("\\*", ".*") 33 | expected_re = re.compile(expected_bytes) 34 | 35 | with open(actual_filepath) as in_file: 36 | actual_bytes = in_file.read() 37 | 38 | try: 39 | self.assertRegex(actual_bytes, expected_re) 40 | except Exception: 41 | shutil.copyfile(actual_filepath, f"{expected_filepath}.actual") 42 | raise 43 | 44 | def test_ninja(self) -> None: 45 | rc = gyp.main(["-f", "ninja", "--depth", fixture_dir, gyp_file]) 46 | assert rc == 0 47 | 48 | self.assert_file("out/Default/obj/test.ninja", "ninja/test.ninja") 49 | 50 | def test_make(self) -> None: 51 | rc = gyp.main( 52 | [ 53 | "-f", 54 | "make", 55 | "--depth", 56 | fixture_dir, 57 | "--generator-output", 58 | "out", 59 | gyp_file, 60 | ] 61 | ) 62 | assert rc == 0 63 | 64 | self.assert_file("out/test.target.mk", "make/test.target.mk") 65 | 66 | def test_cmake(self) -> None: 67 | rc = gyp.main(["-f", "cmake", "--depth", fixture_dir, gyp_file]) 68 | assert rc == 0 69 | 70 | self.assert_file("out/Default/CMakeLists.txt", "cmake/CMakeLists.txt") 71 | -------------------------------------------------------------------------------- /test_gyp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2012 Google Inc. All rights reserved. 3 | # Use of this source code is governed by a BSD-style license that can be 4 | # found in the LICENSE file. 5 | 6 | """gyptest.py -- test runner for GYP tests.""" 7 | 8 | 9 | import argparse 10 | import os 11 | import platform 12 | import subprocess 13 | import sys 14 | import time 15 | 16 | 17 | def is_test_name(f): 18 | return f.startswith("gyptest") and f.endswith(".py") 19 | 20 | 21 | def find_all_gyptest_files(directory): 22 | result = [] 23 | for root, dirs, files in os.walk(directory): 24 | result.extend([os.path.join(root, f) for f in files if is_test_name(f)]) 25 | result.sort() 26 | return result 27 | 28 | 29 | def main(argv=None): 30 | if argv is None: 31 | argv = sys.argv 32 | 33 | parser = argparse.ArgumentParser() 34 | parser.add_argument("-a", "--all", action="store_true", help="run all tests") 35 | parser.add_argument("-C", "--chdir", action="store", help="change to directory") 36 | parser.add_argument( 37 | "-f", 38 | "--format", 39 | action="store", 40 | default="", 41 | help="run tests with the specified formats", 42 | ) 43 | parser.add_argument( 44 | "-G", 45 | "--gyp_option", 46 | action="append", 47 | default=[], 48 | help="Add -G options to the gyp command line", 49 | ) 50 | parser.add_argument( 51 | "-l", "--list", action="store_true", help="list available tests and exit" 52 | ) 53 | parser.add_argument( 54 | "-n", 55 | "--no-exec", 56 | action="store_true", 57 | help="no execute, just print the command line", 58 | ) 59 | parser.add_argument( 60 | "--path", action="append", default=[], help="additional $PATH directory" 61 | ) 62 | parser.add_argument( 63 | "-q", 64 | "--quiet", 65 | action="store_true", 66 | help="quiet, don't print anything unless there are failures", 67 | ) 68 | parser.add_argument( 69 | "-v", 70 | "--verbose", 71 | action="store_true", 72 | help="print configuration info and test results.", 73 | ) 74 | parser.add_argument("tests", nargs="*") 75 | args = parser.parse_args(argv[1:]) 76 | 77 | if args.chdir: 78 | os.chdir(args.chdir) 79 | 80 | if args.path: 81 | extra_path = [os.path.abspath(p) for p in args.path] 82 | extra_path = os.pathsep.join(extra_path) 83 | os.environ["PATH"] = extra_path + os.pathsep + os.environ["PATH"] 84 | 85 | if not args.tests: 86 | if not args.all: 87 | sys.stderr.write("Specify -a to get all tests.\n") 88 | return 1 89 | args.tests = ["test"] 90 | 91 | tests = [] 92 | for arg in args.tests: 93 | if os.path.isdir(arg): 94 | tests.extend(find_all_gyptest_files(os.path.normpath(arg))) 95 | else: 96 | if not is_test_name(os.path.basename(arg)): 97 | print(arg, "is not a valid gyp test name.", file=sys.stderr) 98 | sys.exit(1) 99 | tests.append(arg) 100 | 101 | if args.list: 102 | for test in tests: 103 | print(test) 104 | sys.exit(0) 105 | 106 | os.environ["PYTHONPATH"] = os.path.abspath("test/lib") 107 | 108 | if args.verbose: 109 | print_configuration_info() 110 | 111 | if args.gyp_option and not args.quiet: 112 | print("Extra Gyp options: %s\n" % args.gyp_option) 113 | 114 | if args.format: 115 | format_list = args.format.split(",") 116 | else: 117 | format_list = { 118 | "aix5": ["make"], 119 | "os400": ["make"], 120 | "freebsd7": ["make"], 121 | "freebsd8": ["make"], 122 | "openbsd5": ["make"], 123 | "cygwin": ["msvs"], 124 | "win32": ["msvs", "ninja"], 125 | "linux": ["make", "ninja"], 126 | "linux2": ["make", "ninja"], 127 | "linux3": ["make", "ninja"], 128 | # TODO: Re-enable xcode-ninja. 129 | # https://bugs.chromium.org/p/gyp/issues/detail?id=530 130 | # 'darwin': ['make', 'ninja', 'xcode', 'xcode-ninja'], 131 | "darwin": ["make", "ninja", "xcode"], 132 | }[sys.platform] 133 | 134 | gyp_options = [] 135 | for option in args.gyp_option: 136 | gyp_options += ["-G", option] 137 | 138 | runner = Runner(format_list, tests, gyp_options, args.verbose) 139 | runner.run() 140 | 141 | if not args.quiet: 142 | runner.print_results() 143 | 144 | return 1 if runner.failures else 0 145 | 146 | 147 | def print_configuration_info(): 148 | print("Test configuration:") 149 | if sys.platform == "darwin": 150 | sys.path.append(os.path.abspath("test/lib")) 151 | import TestMac 152 | 153 | print(f" Mac {platform.mac_ver()[0]} {platform.mac_ver()[2]}") 154 | print(f" Xcode {TestMac.Xcode.Version()}") 155 | elif sys.platform == "win32": 156 | sys.path.append(os.path.abspath("pylib")) 157 | import gyp.MSVSVersion 158 | 159 | print(" Win %s %s\n" % platform.win32_ver()[0:2]) 160 | print(" MSVS %s" % gyp.MSVSVersion.SelectVisualStudioVersion().Description()) 161 | elif sys.platform in ("linux", "linux2"): 162 | print(" Linux %s" % " ".join(platform.linux_distribution())) 163 | print(f" Python {platform.python_version()}") 164 | print(f" PYTHONPATH={os.environ['PYTHONPATH']}") 165 | print() 166 | 167 | 168 | class Runner: 169 | def __init__(self, formats, tests, gyp_options, verbose): 170 | self.formats = formats 171 | self.tests = tests 172 | self.verbose = verbose 173 | self.gyp_options = gyp_options 174 | self.failures = [] 175 | self.num_tests = len(formats) * len(tests) 176 | num_digits = len(str(self.num_tests)) 177 | self.fmt_str = "[%%%dd/%%%dd] (%%s) %%s" % (num_digits, num_digits) 178 | self.isatty = sys.stdout.isatty() and not self.verbose 179 | self.env = os.environ.copy() 180 | self.hpos = 0 181 | 182 | def run(self): 183 | run_start = time.time() 184 | 185 | i = 1 186 | for fmt in self.formats: 187 | for test in self.tests: 188 | self.run_test(test, fmt, i) 189 | i += 1 190 | 191 | if self.isatty: 192 | self.erase_current_line() 193 | 194 | self.took = time.time() - run_start 195 | 196 | def run_test(self, test, fmt, i): 197 | if self.isatty: 198 | self.erase_current_line() 199 | 200 | msg = self.fmt_str % (i, self.num_tests, fmt, test) 201 | self.print_(msg) 202 | 203 | start = time.time() 204 | cmd = [sys.executable, test] + self.gyp_options 205 | self.env["TESTGYP_FORMAT"] = fmt 206 | proc = subprocess.Popen( 207 | cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.env 208 | ) 209 | proc.wait() 210 | took = time.time() - start 211 | 212 | stdout = proc.stdout.read().decode("utf8") 213 | if proc.returncode == 2: 214 | res = "skipped" 215 | elif proc.returncode: 216 | res = "failed" 217 | self.failures.append(f"({test}) {fmt}") 218 | else: 219 | res = "passed" 220 | res_msg = f" {res} {took:.3f}s" 221 | self.print_(res_msg) 222 | 223 | if stdout and not stdout.endswith(("PASSED\n", "NO RESULT\n")): 224 | print() 225 | print("\n".join(f" {line}" for line in stdout.splitlines())) 226 | elif not self.isatty: 227 | print() 228 | 229 | def print_(self, msg): 230 | print(msg, end="") 231 | index = msg.rfind("\n") 232 | if index == -1: 233 | self.hpos += len(msg) 234 | else: 235 | self.hpos = len(msg) - index 236 | sys.stdout.flush() 237 | 238 | def erase_current_line(self): 239 | print("\b" * self.hpos + " " * self.hpos + "\b" * self.hpos, end="") 240 | sys.stdout.flush() 241 | self.hpos = 0 242 | 243 | def print_results(self): 244 | num_failures = len(self.failures) 245 | if num_failures: 246 | print() 247 | if num_failures == 1: 248 | print("Failed the following test:") 249 | else: 250 | print("Failed the following %d tests:" % num_failures) 251 | print("\t" + "\n\t".join(sorted(self.failures))) 252 | print() 253 | print( 254 | "Ran %d tests in %.3fs, %d failed." 255 | % (self.num_tests, self.took, num_failures) 256 | ) 257 | print() 258 | 259 | 260 | if __name__ == "__main__": 261 | sys.exit(main()) 262 | -------------------------------------------------------------------------------- /tools/README: -------------------------------------------------------------------------------- 1 | pretty_vcproj: 2 | Usage: pretty_vcproj.py "c:\path\to\vcproj.vcproj" [key1=value1] [key2=value2] 3 | 4 | They key/value pair are used to resolve vsprops name. 5 | 6 | For example, if I want to diff the base.vcproj project: 7 | 8 | pretty_vcproj.py z:\dev\src-chrome\src\base\build\base.vcproj "$(SolutionDir)=z:\dev\src-chrome\src\chrome\\" "$(CHROMIUM_BUILD)=" "$(CHROME_BUILD_TYPE)=" > original.txt 9 | pretty_vcproj.py z:\dev\src-chrome\src\base\base_gyp.vcproj "$(SolutionDir)=z:\dev\src-chrome\src\chrome\\" "$(CHROMIUM_BUILD)=" "$(CHROME_BUILD_TYPE)=" > gyp.txt 10 | 11 | And you can use your favorite diff tool to see the changes. 12 | 13 | Note: In the case of base.vcproj, the original vcproj is one level up the generated one. 14 | I suggest you do a search and replace for '"..\' and replace it with '"' in original.txt 15 | before you perform the diff. -------------------------------------------------------------------------------- /tools/Xcode/README: -------------------------------------------------------------------------------- 1 | Specifications contains syntax formatters for Xcode 3. These do not appear to be supported yet on Xcode 4. To use these with Xcode 3 please install both the gyp.pbfilespec and gyp.xclangspec files in 2 | 3 | ~/Library/Application Support/Developer/Shared/Xcode/Specifications/ 4 | 5 | and restart Xcode. -------------------------------------------------------------------------------- /tools/Xcode/Specifications/gyp.pbfilespec: -------------------------------------------------------------------------------- 1 | /* 2 | gyp.pbfilespec 3 | GYP source file spec for Xcode 3 4 | 5 | There is not much documentation available regarding the format 6 | of .pbfilespec files. As a starting point, see for instance the 7 | outdated documentation at: 8 | http://maxao.free.fr/xcode-plugin-interface/specifications.html 9 | and the files in: 10 | /Developer/Library/PrivateFrameworks/XcodeEdit.framework/Versions/A/Resources/ 11 | 12 | Place this file in directory: 13 | ~/Library/Application Support/Developer/Shared/Xcode/Specifications/ 14 | */ 15 | 16 | ( 17 | { 18 | Identifier = sourcecode.gyp; 19 | BasedOn = sourcecode; 20 | Name = "GYP Files"; 21 | Extensions = ("gyp", "gypi"); 22 | MIMETypes = ("text/gyp"); 23 | Language = "xcode.lang.gyp"; 24 | IsTextFile = YES; 25 | IsSourceFile = YES; 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /tools/Xcode/Specifications/gyp.xclangspec: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Google Inc. All rights reserved. 3 | Use of this source code is governed by a BSD-style license that can be 4 | found in the LICENSE file. 5 | 6 | gyp.xclangspec 7 | GYP language specification for Xcode 3 8 | 9 | There is not much documentation available regarding the format 10 | of .xclangspec files. As a starting point, see for instance the 11 | outdated documentation at: 12 | http://maxao.free.fr/xcode-plugin-interface/specifications.html 13 | and the files in: 14 | /Developer/Library/PrivateFrameworks/XcodeEdit.framework/Versions/A/Resources/ 15 | 16 | Place this file in directory: 17 | ~/Library/Application Support/Developer/Shared/Xcode/Specifications/ 18 | */ 19 | 20 | ( 21 | 22 | { 23 | Identifier = "xcode.lang.gyp.keyword"; 24 | Syntax = { 25 | Words = ( 26 | "and", 27 | "or", 28 | " "{dst}"') 84 | 85 | print("}") 86 | 87 | 88 | def main(): 89 | if len(sys.argv) < 2: 90 | print(__doc__, file=sys.stderr) 91 | print(file=sys.stderr) 92 | print("usage: %s target1 target2..." % (sys.argv[0]), file=sys.stderr) 93 | return 1 94 | 95 | edges = LoadEdges("dump.json", sys.argv[1:]) 96 | 97 | WriteGraph(edges) 98 | return 0 99 | 100 | 101 | if __name__ == "__main__": 102 | sys.exit(main()) 103 | -------------------------------------------------------------------------------- /tools/pretty_gyp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2012 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """Pretty-prints the contents of a GYP file.""" 8 | 9 | 10 | import re 11 | import sys 12 | 13 | # Regex to remove comments when we're counting braces. 14 | COMMENT_RE = re.compile(r"\s*#.*") 15 | 16 | # Regex to remove quoted strings when we're counting braces. 17 | # It takes into account quoted quotes, and makes sure that the quotes match. 18 | # NOTE: It does not handle quotes that span more than one line, or 19 | # cases where an escaped quote is preceded by an escaped backslash. 20 | QUOTE_RE_STR = r'(?P[\'"])(.*?)(? 0: 106 | after = True 107 | 108 | # This catches the special case of a closing brace having something 109 | # other than just whitespace ahead of it -- we don't want to 110 | # unindent that until after this line is printed so it stays with 111 | # the previous indentation level. 112 | if cnt < 0 and closing_prefix_re.match(stripline): 113 | after = True 114 | return (cnt, after) 115 | 116 | 117 | def prettyprint_input(lines): 118 | """Does the main work of indenting the input based on the brace counts.""" 119 | indent = 0 120 | basic_offset = 2 121 | for line in lines: 122 | if COMMENT_RE.match(line): 123 | print(line) 124 | else: 125 | line = line.strip("\r\n\t ") # Otherwise doesn't strip \r on Unix. 126 | if len(line) > 0: 127 | (brace_diff, after) = count_braces(line) 128 | if brace_diff != 0: 129 | if after: 130 | print(" " * (basic_offset * indent) + line) 131 | indent += brace_diff 132 | else: 133 | indent += brace_diff 134 | print(" " * (basic_offset * indent) + line) 135 | else: 136 | print(" " * (basic_offset * indent) + line) 137 | else: 138 | print() 139 | 140 | 141 | def main(): 142 | if len(sys.argv) > 1: 143 | data = open(sys.argv[1]).read().splitlines() 144 | else: 145 | data = sys.stdin.read().splitlines() 146 | # Split up the double braces. 147 | lines = split_double_braces(data) 148 | 149 | # Indent and print the output. 150 | prettyprint_input(lines) 151 | return 0 152 | 153 | 154 | if __name__ == "__main__": 155 | sys.exit(main()) 156 | -------------------------------------------------------------------------------- /tools/pretty_sln.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (c) 2012 Google Inc. All rights reserved. 4 | # Use of this source code is governed by a BSD-style license that can be 5 | # found in the LICENSE file. 6 | 7 | """Prints the information in a sln file in a diffable way. 8 | 9 | It first outputs each projects in alphabetical order with their 10 | dependencies. 11 | 12 | Then it outputs a possible build order. 13 | """ 14 | 15 | 16 | import os 17 | import re 18 | import sys 19 | 20 | import pretty_vcproj 21 | 22 | __author__ = "nsylvain (Nicolas Sylvain)" 23 | 24 | 25 | def BuildProject(project, built, projects, deps): 26 | # if all dependencies are done, we can build it, otherwise we try to build the 27 | # dependency. 28 | # This is not infinite-recursion proof. 29 | for dep in deps[project]: 30 | if dep not in built: 31 | BuildProject(dep, built, projects, deps) 32 | print(project) 33 | built.append(project) 34 | 35 | 36 | def ParseSolution(solution_file): 37 | # All projects, their clsid and paths. 38 | projects = {} 39 | 40 | # A list of dependencies associated with a project. 41 | dependencies = {} 42 | 43 | # Regular expressions that matches the SLN format. 44 | # The first line of a project definition. 45 | begin_project = re.compile( 46 | r'^Project\("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942' 47 | r'}"\) = "(.*)", "(.*)", "(.*)"$' 48 | ) 49 | # The last line of a project definition. 50 | end_project = re.compile("^EndProject$") 51 | # The first line of a dependency list. 52 | begin_dep = re.compile(r"ProjectSection\(ProjectDependencies\) = postProject$") 53 | # The last line of a dependency list. 54 | end_dep = re.compile("EndProjectSection$") 55 | # A line describing a dependency. 56 | dep_line = re.compile(" *({.*}) = ({.*})$") 57 | 58 | in_deps = False 59 | solution = open(solution_file) 60 | for line in solution: 61 | results = begin_project.search(line) 62 | if results: 63 | # Hack to remove icu because the diff is too different. 64 | if results.group(1).find("icu") != -1: 65 | continue 66 | # We remove "_gyp" from the names because it helps to diff them. 67 | current_project = results.group(1).replace("_gyp", "") 68 | projects[current_project] = [ 69 | results.group(2).replace("_gyp", ""), 70 | results.group(3), 71 | results.group(2), 72 | ] 73 | dependencies[current_project] = [] 74 | continue 75 | 76 | results = end_project.search(line) 77 | if results: 78 | current_project = None 79 | continue 80 | 81 | results = begin_dep.search(line) 82 | if results: 83 | in_deps = True 84 | continue 85 | 86 | results = end_dep.search(line) 87 | if results: 88 | in_deps = False 89 | continue 90 | 91 | results = dep_line.search(line) 92 | if results and in_deps and current_project: 93 | dependencies[current_project].append(results.group(1)) 94 | continue 95 | 96 | # Change all dependencies clsid to name instead. 97 | for project, deps in dependencies.items(): 98 | # For each dependencies in this project 99 | new_dep_array = [] 100 | for dep in deps: 101 | # Look for the project name matching this cldis 102 | for project_info in projects: 103 | if projects[project_info][1] == dep: 104 | new_dep_array.append(project_info) 105 | dependencies[project] = sorted(new_dep_array) 106 | 107 | return (projects, dependencies) 108 | 109 | 110 | def PrintDependencies(projects, deps): 111 | print("---------------------------------------") 112 | print("Dependencies for all projects") 113 | print("---------------------------------------") 114 | print("-- --") 115 | 116 | for (project, dep_list) in sorted(deps.items()): 117 | print("Project : %s" % project) 118 | print("Path : %s" % projects[project][0]) 119 | if dep_list: 120 | for dep in dep_list: 121 | print(" - %s" % dep) 122 | print() 123 | 124 | print("-- --") 125 | 126 | 127 | def PrintBuildOrder(projects, deps): 128 | print("---------------------------------------") 129 | print("Build order ") 130 | print("---------------------------------------") 131 | print("-- --") 132 | 133 | built = [] 134 | for (project, _) in sorted(deps.items()): 135 | if project not in built: 136 | BuildProject(project, built, projects, deps) 137 | 138 | print("-- --") 139 | 140 | 141 | def PrintVCProj(projects): 142 | 143 | for project in projects: 144 | print("-------------------------------------") 145 | print("-------------------------------------") 146 | print(project) 147 | print(project) 148 | print(project) 149 | print("-------------------------------------") 150 | print("-------------------------------------") 151 | 152 | project_path = os.path.abspath( 153 | os.path.join(os.path.dirname(sys.argv[1]), projects[project][2]) 154 | ) 155 | 156 | pretty = pretty_vcproj 157 | argv = [ 158 | "", 159 | project_path, 160 | "$(SolutionDir)=%s\\" % os.path.dirname(sys.argv[1]), 161 | ] 162 | argv.extend(sys.argv[3:]) 163 | pretty.main(argv) 164 | 165 | 166 | def main(): 167 | # check if we have exactly 1 parameter. 168 | if len(sys.argv) < 2: 169 | print('Usage: %s "c:\\path\\to\\project.sln"' % sys.argv[0]) 170 | return 1 171 | 172 | (projects, deps) = ParseSolution(sys.argv[1]) 173 | PrintDependencies(projects, deps) 174 | PrintBuildOrder(projects, deps) 175 | 176 | if "--recursive" in sys.argv: 177 | PrintVCProj(projects) 178 | return 0 179 | 180 | 181 | if __name__ == "__main__": 182 | sys.exit(main()) 183 | --------------------------------------------------------------------------------