pax_global_header00006660000000000000000000000064150136026000014503gustar00rootroot0000000000000052 comment=746e1b19ae95be22dd68312287ec44e6fb337888 zephyrproject-rtos-west-f42ad3c/000077500000000000000000000000001501360260000170505ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/.codecov.yml000066400000000000000000000006351501360260000212770ustar00rootroot00000000000000# Let CodeCov post a comment in PRs for the coverage of changes. comment: layout: "reach, diff, flags, files" behavior: default require_changes: false require_base: false require_head: true hide_project_coverage: false branches: - "main" # Do not block PRs (yet) coverage: status: project: default: informational: true patch: default: informational: true zephyrproject-rtos-west-f42ad3c/.github/000077500000000000000000000000001501360260000204105ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/.github/dependabot.yml000066400000000000000000000003711501360260000232410ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" commit-message: prefix: "ci: github: " labels: [] groups: actions-deps: patterns: - "*" zephyrproject-rtos-west-f42ad3c/.github/workflows/000077500000000000000000000000001501360260000224455ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/.github/workflows/codeql.yml000066400000000000000000000021761501360260000244450ustar00rootroot00000000000000name: "CodeQL" on: push: branches: [ "main", "v*-branch" ] pull_request: branches: [ "main", "v*-branch" ] schedule: - cron: '18 2 * * 6' permissions: contents: read jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ubuntu-24.04 permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: python build-mode: none steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Initialize CodeQL uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: category: "/language:${{matrix.language}}" zephyrproject-rtos-west-f42ad3c/.github/workflows/dependency-review.yml000066400000000000000000000014411501360260000266050ustar00rootroot00000000000000# Dependency Review Action # # This Action will scan dependency manifest files that change as part of a Pull Request, # surfacing known-vulnerable versions of the packages declared or updated in the PR. # Once installed, if the workflow run is marked as required, # PRs introducing known-vulnerable packages will be blocked from merging. # # Source repository: https://github.com/actions/dependency-review-action name: 'Dependency Review' on: [pull_request] permissions: contents: read jobs: dependency-review: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: 'Dependency Review' uses: actions/dependency-review-action@8805179dc9a63c54224914839d370dd93bd37b2e # v4 zephyrproject-rtos-west-f42ad3c/.github/workflows/format.yml000066400000000000000000000044211501360260000244610ustar00rootroot00000000000000name: Format check on: pull_request: branches: - main paths: - '**.py' permissions: contents: read jobs: find-changed-files: runs-on: ubuntu-latest outputs: files: ${{ steps.git-diff-files.outputs.files }} name: Detect added and changed files steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - name: Create json diff uses: GrantBirki/git-diff-action@f65a78c343ee50737aebbe653e35f3067752c7b3 # v2.8.0 id: git-diff with: base_branch: origin/main search_path: '**.py' json_diff_file_output: diff.json file_output_only: 'true' # Ignore deleted files git_options: '--no-color --diff-filter=d' - name: Convert json diff to matrix array id: git-diff-files env: JSON_DIFF: ${{ steps.git-diff.outputs.json-diff-path }} run: | # Github output expects oneliners, use compact mode files=$(cat $JSON_DIFF | jq -c -r '[.files[] | {path: .path}]') echo "files=$files" >> $GITHUB_OUTPUT ruff-format: needs: find-changed-files if: ${{ needs.find-changed-files.outputs.files != '[]' }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: files: ${{ fromJSON(needs.find-changed-files.outputs.files) }} name: Check file ${{ matrix.files.path }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Run ruff format check for ${{ matrix.files.path }} id: format-check uses: astral-sh/ruff-action@84f83ecf9e1e15d26b7984c7ec9cf73d39ffc946 # v3.3.1 # Allow the job run to pass when this step fails continue-on-error: true with: args: "format --check --diff" src: "${{ matrix.files.path }}" version: 0.8.1 - name: Annotate unformatted file if: ${{ steps.format-check.outcome }} == 'failure' run: | JOB_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" echo "::notice file=${{ matrix.files.path }},title=Unformatted file::Consider running 'ruff format ${{ matrix.files.path }}'%0ASee $JOB_URL for more details" zephyrproject-rtos-west-f42ad3c/.github/workflows/package.yml000066400000000000000000000014251501360260000245650ustar00rootroot00000000000000name: Python Package on: [push, pull_request, workflow_call] permissions: contents: read jobs: package: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.13" - name: Install Python dependencies run: | pip3 install build - name: Build a binary wheel and a source tarball run: | python -m build - name: Store the distribution packages uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: python-package-distributions path: dist/ zephyrproject-rtos-west-f42ad3c/.github/workflows/release.yml000066400000000000000000000022761501360260000246170ustar00rootroot00000000000000name: Release # This workflow follows Pypi guidelines for publishing using trusted publishers # See https://docs.pypi.org/trusted-publishers/using-a-publisher/ for more details. on: release: types: [published] permissions: contents: read jobs: package: name: Package uses: ./.github/workflows/package.yml release: name: Release needs: [package] runs-on: ubuntu-latest # Make the GH environment explicit environment: release permissions: # Mandatory for attaching assets to releases contents: write # Mandatory for trusted publishing id-token: write steps: - name: Download build artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: python-package-distributions path: dist/ # The assets can be attached to an existing release, if a matching tag is found - name: Upload release assets uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2 with: files: dist/*.whl - name: Publish package to PyPI uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1 zephyrproject-rtos-west-f42ad3c/.github/workflows/scorecards.yml000066400000000000000000000044671501360260000253330ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecards supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '43 7 * * 6' push: branches: - main permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed for Code scanning upload security-events: write # Needed for GitHub OIDC token if publish_results is true id-token: write steps: - name: "Checkout code" uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 with: results_file: results.sarif results_format: sarif # Publish results to OpenSSF REST API for easy access by consumers. # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable # uploads of run results in SARIF format to the repository Actions tab. # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts - name: "Upload artifact" uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17 with: sarif_file: results.sarif zephyrproject-rtos-west-f42ad3c/.github/workflows/test-distros.yml000066400000000000000000000044051501360260000256370ustar00rootroot00000000000000name: Python Test on Linux distros # This workflow should only run when preparing releases to make sure linux distros other than # ubuntu work as expected. # In pull requests only run if this file has been changed on: workflow_dispatch: pull_request: branches: - main paths: - '.github/workflows/test-distros.yml' # Running this workflow locally can be done using docker, with the following commands in # your local clone of the west repository: # 1. Start a docker container with (read-only) access to the source files # $ docker run -it -v $(pwd):/west:ro # # 2. Install, depending on the OS, git and python-pip, for example in Arch linux: # $ pacman -Syu --noconfirm git python-pip # # 3. Clone the repository to a temporary directory # $ git config --global --add safe.directory /west # $ git clone -q /west /west-tmp # # 4. Install tox # $ pip3 install tox --break-system-packages # # 5. Run tox # $ tox -c /west-tmp -- -W error permissions: contents: read jobs: build: runs-on: ubuntu-latest container: ${{ matrix.container }} strategy: fail-fast: false matrix: container: - "archlinux:latest" - "debian:stable" - "debian:testing" - "fedora:latest" - "fedora:rawhide" - "opensuse/tumbleweed:latest" - "ubuntu:devel" steps: - if: ${{ startsWith(matrix.container, 'archlinux') }} run: | pacman -Syu --noconfirm git python-pip - if: ${{ startsWith(matrix.container, 'debian') || startsWith(matrix.container, 'ubuntu') }} run: | apt-get update apt-get install -y git python3-pip python3-venv - if: ${{ startsWith(matrix.container, 'fedora') }} run: | dnf install -y git python3-pip - if: ${{ startsWith(matrix.container, 'opensuse') }} run: | zypper -n install git python3-pip python3-sqlite3 - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Display Python version run: python3 -c "import sys; print(sys.version); print(sys.platform)" - name: Install tox run: pip3 install tox --break-system-packages - name: Run tox run: tox -- -W error zephyrproject-rtos-west-f42ad3c/.github/workflows/test.yml000066400000000000000000000050201501360260000241440ustar00rootroot00000000000000name: Python Test on: [push, pull_request] permissions: contents: read # Cancel ongoing builds on new changes concurrency: group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: build: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # This is enough to find many quoting issues with: path: "./check out" - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: ${{ matrix.python-version }} - name: Display Python version run: python -c "import sys; print(sys.version); print(sys.platform)" - name: Install tox run: pip3 install tox - name: Run tox run: tox -c 'check out' -- -W error - name: Upload coverage reports uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-${{ matrix.os }}-${{ matrix.python-version }} path: "./check out/.coverage" include-hidden-files: true coverage-report: runs-on: ubuntu-latest needs: ["build"] steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.13' - name: Download all coverage artifacts uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - name: Install coverage run: pip3 install coverage - name: Create coverage report run: | coverage combine coverage-*/.coverage coverage xml - name: Upload combined coverage report uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-combined path: coverage.xml - name: Upload coverage to Codecov if: github.repository_owner == 'zephyrproject-rtos' uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} slug: ${{ github.repository_owner }}/west zephyrproject-rtos-west-f42ad3c/.gitignore000066400000000000000000000003111501360260000210330ustar00rootroot00000000000000build/ dist/ west.egg-info/ __pycache__/ .pytest_cache/ .eggs/ shippable/ .tox/ .coverage* .mypy_cache/ # htmlcov is generated by tox due to coverage gathering in pytest htmlcov/ .dir-locals.el .venv/ zephyrproject-rtos-west-f42ad3c/LICENSE000066400000000000000000000261351501360260000200640ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. zephyrproject-rtos-west-f42ad3c/MAINTAINERS.rst000066400000000000000000000145071501360260000213630ustar00rootroot00000000000000Notes for maintainers. Pre-release test plan --------------------- 0. If no release branch exists, fork the version numbers in the release branch (vX.Y-branch) and the main branch. See "Cutting a release branch", below, for details. The rest of these steps should be done in the release branch:: git checkout vX.Y-branch 1. Make tox happy on the following first-party non-Linux platforms: - Windows 10 - the latest macOS Do this by hand and check for any anomalous warnings in the output. Do not just trust CI. 2. Make tox happy on other popular Linux distributions: - Arch - the latest Ubuntu development release - Debian stable - Debian testing - the latest Fedora release - the latest Fedora rawhide release - the rolling openSUSE Tumbleweed release Automated infrastructure for doing this using docker is in the test-distros.yml workflow action on Github. Make sure to check the tox.log files mentioned in the output for any anomalous warnings. 3. Build alpha N (N=1 to start, then N=2 if you need more commits, etc.) and upload to pypi. See "Building and uploading the release wheels" below for a procedure. 4. Install the alpha on test platforms. :: pip3 install west==X.YaN 5. Create and update a default (Zephyr) workspace on all of the platforms from 1., using the installed alpha:: west init zephyrproject cd zephyrproject west update Make sure zephyrproject/zephyr has a branch checked out that matches the default branch used by zephyr itself. 6. Do the following Zephyr specific testing in the Zephyr workspace on all of the platforms from 1. Skip QEMU tests on non-Linux platforms, and make sure ZEPHYR_BASE is unset in the calling environment. :: west build -b qemu_x86 -s zephyr/samples/hello_world -d build-qemu-x86 west build -d build-qemu-x86 -t run west build -b qemu_cortex_m3 -s zephyr/samples/hello_world -d build-qemu-m3 west build -d build-qemu-m3 -t run # This example uses a Nordic board. Do this for as many boards # as you have access to / volunteers for. west build -b nrf52dk_nrf52832 -s zephyr/samples/hello_world -d build-nrf52 west flash -d build-nrf52 west debug -d build-nrf52 west debugserver -d build-nrf52 west attach -d build-nrf52 (It's still a pass if ``west build`` requires ``--pristine``.) 7. Assuming that all went well (if it didn't, go fix it and repeat): - update version in pyproject.toml to 'X.Y.Z' (i.e. drop the 'aN' suffix that denotes alpha N) - tag the release on GitHub (see "Tagging the release" for a procedure) - create a release on GitHub from the new tag by going to https://github.com/zephyrproject-rtos/west/releases clicking "Draft a new release", and following instructions - upload the release artifacts to PyPI (see "Building and uploading the release wheels" for a procedure) 8. Send email to the Zephyr lists, announce@ and users@, notifying them of the new release. Include 'git shortlog' data of the new commits since the last release to give credit to all contributors. Building and uploading the release wheels ----------------------------------------- Creating Pypi releases is done automatically from Github. After publishing a release on Github a release build is packaged and uploaded with the version specified in pyproject.toml. To do these steps manually, you need the zephyr-project PyPI credentials for the 'twine upload' command. :: git clean -ffdx pip3 install --upgrade build twine python -m build twine upload -u zephyr-project dist/* The 'git clean' step is important. We've anecdotally observed broken wheels being generated from dirty repositories. Check out [packaging.python.org](https://packaging.python.org/en/latest/tutorials/packaging-projects/#generating-distribution-archives) for more detailed instructions. Tagging the release ------------------- Create and push a GPG signed tag. :: git tag -a -s vX.Y.Z -m 'West vX.Y.Z Signed-off-by: Your Name ' git push origin vX.Y.Z Cutting a release branch ------------------------ This is how to cut a new release branch for minor version vX.Y. Summary of what happens: - before: - vX.Y-branch does not exist - main branch is at version X.(Y-1).99 - after: - vX.Y-branch exists and is at version X.Y.0a1 - main is at version X.Y.99 - west.manifest.SCHEMA_VERSION may be updated 1. Check the git logs since the last release:: git log vX.(Y-1).99..origin/main Decide if west.manifest.SCHEMA_VERSION needs an update: - SCHEMA_VERSION should be updated to X.Y if release vX.Y will have manifest syntax changes that earlier versions of west cannot parse. - SCHEMA_VERSION should *not* be changed for west vX.Y if the manifest syntax is fully compatible with what west vX.(Y-1) can handle. If you want to change SCHEMA_VERSION, send this as a pull request to the main branch and get it reviewed and merged. (This requires a PR and review even though the rest of the steps don't.) **Don't** introduce incompatible manifest changes in patch versions. That violates semantic versioning. Example: if v0.7.3 can parse a manifest, v0.7.2 should be able to parse it, too, and with the same results. 2. Create and push the release branch for minor version vX.Y.0, which is named "vX.Y-branch":: git checkout -b vX.Y-branch origin/main git push origin vX.Y-branch This should already contain the SCHEMA_VERSION change if one is needed. Subsequent fixes for patch versions vX.Y.Z should go to vX.Y-branch after being backported from main (or the other way around in case of an urgent hotfix). 3. In vX.Y-branch, in src/west/version.py, set __version__ to X.Y.0a1. Push this to origin/vX.Y-branch. You don't need a PR for this. 4. In the main branch, set version in pyproject.toml to X.Y.99. Push this to origin/main. You don't need a PR for this. 5. Create an annotated tag vX.Y.99 which points to the main branch commit you just created in the previous step. Push it to origin/main. You don't need a PR for this. See refs/tags/v0.12.99 for an example. (This makes 'git describe' output easy to read during development.) From this point forward, the main branch is moving independently from the release branch. Do the release prep work in the release branch. zephyrproject-rtos-west-f42ad3c/MANIFEST.in000066400000000000000000000001511501360260000206030ustar00rootroot00000000000000include src/west/west-commands-schema.yml include src/west/manifest-schema.yml include src/west/py.typed zephyrproject-rtos-west-f42ad3c/README.rst000066400000000000000000000105141501360260000205400ustar00rootroot00000000000000.. image:: https://img.shields.io/pypi/pyversions/west?logo=python :target: https://pypi.org/project/west/ .. image:: https://api.securityscorecards.dev/projects/github.com/zephyrproject-rtos/west/badge :target: https://scorecard.dev/viewer/?uri=github.com/zephyrproject-rtos/west .. image:: https://codecov.io/gh/zephyrproject-rtos/west/graph/badge.svg :target: https://codecov.io/gh/zephyrproject-rtos/west This is the Zephyr RTOS meta tool, ``west``. https://docs.zephyrproject.org/latest/guides/west/index.html Installation ------------ Using pip:: pip3 install west (Use ``pip3 uninstall west`` to uninstall it.) Basic Usage ----------- West lets you manage multiple Git repositories under a single directory using a single file, called the *west manifest file*, or *manifest* for short. By default the manifest file is named ``west.yml``. You use ``west init`` to set up this directory, then ``west update`` to fetch and/or update the repositories named in the manifest. By default, west uses `upstream Zephyr's manifest file `_, but west doesn't care if the manifest repository is zephyr or not. You can and are encouraged to make your own manifest repositories to meet your needs. For more details, see the `West guide `_ in the Zephyr documentation. Example usage using the upstream manifest file:: mkdir zephyrproject && cd zephyrproject west init west update What just happened: - ``west init`` clones the upstream *west manifest* repository, which in this case is the zephyr repository. The manifest repository contains ``west.yml``, a YAML description of the Zephyr installation, including Git repositories and other metadata. - ``west update`` clones the other repositories named in the manifest file, creating working trees in the installation directory ``zephyrproject``. Use ``west init -m`` to specify another manifest repository. Use ``--mr`` to use a revision to inialize from; if not given, the remote's default branch is used. Use ``--mf`` to use a manifest file other than ``west.yml``. Additional Commands ------------------- West has multiple sub-commands. After running ``west init``, you can run them from anywhere under ``zephyrproject``. For a list of available commands, run ``west -h``. Get help on a command with ``west -h``. West is extensible: you can add new commands to west without modifying its source code. See `Extensions `_ in the documentation for details. Running the Tests ----------------- First, install tox:: # macOS, Windows pip3 install tox # Linux pip3 install --user tox Then, run the test suite locally from the top level directory:: tox You can use ``--`` to tell tox to pass arguments to ``pytest``. This is especially useful to focus on specific tests and save time. Examples:: # Run a subset of tests tox -- tests/test_project.py # Debug the ``test_update_narrow()`` code with ``pdb`` (but _not_ the # west code which is running in subprocesses) tox -- --verbose --exitfirst --trace -k test_update_narrow # Run all tests with "import" in their name and let them log to the # current terminal tox -- -v -k import --capture=no The tests cannot be run with ``pytest`` directly, they require the tox environment. See the tox configuration file, tox.ini, for more details. Hacking on West --------------- This section contains notes for getting started developing west itself. Editable Install ~~~~~~~~~~~~~~~~ To run west "live" from the current source code tree, run this command from the top level directory in the west repository:: pip3 install -e . This is useful if you are actively working on west and don't want to re-package and install a wheel each time you run it. Installing from Source ~~~~~~~~~~~~~~~~~~~~~~ You can create and install a wheel package to install west as well. To build the west wheel file:: pip3 install --upgrade build python -m build This will create a file named ``dist/west-x.y.z-py3-none-any.whl``, where ``x.y.z`` is the current version in setup.py. To install the wheel:: pip3 install -U dist/west-x.y.z-py3-none-any.whl You can ``pip3 uninstall west`` to remove this wheel before re-installing the version from PyPI, etc. zephyrproject-rtos-west-f42ad3c/pyproject.toml000066400000000000000000000032121501360260000217620ustar00rootroot00000000000000[build-system] requires = ["setuptools>=61.2"] build-backend = "setuptools.build_meta" [project] name = "west" version = "1.4.0" authors = [{name = "Zephyr Project", email = "devel@lists.zephyrproject.org"}] description = "Zephyr RTOS Project meta-tool" classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", ] requires-python = ">=3.9" dependencies = [ "colorama", "PyYAML>=5.1", "pykwalify", "packaging", ] [project.license] file = "LICENSE" [project.readme] file = "README.rst" content-type = "text/x-rst" [project.urls] Homepage = "https://github.com/zephyrproject-rtos/west" [project.scripts] west = "west.app.main:main" [tool.setuptools] package-dir = {"" = "src"} zip-safe = false include-package-data = true [tool.setuptools.packages.find] where = ["src"] namespaces = false [tool.coverage.run] relative_files = true omit = [ "*/tmp/*", ] [tool.coverage.report] omit = [ "*/tmp/*", ] [tool.coverage.paths] source = [ "src/west", "*/site-packages/west", "*/src/west", ] [tool.ruff] line-length = 100 [tool.ruff.lint] extend-select = [ "B", # flake8-bugbear "E", # pycodestyle errors "F", # Pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.format] quote-style = "preserve" line-ending = "lf" zephyrproject-rtos-west-f42ad3c/src/000077500000000000000000000000001501360260000176375ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/src/west/000077500000000000000000000000001501360260000206215ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/src/west/__init__.py000066400000000000000000000003141501360260000227300ustar00rootroot00000000000000# Copyright (c) 2021 Nordic Semiconductor ASA import logging # https://docs.python.org/3/howto/logging.html#configuring-logging-for-a-library logging.getLogger('west').addHandler(logging.NullHandler()) zephyrproject-rtos-west-f42ad3c/src/west/__main__.py000066400000000000000000000000471501360260000227140ustar00rootroot00000000000000from west.app.main import main main() zephyrproject-rtos-west-f42ad3c/src/west/app/000077500000000000000000000000001501360260000214015ustar00rootroot00000000000000zephyrproject-rtos-west-f42ad3c/src/west/app/README.txt000066400000000000000000000004601501360260000230770ustar00rootroot00000000000000The west.app package contains the implementation of the west command line tool and its built-in features. Nothing in this package should be considered API. In particular, authors of west extension commands should not rely on the behavior this package or any of its contents to remain stable over time. zephyrproject-rtos-west-f42ad3c/src/west/app/__init__.py000066400000000000000000000000771501360260000235160ustar00rootroot00000000000000# Copyright (c) 2020, Nordic Semiconductor ASA # nothing here zephyrproject-rtos-west-f42ad3c/src/west/app/config.py000066400000000000000000000170141501360260000232230ustar00rootroot00000000000000# Copyright (c) 2019, Nordic Semiconductor ASA # # SPDX-License-Identifier: Apache-2.0 '''West config commands''' import argparse from west.commands import CommandError, WestCommand from west.configuration import ConfigFile CONFIG_DESCRIPTION = '''\ West configuration file handling. West follows Git-like conventions for configuration file locations. There are three types of configuration file: system-wide files apply to all users on the current machine, global files apply to the current user, and local files apply to the current west workspace. System files: - Linux: /etc/westconfig - macOS: /usr/local/etc/westconfig - Windows: %PROGRAMDATA%\\west\\config Global files: - Linux: ~/.westconfig or (if $XDG_CONFIG_HOME is set) $XDG_CONFIG_HOME/west/config - macOS: ~/.westconfig - Windows: .westconfig in the user's home directory, as determined by os.path.expanduser. Local files: - Linux, macOS, Windows: /.west/config You can override these files' locations with the WEST_CONFIG_SYSTEM, WEST_CONFIG_GLOBAL, and WEST_CONFIG_LOCAL environment variables. Configuration values from later configuration files override configuration from earlier ones. Local values have highest precedence, and system values lowest. To get a value for , type: west config To set a value for , type: west config To append to a value for , type: west config -a A value must exist in the selected configuration file in order to be able to append to it. The existing value can be empty. Examples: west config -a build.cmake-args -- " -DEXTRA_CFLAGS='-Wextra -g0' -DFOO=BAR" west config -a manifest.group-filter ,+optional To list all options and their values: west config -l To delete in the local or global file (wherever it's set first, not in both; if set locally, global values become visible): west config -d To delete in the global file only: west config -d --global To delete everywhere it's set, including the system file: west config -D ''' CONFIG_EPILOG = '''\ If the configuration file to use is not set, reads use all three in precedence order, and writes (including appends) use the local file.''' ALL = ConfigFile.ALL SYSTEM = ConfigFile.SYSTEM GLOBAL = ConfigFile.GLOBAL LOCAL = ConfigFile.LOCAL class Config(WestCommand): def __init__(self): super().__init__( 'config', 'get or set config file values', CONFIG_DESCRIPTION, requires_workspace=False) def do_add_parser(self, parser_adder): parser = parser_adder.add_parser( self.name, help=self.help, formatter_class=argparse.RawDescriptionHelpFormatter, description=self.description, epilog=CONFIG_EPILOG) group = parser.add_argument_group( "action to perform (give at most one)" ).add_mutually_exclusive_group() group.add_argument('-l', '--list', action='store_true', help='list all options and their values') group.add_argument('-d', '--delete', action='store_true', help='delete an option in one config file') group.add_argument('-D', '--delete-all', action='store_true', help="delete an option everywhere it's set") group.add_argument('-a', '--append', action='store_true', help='append to an existing value') group = parser.add_argument_group( "configuration file to use (give at most one)" ).add_mutually_exclusive_group() group.add_argument('--system', dest='configfile', action='store_const', const=SYSTEM, help='system-wide file') group.add_argument('--global', dest='configfile', action='store_const', const=GLOBAL, help='global (user-wide) file') group.add_argument('--local', dest='configfile', action='store_const', const=LOCAL, help="this workspace's file") parser.add_argument('name', nargs='?', help='''config option in section.key format; e.g. "foo.bar" is section "foo", key "bar"''') parser.add_argument('value', nargs='?', help='value to set "name" to') return parser def do_run(self, args, user_args): delete = args.delete or args.delete_all if args.list: if args.name: self.parser.error('-l cannot be combined with name argument') elif not args.name: self.parser.error('missing argument name ' '(to list all options and values, use -l)') elif args.append: if args.value is None: self.parser.error('-a requires both name and value') if args.list: self.list(args) elif delete: self.delete(args) elif args.value is None: self.read(args) elif args.append: self.append(args) else: self.write(args) def list(self, args): what = args.configfile or ALL for option, value in self.config.items(configfile=what): self.inf(f'{option}={value}') def delete(self, args): self.check_config(args.name) if args.delete_all: configfiles = [ALL] elif args.configfile: configfiles = [args.configfile] else: # local or global, whichever comes first configfiles = [LOCAL, GLOBAL] for i, configfile in enumerate(configfiles): try: self.config.delete(args.name, configfile=configfile) return except KeyError as err: if i == len(configfiles) - 1: self.dbg( f'{args.name} was not set in requested location(s)') raise CommandError(returncode=1) from err except PermissionError as pe: self._perm_error(pe, configfile, args.name) def check_config(self, option): if '.' not in option: self.die(f'invalid configuration option "{option}"; ' 'expected "section.key" format') def read(self, args): self.check_config(args.name) value = self.config.get(args.name, configfile=args.configfile or ALL) if value is not None: self.inf(value) else: self.err(f'{args.name} is unset') raise CommandError(returncode=1) def append(self, args): self.check_config(args.name) where = args.configfile or LOCAL value = self.config.get(args.name, configfile=where) if value is None: self.die(f'option {args.name} not found in the {where.name.lower()} ' 'configuration file') args.value = value + args.value self.write(args) def write(self, args): self.check_config(args.name) what = args.configfile or LOCAL try: self.config.set(args.name, args.value, configfile=what) except PermissionError as pe: self._perm_error(pe, what, args.name) def _perm_error(self, pe, what, name): rootp = ('; are you root/administrator?' if what in [SYSTEM, ALL] else '') self.die(f"can't update {name}: " f"permission denied when writing {pe.filename}{rootp}") zephyrproject-rtos-west-f42ad3c/src/west/app/main.py000077500000000000000000001417671501360260000227220ustar00rootroot00000000000000#!/usr/bin/env python3 # Copyright 2018 Open Source Foundries Limited. # Copyright 2019 Foundries.io Limited. # Copyright (c) 2019, Nordic Semiconductor ASA # # SPDX-License-Identifier: Apache-2.0 '''Zephyr RTOS meta-tool (west) main module Nothing in here is public API. ''' import argparse import logging import os import platform import shlex import shutil import signal import sys import tempfile import textwrap import traceback from collections import OrderedDict from io import StringIO from pathlib import Path, PurePath from subprocess import CalledProcessError from typing import NamedTuple, Optional import colorama import west.configuration from west import log from west.app.config import Config from west.app.project import ( Compare, Diff, ForAll, Grep, Init, List, ManifestCommand, SelfUpdate, Status, Topdir, Update, ) from west.commands import ( CommandError, ExtensionCommandError, Verbosity, WestCommand, extension_commands, ) from west.manifest import ( MANIFEST_REV_BRANCH, MalformedConfig, MalformedManifest, Manifest, ManifestImportFailed, ManifestProject, ManifestVersionError, _ManifestImportDepth, ) from west.util import WestNotFound, quote_sh_list, west_topdir from west.version import __version__ class EarlyArgs(NamedTuple): # Data type for storing "early" argument parsing results. # # We do some manual parsing of the command-line arguments before # delegating the hard parts to argparse. This extra work figures # out the command name, verbosity level, etc. # # This is necessary for: # # - nicer error-handling in situations where the command is an # extension but we're not in a workspace # # - setting up log levels from the verbosity level # Expected arguments: help: bool # True if -h was given version: bool # True if -V was given zephyr_base: Optional[str] # -z argument value verbosity: int # 0 if not given, otherwise counts command_name: Optional[str] # Other arguments are appended here. unexpected_arguments: list[str] def parse_early_args(argv: list[str]) -> EarlyArgs: # Hand-rolled argument parser for early arguments. help = False version = False zephyr_base = None verbosity = 0 command_name = None unexpected_arguments = [] expecting_zephyr_base = False def consume_more_args(rest): # Handle the 'Vv' portion of 'west -hVv'. nonlocal help, version, zephyr_base, verbosity nonlocal expecting_zephyr_base if not rest: return if rest.startswith('h'): help = True consume_more_args(rest[1:]) elif rest.startswith('V'): version = True consume_more_args(rest[1:]) elif rest.startswith('v'): verbosity += 1 consume_more_args(rest[1:]) elif rest.startswith('q'): verbosity -= 1 consume_more_args(rest[1:]) elif rest.startswith('z'): if not rest[1:]: expecting_zephyr_base = True elif rest[1] == '=': zephyr_base = rest[2:] else: zephyr_base = rest[1:] else: unexpected_arguments.append(rest) for arg in argv: if expecting_zephyr_base: zephyr_base = arg elif arg.startswith('-h'): help = True consume_more_args(arg[2:]) elif arg.startswith('-V'): version = True consume_more_args(arg[2:]) elif arg == '--version': version = True elif arg.startswith('-v'): verbosity += 1 consume_more_args(arg[2:]) elif arg.startswith('-q'): verbosity -= 1 consume_more_args(arg[2:]) elif arg == '--verbose': verbosity += 1 elif arg == '--quiet': verbosity -= 1 elif arg.startswith('-z'): if arg == '-z': expecting_zephyr_base = True elif arg.startswith('-z='): zephyr_base = arg[3:] else: zephyr_base = arg[2:] elif arg.startswith('-'): unexpected_arguments.append(arg) else: command_name = arg break return EarlyArgs(help, version, zephyr_base, verbosity, command_name, unexpected_arguments) class LogFormatter(logging.Formatter): def __init__(self): super().__init__(fmt='%(name)s: %(levelname)s: %(message)s') class LogHandler(logging.Handler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setFormatter(LogFormatter()) def emit(self, record): formatted = self.format(record) if record.levelno >= logging.WARNING: print(formatted, file=sys.stderr) else: print(formatted) class WestApp: # The west 'application' object. # # There's enough state to keep track of when building the final # WestCommand we want to run that it's convenient to have an # object to stash it all in. # # We could use globals, but that would make it harder to white-box # test multiple main() invocations from the same Python process, # which is a goal. See #149. def __init__(self): self.topdir = None # west_topdir() self.config = None # west.configuration.Configuration self.manifest = None # west.manifest.Manifest self.mle = None # saved exception if load_manifest() fails self.builtins = {} # command name -> WestCommand instance self.extensions = {} # extension command name -> spec self.aliases = {} # alias -> WestCommand instance self.builtin_groups = OrderedDict() # group name -> WestCommand list self.extension_groups = OrderedDict() # project path -> ext spec list self.west_parser = None # a WestArgumentParser self.subparser_gen = None # an add_subparsers() return value self.cmd = None # west.commands.WestCommand, eventually self.queued_io = [] # I/O hooks we want self.cmd to do for group, classes in BUILTIN_COMMAND_GROUPS.items(): lst = [cls() for cls in classes] self.builtins.update({command.name: command for command in lst}) self.builtin_groups[group] = lst # Give the help instance a back-pointer up here. # # A dirty layering violation, but it does need this data: # # - 'west help ' needs to call into 's # parser's print_help() # - 'west help' needs self.west_parser, which # the argparse API does not give us a future-proof way # to access from the Help object's parser attribute, # which comes from subparser_gen. self.builtins['help'].app = self def run(self, argv): # Run the command-line application with argument list 'argv'. early_args = parse_early_args(argv) # Silence validation errors from pykwalify, which are logged at # logging.ERROR level. We want to handle those ourselves as # needed. logging.getLogger('pykwalify').setLevel(logging.CRITICAL) # Use verbosity to determine west API log levels self.setup_west_logging(early_args.verbosity) # Makes ANSI color escapes work on Windows, and strips them when # stdout/stderr isn't a terminal colorama.init() # See if we're in a workspace. It's fine if we're not. # Note that this falls back on searching from ZEPHYR_BASE # if the current directory isn't inside a west workspace. try: self.topdir = west_topdir() except WestNotFound: pass # Read the configuration files. We need this to get # manifest.path to parse the manifest, etc. self.config = west.configuration.Configuration(topdir=self.topdir) # Also set up the global configuration object to match, for # backwards compatibility. self.config._copy_to_configparser(west.configuration.config) # Set self.manifest and self.extensions. self.load_manifest() self.load_extension_specs() self.load_aliases() # Set up initial argument parsers. This requires knowing # self.extensions, so it can't happen before now. self.setup_parsers() # OK, we are all set. Run the command. self.run_command(argv, early_args) def load_manifest(self): # Try to parse the manifest. We'll save it if that works, so # it doesn't have to be re-parsed. if not self.topdir: return try: self.manifest = Manifest.from_topdir(topdir=self.topdir, config=self.config) except (ManifestVersionError, MalformedManifest, MalformedConfig, ManifestImportFailed, FileNotFoundError, PermissionError) as e: # Defer exception handling to WestCommand.run(), which uses # handle_builtin_manifest_load_err() to decide what to do. # # Make sure to update that function if you change the # exceptions caught here. Unexpected exceptions should # propagate up and fail fast. # # This might be OK, e.g. if we're running 'west config # manifest.path foo' to fix the MalformedConfig error, but # there's no way to know until we've parsed the command # line arguments. self.mle = e def handle_builtin_manifest_load_err(self, args): # Deferred handling for expected load_manifest() exceptions. # Called before attempting to run a built-in command. (No # extension commands can be run, because we learn about them # from the manifest itself, which we have failed to load.) # A few commands are always safe to run without a manifest. # The update command is sometimes safe and sometimes not, but # we need to include it in this list because it's the only way # to fix a manifest-rev revision in a project which is being # imported to point from a bogus manifest to a non-bogus one. no_manifest_ok = ['help', 'config', 'topdir', 'init', 'manifest', 'update'] # Handle ManifestVersionError is a special case. if isinstance(self.mle, ManifestVersionError): if args.command == 'help': self.queued_io.append( lambda cmd: cmd.wrn(mve_msg(self.mle, suggest_upgrade=False) + '\n Cannot get extension command help, ' + "and most commands won't run." + '\n To silence this warning, upgrade west.')) return elif args.command in ['config', 'topdir']: # config and topdir are safe to run, but let's # warn the user that most other commands won't be. self.queued_io.append( lambda cmd: cmd.wrn(mve_msg(self.mle, suggest_upgrade=False) + "\n This should work, but most commands won't." + '\n To silence this warning, upgrade west.')) return elif args.command == 'init': # init is fine to run -- it will print its own error, # with context about where the workspace was found, # and what the user's choices are. return else: self.queued_io.append(lambda cmd: cmd.die(mve_msg(self.mle))) return # Other errors generally just fall back on no_manifest_ok. def isinst(*args): return any(isinstance(self.mle, t) for t in args) if args.command not in no_manifest_ok: if isinst(MalformedManifest, MalformedConfig): self.queued_io.append( lambda cmd: cmd.die("can't load west manifest: " + "\n".join(list(self.mle.args)))) elif isinst(_ManifestImportDepth): self.queued_io.append( lambda cmd: cmd.die( 'recursion depth exceeded during manifest resolution; ' 'your manifest likely contains an import loop. ' 'Run "west -v manifest --resolve" to debug.')) elif isinst(ManifestImportFailed): if args.command == 'update': return # that's fine self.queued_io.append(lambda cmd: cmd.die(mie_msg(self.mle))) elif isinst(FileNotFoundError): # This should ordinarily only happen when the top # level manifest is not found. self.queued_io.append( lambda cmd: cmd.die(f"manifest file not found: {self.mle.filename}\n" "Please check manifest.file and manifest.path in " f"{self.topdir + '/' or ''}.west/config")) elif isinst(PermissionError): self.queued_io.append( lambda cmd: cmd.die("permission denied when loading manifest file: " f"{self.mle.filename}")) else: self.queued_io.append( lambda cmd: cmd.die('internal error:', f'unhandled manifest load exception: {self.mle}')) def load_extension_specs(self): if self.manifest is None: # "None" means "extensions could not be determined". # Leaving this an empty dict would mean "there are no # extensions", which is different. self.extensions = None return try: path_specs = extension_commands(self.config, manifest=self.manifest) except ExtensionCommandError as ece: self.handle_extension_command_error(ece) extension_names = set() for path, specs in path_specs.items(): # Filter out attempts to shadow built-in commands as well as # command names which are already used. filtered = [] for spec in specs: if spec.name in self.builtins: self.queued_io.append( lambda cmd, spec_const=spec: cmd.wrn( f'ignoring project {spec_const.project.name} ' f'extension command "{spec_const.name}"; ' 'this is a built in command')) continue if spec.name in extension_names: self.queued_io.append( lambda cmd, spec_const=spec: cmd.wrn( f'ignoring project {spec_const.project.name} ' f'extension command "{spec_const.name}"; ' f'command "{spec_const.name}" is ' 'already defined as extension command')) continue filtered.append(spec) extension_names.add(spec.name) self.extensions[spec.name] = spec self.extension_groups[path] = filtered def load_aliases(self): if not self.config: return self.aliases = { k[6:]: Alias(k[6:], v) for k, v in self.config.items() if k.startswith('alias.') } def handle_extension_command_error(self, ece): if self.cmd is not None: msg = f"extension command \"{self.cmd.name}\" couldn't be run" else: msg = "could not load extension command(s)" if ece.hint: msg += '\n Hint: ' + ece.hint if self.cmd and self.cmd.verbosity >= Verbosity.DBG_EXTREME: self.cmd.err(msg, fatal=True) self.cmd.banner('Traceback (enabled by -vvv):') traceback.print_exc() else: tb_file = dump_traceback() msg += f'\n See {tb_file} for a traceback.' self.cmd.err(msg, fatal=True) sys.exit(ece.returncode) def setup_parsers(self): # Set up and install command-line argument parsers. west_parser, subparser_gen = self.make_parsers() real_command_names = set() # Add sub-parsers for the built-in commands. for name, command in self.builtins.items(): real_command_names.add(name) command.add_parser(subparser_gen) # Add stub parsers for extensions. # # These just reserve the names of each extension. The real parser # for each extension can't be added until we import the # extension's code, which we won't do unless parse_known_args() # says to run that extension. if self.extensions: for specs in self.extension_groups.values(): for spec in specs: real_command_names.add(spec.name) subparser_gen.add_parser(spec.name, add_help=False) # Add aliases, but skip aliases that shadow other commands # The help parser requires unique commands to be added if self.aliases: for name, alias in self.aliases.items(): # Advanced users shadowing real commands do not get "alias help" if name not in real_command_names: alias.add_parser(subparser_gen) # Save the instance state. self.west_parser = west_parser self.subparser_gen = subparser_gen def make_parsers(self): # Make a fresh instance of the top level argument parser # and subparser generator, and return them in that order. # The prog='west' override avoids the absolute path of the # main.py script showing up when West is run via the wrapper parser = WestArgumentParser( prog='west', description='The Zephyr RTOS meta-tool.', epilog='''Run "west help " for help on each .''', add_help=False, west_app=self, allow_abbrev=False) # Remember to update zephyr's west-completion.bash if you add or # remove flags. This is currently the only place where shell # completion is available. # # If you update these, also update parse_early_args(). parser.add_argument('-h', '--help', action=WestHelpAction, nargs=0, help='get help for west or a command') parser.add_argument('-z', '--zephyr-base', default=None, help='''Override the Zephyr base directory. The default is the manifest project with path "zephyr".''') parser.add_argument('-v', '--verbose', default=0, action='count', help='''Display verbose output. May be given multiple times to increase verbosity.''') parser.add_argument('-q', '--quiet', default=0, action='count', help='''Display less verbose output. May be given multiple times to decrease verbosity.''') parser.add_argument('-V', '--version', action='version', version=f'West version: v{__version__}', help='print the program version and exit') subparser_gen = parser.add_subparsers(metavar='', dest='command') return parser, subparser_gen def run_command(self, argv, early_args): # Parse command line arguments and run the WestCommand. # If we're running an extension, instantiate it from its # spec and re-parse arguments before running. if not early_args.help and early_args.command_name != "help": # Recursively replace alias command(s) if set aliases = self.aliases.copy() while early_args.command_name in aliases: # Make sure we don't end up in an infinite loop alias = aliases.pop(early_args.command_name) self.queued_io.append(lambda cmd, alias=alias: cmd.dbg( f'Replacing alias {alias.name} with {alias.args}' )) if len(alias.args) == 0: # This loses the cmd.dbg() above - too bad, don't use empty aliases self.print_usage_and_exit(f'west: empty alias "{alias.name}"') # Find and replace the command name. Must skip any other early args like -v for i, arg in enumerate(argv): if arg == early_args.command_name: argv = argv[:i] + alias.args + argv[i + 1:] break early_args = early_args._replace(command_name=alias.args[0]) self.handle_early_arg_errors(early_args) args, unknown = self.west_parser.parse_known_args(args=argv) # Set up logging verbosity before running the command, for # backwards compatibility. Remove this when we can part ways # with the log module. log.set_verbosity(args.verbose - args.quiet) # If we were run as 'west -h ...' or 'west --help ...', # monkeypatch the args namespace so we end up running Help. The # user might have also provided a command. If so, print help about # that command. if args.help or args.command is None: args.command_name = args.command args.command = 'help' # Finally, run the command. try: # Both run_builtin() and run_extension() set self.cmd so # we can use it in the exception handling blocks below. if args.command in self.builtins: self.run_builtin(args, unknown) else: self.run_extension(args.command, argv) except KeyboardInterrupt: # Catching this avoids dumping stack. # # Here we replicate CPython's behavior in exit_sigint() in # Modules/main.c (as of # 2f62a5da949cd368a9498e6a03e700f4629fa97f), but in pure # Python since it's not clear how or if we can call that # directly from here. # # For more discussion on this behavior, see: # # https://bugs.python.org/issue1054041 if platform.system() == 'Windows': # The hex number is a standard value (STATUS_CONTROL_C_EXIT): # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/596a1078-e883-4972-9bbc-49e60bebca55 # # Subtracting 2**32 seems to be the convention for # making it fit in an int32_t. CONTROL_C_EXIT_CODE = 0xC000013A - 2**32 sys.exit(CONTROL_C_EXIT_CODE) else: # On Unix, just reinstate the default SIGINT handler # and send the signal again, overriding the CPython # KeyboardInterrupt stack dump. # # In addition to exiting with the correct "dying due # to SIGINT" status code (usually 130), this signals # to the calling environment that we were interrupted, # so that e.g. "while true; do west ... ; done" will # exit out of the entire while loop and not just # the west command. signal.signal(signal.SIGINT, signal.SIG_DFL) os.kill(os.getpid(), signal.SIGINT) except BrokenPipeError: sys.exit(0) except CalledProcessError as cpe: self.cmd.err(f'command exited with status {cpe.returncode}: ' f'{quote_sh_list(cpe.cmd)}', fatal=True) if self.cmd.verbosity >= Verbosity.DBG_EXTREME: self.cmd.banner('Traceback (enabled by -vvv):') traceback.print_exc() sys.exit(cpe.returncode) except ExtensionCommandError as ece: self.handle_extension_command_error(ece) except CommandError as ce: # No need to dump_traceback() here. The command is responsible # for logging its own errors. sys.exit(ce.returncode) except MalformedManifest as mm: # We can get here because 'west update' is allowed to run # even when an invalid manifest was detected, as a way to # try to fix a previous update that left 'manifest-rev' # branches pointing at revisions with invalid manifest # data in projects that get imported. self.cmd.die('\n '.join(str(arg) for arg in mm.args)) except WestNotFound as wnf: self.cmd.die(str(wnf)) def handle_early_arg_errors(self, early_args): # If early_args indicates we should error out, handle it # gracefully. This provides more user-friendly output than # argparse can do on its own. if (early_args.command_name and not early_args.help and (early_args.command_name not in self.builtins and (not self.extensions or early_args.command_name not in self.extensions))): self.handle_unknown_command(early_args.command_name) def handle_unknown_command(self, command_name): # "status" needs "-vv" to show git errors like "dubious ownership"; see #726 if self.topdir: extra_help = (f'workspace {self.topdir} does not define ' 'this extension command -- try "west help"' ' and "west -vv status"') else: extra_help = 'do you need to run this inside a workspace?' self.print_usage_and_exit(f'west: unknown command "{command_name}"; ' f'{extra_help}') def print_usage_and_exit(self, message): self.west_parser.print_usage(file=sys.stderr) sys.exit(message) def setup_west_logging(self, verbosity): logger = logging.getLogger('west.manifest') if verbosity >= 2: logger.setLevel(logging.DEBUG) elif verbosity == 1: logger.setLevel(logging.INFO) elif verbosity == 0: logger.setLevel(logging.WARNING) elif verbosity == -1: logger.setLevel(logging.ERROR) else: logger.setLevel(logging.CRITICAL) logger.addHandler(LogHandler()) def run_builtin(self, args, unknown): self.queued_io.append( lambda cmd: cmd.dbg('args namespace:', args, level=Verbosity.DBG_EXTREME)) self.cmd = self.builtins.get(args.command, self.builtins['help']) adjust_command_verbosity(self.cmd, args) if self.mle: self.handle_builtin_manifest_load_err(args) for io_hook in self.queued_io: self.cmd.add_pre_run_hook(io_hook) self.cmd.run(args, unknown, self.topdir, manifest=self.manifest, config=self.config) def run_extension(self, name, argv): # Check a program invariant. We should never get here # unless we were able to parse the manifest. That's where # information about extensions is loaded from. assert self.manifest is not None and self.mle is None, \ f'internal error: running extension "{name}" ' \ f'but got {self.mle}' self.cmd = self.extensions[name].factory() # Our original top level parser and subparser generator have some # garbage state that prevents us from registering the 'real' # command subparser. Just make new ones. west_parser, subparser_gen = self.make_parsers() self.cmd.add_parser(subparser_gen) # Parse arguments again. args, unknown = west_parser.parse_known_args(argv) adjust_command_verbosity(self.cmd, args) self.queued_io.append( lambda cmd: cmd.dbg('args namespace:', args, level=Verbosity.DBG_EXTREME)) for io_hook in self.queued_io: self.cmd.add_pre_run_hook(io_hook) # HACK: try to set ZEPHYR_BASE. # # Currently required by zephyr extensions like "west build". # # TODO: get rid of this. Instead: # # - support a WEST_DIR environment variable to specify the # workspace if we're not running under a .west directory # (controversial) # - make zephyr extensions that need ZEPHYR_BASE just set it # themselves (easy if above is OK, unnecessary if it isn't) self.set_zephyr_base(args) self.cmd.run(args, unknown, self.topdir, manifest=self.manifest, config=self.config) def set_zephyr_base(self, args): '''Ensure ZEPHYR_BASE is set Order of precedence: 1) Value given as command line argument 2) Value from environment setting: ZEPHYR_BASE 3) Value of zephyr.base setting in west config file 4) Project in the manifest with name, or path, "zephyr" (will be persisted as zephyr.base in the local config if found) Order of precedence between 2) and 3) can be changed with the setting zephyr.base-prefer. zephyr.base-prefer takes the values 'env' and 'configfile' If 2) and 3) have different values and zephyr.base-prefer is unset, a warning is printed.''' manifest = self.manifest topdir = self.topdir config = self.config if args.zephyr_base: # The command line --zephyr-base takes precedence over # everything else. zb = os.path.abspath(args.zephyr_base) zb_origin = 'command line' else: # If the user doesn't specify it concretely, then use ZEPHYR_BASE # from the environment or zephyr.base from west.configuration. # # (We will configure zephyr.base to the project that has path # 'zephyr' as a last resort here.) # # At some point, we need a more flexible way to set environment # variables based on manifest contents, but this is good enough # to get started with and to ask for wider testing. zb_env = os.environ.get('ZEPHYR_BASE') zb_prefer = config.get('zephyr.base-prefer') rel_zb_config = config.get('zephyr.base') if rel_zb_config is None: # Try to find a project named 'zephyr', or with path # 'zephyr' inside the workspace. projects = None try: projects = manifest.get_projects(['zephyr'], allow_paths=False) except ValueError: try: projects = manifest.get_projects([Path(topdir) / 'zephyr']) except ValueError: pass if projects: zephyr = projects[0] config.set('zephyr.base', zephyr.path) rel_zb_config = zephyr.path if rel_zb_config is not None: zb_config = Path(topdir) / rel_zb_config else: zb_config = None if zb_prefer == 'env' and zb_env is not None: zb = zb_env zb_origin = 'env' elif zb_prefer == 'configfile' and zb_config is not None: zb = str(zb_config) zb_origin = 'configfile' elif zb_env is not None: zb = zb_env zb_origin = 'env' try: different = (zb_config and not zb_config.samefile(zb_env)) except FileNotFoundError: different = (zb_config and (PurePath(zb_config)) != PurePath(zb_env)) if different: # The environment ZEPHYR_BASE takes precedence # over the config setting, but is different than # the zephyr.base config value. # # Therefore, issue a warning as the user might have # run zephyr-env.sh/cmd in some other zephyr # workspace and forgotten about it. self.queued_io.append( lambda cmd: cmd.wrn( f'ZEPHYR_BASE={zb_env} ' f'in the calling environment will be used,\n' f'but the zephyr.base config option in {topdir} ' f'is "{rel_zb_config}"\n' 'which implies a different ' f'ZEPHYR_BASE={zb_config}\n' f'To disable this warning in the future, execute ' f"'west config --global zephyr.base-prefer env'")) elif zb_config: zb = str(zb_config) zb_origin = 'configfile' else: zb = None zb_origin = None # No --zephyr-base, no ZEPHYR_BASE, and no zephyr.base. self.queued_io.append( lambda cmd: cmd.wrn( "can't find the zephyr repository\n" ' - no --zephyr-base given\n' ' - ZEPHYR_BASE is unset\n' ' - west config contains no zephyr.base setting\n' ' - no manifest project has name or path "zephyr"\n' '\n' " If this isn't a Zephyr workspace, you can " " silence this warning with something like this:\n" ' west config zephyr.base not-using-zephyr')) if zb is not None: os.environ['ZEPHYR_BASE'] = zb self.queued_io.append( lambda cmd: cmd.dbg(f'ZEPHYR_BASE={zb} (origin: {zb_origin})')) class Help(WestCommand): # west help implementation. def __init__(self): super().__init__('help', 'get help for west or a command', textwrap.dedent('''\ With an argument, prints help for that command. Without one, prints top-level help for west.'''), requires_workspace=False) def do_add_parser(self, parser_adder): parser = parser_adder.add_parser( self.name, help=self.help, description=self.description, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('command_name', nargs='?', default=None, help='name of command to get help for') return parser def do_run(self, args, ignored): assert self.app, "Help has no WestApp and can't do its job" app = self.app name = args.command_name if not name: app.west_parser.print_help(top_level=True) elif name == 'help': self.parser.print_help() elif name in app.builtins: app.builtins[name].parser.print_help() elif app.extensions is not None and name in app.extensions: # It's fine that we don't handle any errors here. The # exception handling block in app.run_command is in a # parent stack frame. app.run_extension(name, [name, '--help']) elif app.aliases is not None and name in app.aliases: app.aliases[name].parser.print_help() else: self.wrn(f'unknown command "{name}"') app.west_parser.print_help(top_level=True) if app.mle: self.wrn('your manifest could not be loaded, ' 'which may be causing this issue.\n' ' Try running "west update" or fixing the manifest.') class Alias(WestCommand): # An alias command, it does not run itself def __init__(self, cmd, args): super().__init__(cmd, args or '', f'An alias that expands to: {args}') self.args = shlex.split(args) # Pseudo-parser that will never actually run except for ".print_help()" def do_add_parser(self, parser_adder): parser = parser_adder.add_parser( self.name, help=self.help, description=self.description, add_help=False, formatter_class=argparse.RawDescriptionHelpFormatter) return parser def do_run(self, args, ignored): raise AssertionError("Alias command can't run directly") class WestHelpAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): # Just mark that help was requested. namespace.help = True class WestArgumentParser(argparse.ArgumentParser): # The argparse module is infuriatingly coy about its parser and # help formatting APIs, marking almost everything you need to # customize help output an "implementation detail". Even accessing # the parser's description and epilog attributes as we do here is # technically breaking the rules. # # Even though the implementation details have been pretty stable # since the module was first introduced in Python 3.2, let's avoid # possible headaches by overriding some "proper" argparse APIs # here instead of monkey-patching the module or breaking # abstraction barriers. This is duplicative but more future-proof. def __init__(self, *args, **kwargs): # The super constructor calls add_argument(), so this has to # come first as our override of that method relies on it. self.west_optionals = [] self.west_app = kwargs.pop('west_app', None) super().__init__(*args, **kwargs) def print_help(self, file=None, top_level=False): print(self.format_help(top_level=top_level), end='', file=file or sys.stdout) def format_help(self, top_level=False): # When top_level is True, we override the parent method to # produce more readable output, which separates commands into # logical groups. In order to print optionals, we rely on the # data available in our add_argument() override below. # # If top_level is False, it's because we're being called from # one of the subcommand parsers, and we delegate to super. if not top_level: return super().format_help() # Format the help to be at most 75 columns wide, the maximum # generally recommended by typographers for readability. # # If the terminal width (COLUMNS) is less than 75, use width # (COLUMNS - 2) instead, unless that is less than 30 columns # wide, which we treat as a hard minimum. width = min(75, max(shutil.get_terminal_size().columns - 2, 30)) with StringIO() as sio: def append(*strings): for s in strings: print(s, file=sio) append(self.format_usage(), self.description, '') append('optional arguments:') for wo in self.west_optionals: self.format_west_optional(append, wo, width) append('') for group, commands in self.west_app.builtin_groups.items(): if group is None: # Skip hidden commands. continue append(group + ':') for command in commands: self.format_command(append, command, width) append('') if self.west_app.extensions is None: if not self.west_app.mle: # This only happens when there is an error. # If there are simply no extensions, it's an empty dict. # If the user has already been warned about the error # because it's due to a ManifestVersionError, don't # warn them again. append('Cannot load extension commands; ' 'help for them is not available.') append('(To debug, try: "west -vv manifest --validate".)') append('') else: # TODO we may want to be more aggressive about loading # command modules by default: the current implementation # prevents us from formatting one-line help here. # # Perhaps a commands.extension_paranoid that if set, uses # thunks, and otherwise just loads the modules and # provides help for each command. # # This has its own wrinkle: we can't let a failed # import break the built-in commands. for _path, specs in self.west_app.extension_groups.items(): # This may occur in case a project defines commands already # defined, in which case it has been filtered out. if not specs: continue project = specs[0].project # they're all from this project append('extension commands from project ' f'{project.name} (path: {project.path}):') for spec in specs: self.format_extension_spec(append, spec, width) append('') if self.west_app.aliases: append('aliases:') for alias in self.west_app.aliases.values(): self.format_command(append, alias, width) append('') if self.epilog: append(self.epilog) return sio.getvalue() def format_west_optional(self, append, wo, width): metavar = wo['metavar'] options = wo['options'] help = wo.get('help') # Join the various options together as a comma-separated list, # with the metavar if there is one. That's our "thing". if metavar is not None: opt_str = ' ' + ', '.join(f'{o} {metavar}' for o in options) else: opt_str = ' ' + ', '.join(options) # Delegate to the generic formatter. self.format_thing_and_help(append, opt_str, help, width) def format_command(self, append, command, width): thing = f' {command.name}:' self.format_thing_and_help(append, thing, command.help, width) def format_extension_spec(self, append, spec, width): self.format_thing_and_help(append, ' ' + spec.name + ':', spec.help, width) def format_thing_and_help(self, append, thing, help, width): # Format help for some "thing" (arbitrary text) and its # corresponding help text an argparse-like way. help_offset = min(max(10, width - 20), 24) help_indent = ' ' * help_offset thinglen = len(thing) if help is None: # If there's no help string, just print the thing. append(thing) else: # Reflow the lines in help to the desired with, using # the help_offset as an initial indent. help = ' '.join(help.split()) help_lines = textwrap.wrap(help, width=width, initial_indent=help_indent, subsequent_indent=help_indent) if thinglen > help_offset - 1: # If the "thing" (plus room for a space) is longer # than the initial help offset, print it on its own # line, followed by the help on subsequent lines. append(thing) append(*help_lines) else: # The "thing" is short enough that we can start # printing help on the same line without overflowing # the help offset, so combine the "thing" with the # first line of help. help_lines[0] = thing + help_lines[0][thinglen:] append(*help_lines) def add_argument(self, *args, **kwargs): # Track information we want for formatting help. The argparse # module calls kwargs.pop(), so can't call super first without # losing data. optional = {'options': [], 'metavar': kwargs.get('metavar', None)} need_metavar = (optional['metavar'] is None and kwargs.get('action') in (None, 'store')) for arg in args: if not arg.startswith('-'): break optional['options'].append(arg) # If no metavar was given, the last option name is # used. By convention, long options go last, so this # matches the default argparse behavior. if need_metavar: optional['metavar'] = arg.lstrip('-').translate( {ord('-'): '_'}).upper() optional['help'] = kwargs.get('help') self.west_optionals.append(optional) # Let argparse handle the actual argument. super().add_argument(*args, **kwargs) def error(self, message): if self.west_app and self.west_app.mle: # If we have a known WestApp instance and the manifest # failed to load, then try to specialize the generic error # message we're getting from argparse to handle west-specific # errors better. app = self.west_app mle = self.west_app.mle if app.cmd: cmd = app.cmd else: # No app.cmd probably means that the user is # running an extension command that they expected # to work, but we don't know about because the # import failed and thus we have no manifest to # load extensions from. # # Just use the help command as a stand-in instead. # We have to manually patch up its config # attribute in order to do this outside of # do_run(). cmd = app.builtins['help'] cmd.config = west.configuration.Configuration( topdir=app.topdir) if isinstance(mle, ManifestVersionError): cmd.die(mve_msg(mle)) elif isinstance(mle, ManifestImportFailed): cmd.die(mie_msg(mle)) super().error(message=message) def mve_msg(mve, suggest_upgrade=True): # Helper for getting a message for a ManifestVersionError. return '\n '.join( [f'west v{mve.version} or later is required by the manifest', f'West version: v{__version__}'] + ([f'Manifest file: {mve.file}'] if mve.file else []) + (['Please upgrade west and retry.'] if suggest_upgrade else [])) def mie_msg(mie): # Helper for getting a message for a ManifestImportError. p, imp = mie.project, mie.imp ret = (f'failed manifest import in {p.name_and_path}:\n' f' Failed importing "{imp}"') if not isinstance(p, ManifestProject): # Try to be more helpful by explaining exactly # what west.manifest needs to happen before we can # resolve the missing import. ret += (f' from revision "{p.revision}"\n' f' Hint: {p.name} must be cloned, owned by the user and its ' f'{MANIFEST_REV_BRANCH} ref must point to a ' 'commit with the import data\n' ' To fix, run "west update. If it still fails, try "west -vv ..."') return ret def adjust_command_verbosity(command, args): command.verbosity = max( min(command.verbosity + args.verbose - args.quiet, Verbosity.DBG_EXTREME), Verbosity.QUIET ) def dump_traceback(): # Save the current exception to a file and return its path. fd, name = tempfile.mkstemp(prefix='west-exc-', suffix='.txt') os.close(fd) # traceback has no use for the fd with open(name, 'w') as f: traceback.print_exc(file=f) return name def main(argv=None): # Create the WestApp instance and let it run. app = WestApp() app.run(argv or sys.argv[1:]) # If you add a command here, make sure to think about how it should be # handled in case of ManifestVersionError or other reason the manifest # might fail to load (import error, configuration file error, etc.) BUILTIN_COMMAND_GROUPS = { 'built-in commands for managing git repositories': [ Init, Update, List, ManifestCommand, Compare, Diff, Status, ForAll, Grep, ], 'other built-in commands': [ Help, Config, Topdir, ], # None is for hidden commands we don't want to show to the user. None: [SelfUpdate] } if __name__ == "__main__": main() zephyrproject-rtos-west-f42ad3c/src/west/app/project.py000066400000000000000000002743651501360260000234420ustar00rootroot00000000000000# Copyright (c) 2018, 2019 Nordic Semiconductor ASA # Copyright 2018, 2019 Foundries.io # # SPDX-License-Identifier: Apache-2.0 '''West project commands''' import argparse import logging import os import shlex import shutil import subprocess import sys import textwrap import time from functools import partial from os.path import abspath, relpath from pathlib import Path, PurePath from time import perf_counter from urllib.parse import urlparse from west import util from west.commands import CommandError, Verbosity, WestCommand from west.configuration import Configuration from west.manifest import MANIFEST_REV_BRANCH as MANIFEST_REV from west.manifest import QUAL_MANIFEST_REV_BRANCH as QUAL_MANIFEST_REV from west.manifest import QUAL_REFS_WEST as QUAL_REFS from west.manifest import ( ImportFlag, Manifest, ManifestImportFailed, ManifestProject, Submodule, _manifest_content_at, ) from west.manifest import is_group as is_project_group # # Project-related or multi-repo commands, like "init", "update", # "diff", etc. # class _ProjectCommand(WestCommand): # Helper class which contains common code needed by various commands # in this file. def _parser(self, parser_adder, **kwargs): # Create and return a "standard" parser. kwargs['help'] = self.help kwargs['description'] = self.description kwargs['formatter_class'] = argparse.RawDescriptionHelpFormatter return parser_adder.add_parser(self.name, **kwargs) def _cloned_projects(self, args, only_active=False): # Returns _projects(args.projects, only_cloned=True) if # args.projects is not empty (i.e., explicitly given projects # are required to be cloned). Otherwise, returns all cloned # projects. if args.projects: ret = self._projects(args.projects, only_cloned=True) else: ret = [p for p in self.manifest.projects if p.is_cloned()] if args.projects or not only_active: return ret return [p for p in ret if self.manifest.is_active(p)] def _projects(self, ids, only_cloned=False): try: return self.manifest.get_projects(ids, only_cloned=only_cloned) except ValueError as ve: if len(ve.args) != 2: raise # not directly raised by get_projects() # Die with an error message on unknown or uncloned projects. unknown, uncloned = ve.args if unknown: self._die_unknown(unknown) elif only_cloned and uncloned: s = 's' if len(uncloned) > 1 else '' names = ' '.join(p.name for p in uncloned) self.die(f'uncloned project{s}: {names}.\n' ' Hint: run "west update" and retry.') else: # Should never happen, but re-raise to fail fast and # preserve a stack trace, to encourage a bug report. raise def _handle_failed(self, args, failed): # Shared code for commands (like status, diff, update) that need # to do the same thing to multiple projects, but collect # and report errors if anything failed. if not failed: self.dbg(f'git {self.name} failed for zero project') return elif len(failed) < 20: s = 's:' if len(failed) > 1 else '' projects = ', '.join(f'{p.name}' for p in failed) self.err(f'{self.name} failed for project{s} {projects}') else: self.err(f'{self.name} failed for multiple projects; see above') raise CommandError(1) def _die_unknown(self, unknown): # Scream and die about unknown projects. s = 's' if len(unknown) > 1 else '' names = ' '.join(unknown) self.die(f'unknown project name{s}/path{s}: {names}\n' ' Hint: use "west list" to list all projects.') def _has_nonempty_status(self, project): # Check if the project has any status output to print. We # manually use Popen in order to try to exit as quickly as # possible if 'git status' prints anything. popen = subprocess.Popen(['git', 'status', '--porcelain'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=project.abspath) def has_output(): # 'git status --porcelain' prints nothing if there # are no notable changes, so any output at all # means we should run 'git status' on the project. stdout, stderr = None, None try: stdout, stderr = popen.communicate(timeout=0.1) except subprocess.TimeoutExpired: pass return stdout or stderr while True: if has_output(): popen.kill() return True if popen.poll() is not None: break return has_output() class Init(_ProjectCommand): def __init__(self): super().__init__( 'init', 'create a west workspace', f'''\ Creates a west workspace. With -l, creates a workspace around an existing local repository; without -l, creates a workspace by cloning a manifest repository by URL. With -m, clones the repository at that URL and uses it as the manifest repository. If --mr is not given, the remote's default branch will be used, if it exists. With neither, -m {MANIFEST_URL_DEFAULT} is assumed. Warning: 'west init' renames and/or deletes temporary files inside the workspace being created. This fails on some filesystems when some development tool or any other program is trying to read/index these temporary files at the same time. For instance, it is required to stop Visual Studio Code before running 'west init` on the Windows NTFS filesystem. Find other, similar "Access is denied" examples in west issue #558. This is not required with most Linux filesystems that have an inode indirection layer and can wait to finalize the deletion until there is no concurrent user left. If you cannot identify or cannot stop the background scanner that is interfering with renames on your system, try the --rename-delay hack below. ''', requires_workspace=False) def do_add_parser(self, parser_adder): # We set a custom usage because there are two distinct ways # to call this command, and the default usage generated by # argparse doesn't make that very clear. parser = self._parser( parser_adder, usage=''' %(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [directory] %(prog)s -l [--mf FILE] directory ''') # Remember to update the usage if you modify any arguments. parser.add_argument('-m', '--manifest-url', help='''manifest repository URL to clone; cannot be combined with -l''') parser.add_argument('-o', '--clone-opt', action='append', default=[], help='''additional option to pass to 'git clone' (e.g. '-o=--depth=1'); may be given more than once; cannot be combined with -l''') parser.add_argument('--mr', '--manifest-rev', dest='manifest_rev', help='''manifest repository branch or tag name to check out first; cannot be combined with -l''') parser.add_argument('--mf', '--manifest-file', dest='manifest_file', help='manifest file name to use') parser.add_argument('-l', '--local', action='store_true', help='''use "directory" as an existing local manifest repository instead of cloning one from MANIFEST_URL; .west is created next to "directory" in this case, and manifest.path points at "directory"''') parser.add_argument('--rename-delay', type=int, help='''Number of seconds to wait before renaming some temporary directories. Some filesystems like NTFS cannot rename files in use; see above. This is a HACK that may or may not give enough time for some random background scanner to complete. ''') parser.add_argument( 'directory', nargs='?', default=None, help='''with -l, the path to the local manifest repository; without it, the directory to create the workspace in (defaulting to the current working directory in this case)''') return parser def do_run(self, args, _): if self.topdir: zb = os.environ.get('ZEPHYR_BASE') if zb: msg = textwrap.dedent(f''' Note: In your environment, ZEPHYR_BASE is set to: {zb} This forces west to search for a workspace there. Try unsetting ZEPHYR_BASE and re-running this command.''') else: west_dir = Path(self.topdir) / WEST_DIR msg = ("\n Hint: if you do not want a workspace there, \n" " remove this directory and re-run this command:\n\n" f" {west_dir}") self.die_already(self.topdir, msg) if args.local and (args.manifest_url or args.manifest_rev or args.clone_opt): self.die('-l cannot be combined with -m, -o or --mr') self.die_if_no_git() if args.local: topdir = self.local(args) else: topdir = self.bootstrap(args) self.banner(f'Initialized. Now run "west update" inside {topdir}.') def die_already(self, where, also=None): self.die(f'already initialized in {where}, aborting.{also or ""}') def local(self, args) -> Path: if args.manifest_rev is not None: self.die('--mr cannot be used with -l') # We need to resolve this to handle the case that args.directory # is '.'. In that case, Path('.').parent is just Path('.') instead of # Path('..'). # # https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent manifest_dir = Path(args.directory or os.getcwd()).resolve() manifest_filename = args.manifest_file or 'west.yml' manifest_file = manifest_dir / manifest_filename topdir = manifest_dir.parent rel_manifest = manifest_dir.name west_dir = topdir / WEST_DIR if not manifest_file.is_file(): self.die(f'can\'t init: no {manifest_filename} found in ' f'{manifest_dir}') self.banner('Initializing from existing manifest repository', rel_manifest) self.small_banner(f'Creating {west_dir} and local configuration file') self.create(west_dir) os.chdir(topdir) self.config = Configuration(topdir=topdir) self.config.set('manifest.path', os.fspath(rel_manifest)) self.config.set('manifest.file', manifest_filename) return topdir def bootstrap(self, args) -> Path: topdir = Path(abspath(args.directory or os.getcwd())) self.banner('Initializing in', topdir) manifest_url = args.manifest_url or MANIFEST_URL_DEFAULT if args.manifest_rev: # This works with tags, too. branch_opt = ['--branch', args.manifest_rev] else: branch_opt = [] west_dir = topdir / WEST_DIR try: already = util.west_topdir(topdir, fall_back=False) self.die_already(already) except util.WestNotFound: pass if not topdir.is_dir(): self.create(topdir, exist_ok=False) tempdir: Path = west_dir / 'manifest-tmp' if tempdir.is_dir(): self.dbg('removing existing temporary manifest directory', tempdir) shutil.rmtree(tempdir) # Test that we can rename and delete directories. For the vast # majority of users this is a no-op but some filesystem # permissions can be weird; see October 2024 example in west # issue #558. Git cloning can take a long time, so check this # first. Failing ourselves is not just faster, it's also much # clearer than when git is involved in the mix. tempdir.mkdir(parents=True) (tempdir / 'not empty').mkdir() # Ignore the --rename-delay hack here not to double the wait; # we only have a couple directories and no file at this point! tempdir2 = tempdir.parent / 'renamed tempdir' os.rename(tempdir, tempdir2) # No need to delete west_dir parent shutil.rmtree(tempdir2) # Clone the manifest repository into a temporary directory. try: self.small_banner( f'Cloning manifest repository from {manifest_url}' + (f', rev. {args.manifest_rev}' if args.manifest_rev else '')) self.check_call(['git', 'clone'] + branch_opt + args.clone_opt + [manifest_url, os.fspath(tempdir)]) except subprocess.CalledProcessError: shutil.rmtree(tempdir, ignore_errors=True) raise # Verify the manifest file exists. temp_manifest_filename = args.manifest_file or 'west.yml' temp_manifest = tempdir / temp_manifest_filename if not temp_manifest.is_file(): self.die(f'can\'t init: no {temp_manifest_filename} found in ' f'{tempdir}\n' f' Hint: check --manifest-url={manifest_url}' + (f' and --manifest-rev={args.manifest_rev}' if args.manifest_rev else '') + f' You may need to remove {west_dir} before retrying.') # Parse the manifest to get "self: path:", if it declares one. # Otherwise, use the URL. Ignore imports -- all we really # want to know is if there's a "self: path:" or not. manifest = Manifest.from_data(temp_manifest.read_text(encoding=Manifest.encoding), import_flags=ImportFlag.IGNORE) if manifest.yaml_path: manifest_path = manifest.yaml_path else: # We use PurePath() here in case manifest_url is a # windows-style path. That does the right thing in that # case, without affecting POSIX platforms, where PurePath # is PurePosixPath. manifest_path = PurePath(urlparse(manifest_url).path).name manifest_abspath = topdir / manifest_path # Some filesystems like NTFS can't rename files in use. # See west issue #558. Will ReFS address this? ren_delay = args.rename_delay if ren_delay is not None: self.inf(f"HACK: waiting {ren_delay} seconds before renaming {tempdir}") time.sleep(ren_delay) self.dbg('moving', tempdir, 'to', manifest_abspath, level=Verbosity.DBG_EXTREME) # As shutil.move() is used to relocate tempdir, if manifest_abspath # is an existing directory, tmpdir will be moved _inside_ it, instead # of _to_ that path - this must be avoided. If manifest_abspath exists # but is not a directory, then semantics depend on os.rename(), so # avoid that too... if manifest_abspath.exists(): self.die(f'target directory already exists ({manifest_abspath})') manifest_abspath.parent.mkdir(parents=True, exist_ok=True) try: shutil.move(os.fspath(tempdir), os.fspath(manifest_abspath)) except shutil.Error as e: self.die(e) self.small_banner('setting manifest.path to', manifest_path) self.config = Configuration(topdir=topdir) self.config.set('manifest.path', manifest_path) self.config.set('manifest.file', temp_manifest_filename) return topdir def create(self, directory: Path, exist_ok: bool = True) -> None: try: directory.mkdir(parents=True, exist_ok=exist_ok) except PermissionError: self.die(f'Cannot initialize in {directory}: permission denied') except FileExistsError: self.die(f'Cannot initialize in {directory}: it already exists') except Exception as e: self.die(f"Can't create {directory}: {e}") class List(_ProjectCommand): def __init__(self): super().__init__( 'list', 'print information about projects', textwrap.dedent('''\ Print information about projects in the west manifest, using format strings.''')) def do_add_parser(self, parser_adder): default_fmt = '{name:12} {path:28} {revision:40} {url}' parser = self._parser( parser_adder, epilog=f'''\ {ACTIVE_PROJECTS_HELP} Note: To list only inactive projects you can use --inactive. FORMAT STRINGS -------------- Projects are listed using a Python 3 format string. Arguments to the format string are accessed by name. The default format string is: "{default_fmt}" The following arguments are available: - name: project name in the manifest - description: project description in the manifest - url: full remote URL as specified by the manifest - path: the relative path to the project from the top level, as specified in the manifest where applicable - abspath: absolute and normalized path to the project - posixpath: like abspath, but in posix style, that is, with '/' as the separator character instead of '\\' - revision: project's revision as it appears in the manifest - sha: project's revision as a SHA. Note that use of this requires that the project has been cloned. - cloned: "cloned" if the project has been cloned, "not-cloned" otherwise - active: "active" if the project is currently active, "inactive" otherwise - clone_depth: project clone depth if specified, "None" otherwise - groups: project groups, as a comma-separated list ''') group = parser.add_mutually_exclusive_group(required=False) group.add_argument('-a', '--all', action='store_true', help='include inactive projects'), group.add_argument('-i', '--inactive', action='store_true', help='list only inactive projects'), parser.add_argument('--manifest-path-from-yaml', action='store_true', help='''print the manifest repository's path according to the manifest file YAML, which may disagree with the manifest.path configuration option'''), parser.add_argument('-f', '--format', default=default_fmt, help='''format string to use to list each project; see FORMAT STRINGS below.''') parser.add_argument('projects', metavar='PROJECT', nargs='*', help='''projects (by name or path) to operate on; see ACTIVE PROJECTS below''') return parser def do_run(self, args, user_args): def sha_thunk(project): self.die_if_no_git() if not project.is_cloned(): self.die( f'cannot get sha for uncloned project {project.name}; ' f'run "west update {project.name}" and retry') elif isinstance(project, ManifestProject): return f'{"N/A":40}' else: return project.sha(MANIFEST_REV) def cloned_thunk(project): self.die_if_no_git() return "cloned" if project.is_cloned() else "not-cloned" def active_thunk(project): self.die_if_no_git() return "active" if self.manifest.is_active(project) else "inactive" def delay(func, project): return DelayFormat(partial(func, project)) if args.inactive and args.projects: self.parser.error('-i cannot be combined with an explicit project ' 'list') for project in self._projects(args.projects): # Include the project based on the inactive flag. If the flag is # set, include only inactive projects. Otherwise, include only # active ones. include = self.manifest.is_active(project) != bool(args.inactive) # Skip not included projects unless the user said # --all or named some projects explicitly. if not (args.all or args.projects or include): self.dbg(f'{project.name}: skipping project') continue # Spelling out the format keys explicitly here gives us # future-proofing if the internal Project representation # ever changes. # # Using DelayFormat delays computing derived values, such # as SHAs, unless they are specifically requested, and then # ensures they are only computed once. try: if isinstance(project, ManifestProject): # Special-case the manifest repository while it's # still showing up in the 'projects' list. Yet # more evidence we should tackle #327. if args.manifest_path_from_yaml: path = self.manifest.yaml_path apath = (abspath(os.path.join(self.topdir, path)) if path else None) ppath = Path(apath).as_posix() if apath else None else: path = self.manifest.repo_path apath = self.manifest.repo_abspath ppath = self.manifest.repo_posixpath else: path = project.path apath = project.abspath ppath = project.posixpath result = args.format.format( name=project.name, description=project.description or "None", url=project.url or 'N/A', path=path, abspath=apath, posixpath=ppath, revision=project.revision or 'N/A', clone_depth=project.clone_depth or "None", cloned=delay(cloned_thunk, project), active=delay(active_thunk, project), sha=delay(sha_thunk, project), groups=','.join(project.groups)) except KeyError as e: # The raised KeyError seems to just put the first # invalid argument in the args tuple, regardless of # how many unrecognizable keys there were. self.die(f'unknown key "{e.args[0]}" in format string ' f'{shlex.quote(args.format)}') except IndexError: self.parser.print_usage() self.die(f'invalid format string {shlex.quote(args.format)}') except subprocess.CalledProcessError: self.die(f'subprocess failed while listing {project.name}') self.inf(result, colorize=False) class ManifestCommand(_ProjectCommand): # The slightly weird naming is to avoid a conflict with # west.manifest.Manifest. def __init__(self): super().__init__( 'manifest', 'manage the west manifest', textwrap.dedent('''\ Manages the west manifest. The following actions are available. You must give exactly one. - --resolve: print the current manifest with all imports applied, as an equivalent single manifest file. Any imported manifests must be cloned locally (with "west update"). - --freeze: like --resolve, but with all project revisions converted to their current SHAs, based on the latest manifest-rev branches. All projects must be cloned (with "west update"). - --validate: print an error and exit the process unsuccessfully if the current manifest cannot be successfully parsed. If the manifest can be parsed, print nothing and exit successfully. - --path: print the path to the top level manifest file. If this file uses imports, it will not contain all the manifest data. - --untracked: print all files and directories inside the workspace that are not tracked or managed by west. This effectively means any file or directory that is outside all of the projects' directories on disk (regardless of whether those projects are active or inactive). This is similar to `git status` for untracked files. The output format is relative to the current working directory and is stable and suitable as input for scripting. If the manifest file does not use imports, and all project revisions are SHAs, the --freeze and --resolve output will be identical after a "west update". '''), accepts_unknown_args=False) def do_add_parser(self, parser_adder): parser = self._parser(parser_adder) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--resolve', action='store_true', help='print the manifest with all imports resolved') group.add_argument('--freeze', action='store_true', help='''print the resolved manifest with SHAs for all project revisions''') group.add_argument('--validate', action='store_true', help='''validate the current manifest, exiting with an error if there are issues''') group.add_argument('--path', action='store_true', help="print the top level manifest file's path") group.add_argument('--untracked', action='store_true', help='''print all files and directories not managed or tracked by west''') group = parser.add_argument_group('options for --resolve and --freeze') group.add_argument('-o', '--out', help='output file, default is standard output') group.add_argument('--active-only', action='store_true', help='only resolve active projects') return parser def do_run(self, args, user_args): manifest = self.manifest dump_kwargs = {'default_flow_style': False, 'sort_keys': False} if args.validate: pass # nothing more to do elif args.resolve: if not args.active_only: self._die_if_manifest_project_filter('resolve') self._dump(args, manifest.as_yaml(active_only=args.active_only, **dump_kwargs)) elif args.freeze: if not args.active_only: self._die_if_manifest_project_filter('freeze') self._dump(args, manifest.as_frozen_yaml(active_only=args.active_only, **dump_kwargs)) elif args.untracked: self._untracked() elif args.path: self.inf(manifest.path) else: # Can't happen. raise RuntimeError(f'internal error: unhandled args {args}') def _die_if_manifest_project_filter(self, action): if self.config.get('manifest.project-filter') is not None: self.die(f'"west manifest --{action}" is not (yet) supported ' 'when the manifest.project-filter option is set. ' f'Add --active-only to {action} only the projects ' 'currently active in the workspace. Alternatively, ' 'please clear the project-filter configuration ' 'option and re-run this command, or contact the ' 'west developers if you have a use case for resolving ' 'the manifest while projects are made inactive by the ' 'project filter.') def _untracked(self): ''' "Performs a top-down search of the west topdir, ignoring every directory that corresponds to a west project. ''' ppaths = [] untracked = [] for project in self._projects(None): # We do not check for self.manifest.is_active(project) because # inactive projects are still considered "tracked directories". ppaths.append(Path(project.abspath)) # Since west tolerates nested projects (i.e. a project inside the directory # of another project) we must sort the project paths to ensure that we # hit the "enclosing" project first when iterating. ppaths.sort() def _find_untracked(directory): '''There are three cases for each element in a directory: - It's a project -> Do nothing, ignore the directory. - There are no projects inside -> add to untracked list. - It's not a project directory but there are some projects inside it -> recurse. The directory argument cannot be inside a project, otherwise all bets are off. ''' self.dbg(f'looking for untracked files/directories in: {directory}') for e in [e.absolute() for e in directory.iterdir()]: if not e.is_dir() or e.is_symlink(): untracked.append(e) continue self.dbg(f'processing directory: {e}') for ppath in ppaths: # We cannot use samefile() because it requires the file # to exist (not always the case with inactive or even # uncloned projects). if ppath == e: # We hit a project root directory, skip it. break elif e in ppath.parents: self.dbg(f'recursing into: {e}') _find_untracked(e) break else: # This is not a project and there is no project inside. # Add to untracked elements. untracked.append(e) continue # Avoid using Path.walk() since that returns all files and directories under # a particular directory, which is overkill in our case. Instead, recurse # only when required. _find_untracked(Path(self.topdir)) # Exclude the .west directory, which is maintained by west try: untracked.remove((Path(self.topdir) / Path(WEST_DIR)).resolve()) except ValueError: self.die(f'Directory {WEST_DIR} not found in workspace') # Sort the results for displaying to the user. untracked.sort() for u in untracked: # We cannot use Path.relative_to(p, walk_up=True) because the # walk_up parameter was only added in 3.12 self.inf(os.path.relpath(u, Path.cwd())) def _dump(self, args, to_dump): if args.out: with open(args.out, 'w') as f: f.write(to_dump) else: sys.stdout.write(to_dump) class Compare(_ProjectCommand): def __init__(self): super().__init__( 'compare', "compare project status against the manifest", textwrap.dedent('''\ Compare each project's working tree state against the results of the most recent successful "west update" command. This command prints output for a project if (and only if) at least one of the following is true: 1. its checked out commit (HEAD) is different than the commit checked out by the most recent successful "west update" (which the manifest-rev branch will point to) 2. its working tree is not clean (i.e. there are local uncommitted changes) 3. it has a local checked out branch (unless the configuration option compare.ignore-branches is true or --ignore-branches is given on the command line, either of which disable this) The command also prints output for the manifest repository if it has nonempty status. The output is meant to be human-readable, and may change. It is not a stable interface to write scripts against. This command requires git 2.22 or later.''') ) def do_add_parser(self, parser_adder): parser = self._parser(parser_adder, epilog=ACTIVE_CLONED_PROJECTS_HELP) parser.add_argument('projects', metavar='PROJECT', nargs='*', help='''projects (by name or path) to operate on; defaults to active cloned projects''') parser.add_argument('-a', '--all', action='store_true', help='include output for inactive projects') parser.add_argument('--exit-code', action='store_true', help='''exit with status code 1 if status output was printed for any project''') parser.add_argument('--ignore-branches', default=None, action='store_true', help='''skip output for projects with checked out branches and clean working trees if the branch is at the same commit as the last "west update"''') parser.add_argument('--no-ignore-branches', dest='ignore_branches', action='store_false', help='''overrides a previous --ignore-branches or any compare.ignore-branches configuration option''') return parser def do_run(self, args, ignored): self.die_if_no_git() if self.git_version_info < (2, 22): # This is for git branch --show-current. self.die('git version 2.22 or later is required') if args.ignore_branches is not None: self.ignore_branches = args.ignore_branches else: self.ignore_branches = \ self.config.getboolean('compare.ignore-branches', False) failed = [] printed_output = False for project in self._cloned_projects(args, only_active=not args.all): if isinstance(project, ManifestProject): # West doesn't track the relationship between the manifest # repository and any remote, but users are still interested # in printing output for comparisons that makes sense. if self._has_nonempty_status(project): try: self.compare(project) printed_output = True except subprocess.CalledProcessError: failed.append(project) continue # 'git status' output for all projects is noisy when there # are lots of projects. # # We avoid this problem in 2 steps: # # 1. Check if we need to print any output for the # project. # # 2. If so, run 'git status' on the project. Otherwise, # skip output for the project entirely. # # In verbose mode, we always print output. def has_checked_out_branch(project): if self.ignore_branches: return False return bool(project.git(['branch', '--show-current'], capture_stdout=True, capture_stderr=True).stdout.strip()) try: if not (self.verbosity >= Verbosity.DBG or has_checked_out_branch(project) or (project.sha(QUAL_MANIFEST_REV) != project.sha('HEAD')) or self._has_nonempty_status(project)): continue self.compare(project) printed_output = True except subprocess.CalledProcessError: failed.append(project) self._handle_failed(args, failed) if args.exit_code and printed_output: raise CommandError(1) def compare(self, project): self.banner(f'{project.name_and_path}:') self.print_rev_info(project) self.print_status(project) def print_rev_info(self, project): # For non-manifest repositories, print HEAD's and # manifest-rev's SHAs and commit titles. # # We force git not to print in color so west's colored # banner() separators stand out more in the output. if isinstance(project, ManifestProject): return def rev_info(rev): title = project.git( ['log', '-1', '--color=never', '--pretty=%h%d %s', '--decorate-refs-exclude=refs/heads/manifest-rev', rev], capture_stdout=True, capture_stderr=True).stdout.decode().rstrip() # "HEAD" is special; '--decorate-refs-exclude=HEAD' doesn't work. # Fortunately it's always first. return ( title.replace('(HEAD) ', '').replace('(HEAD, ', '(') .replace('(HEAD -> ', '(') ) head_info = rev_info('HEAD') # If manifest-rev is missing, we already failed earlier. manifest_rev_info = rev_info('manifest-rev') self.small_banner(f'manifest-rev: {manifest_rev_info}') self.inf(f' HEAD: {head_info}') def print_status(self, project): # `git status` shows `manifest-rev` "sometimes", see #643. if self.color_ui: color = '-c status.color=always' else: color = '' cp = project.git(f'{color} status', capture_stdout=True, capture_stderr=True) self.small_banner('status:') self.inf(textwrap.indent(cp.stdout.decode().rstrip(), ' ' * 4)) class Diff(_ProjectCommand): def __init__(self): super().__init__( 'diff', '"git diff" for one or more projects', '''Runs "git diff" on each of the specified projects. Unknown arguments are passed to "git diff".''', accepts_unknown_args=True, ) def do_add_parser(self, parser_adder): parser = self._parser(parser_adder, epilog=ACTIVE_CLONED_PROJECTS_HELP) parser.add_argument('projects', metavar='PROJECT', nargs='*', help='''projects (by name or path) to operate on; defaults to active cloned projects''') parser.add_argument('-a', '--all', action='store_true', help='include output for inactive projects') parser.add_argument('-m', '--manifest', action='store_true', help='show changes relative to "manifest-rev"') return parser def do_run(self, args, user_args): self.die_if_no_git() failed = [] no_diff = 0 # We may need to force git to use colors if the user wants them, # which it won't do ordinarily since stdout is not a terminal. color = ['--color=always'] if self.color_ui else [] for project in self._cloned_projects(args, only_active=not args.all): diff_commit = ( ['manifest-rev'] # see #719 and #747 # Special-case the manifest repository while it's # still showing up in the 'projects' list. Yet # more evidence we should tackle #327. if args.manifest and not isinstance(project, ManifestProject) else [] ) # Use paths that are relative to the base directory to make it # easier to see where the changes are cp = project.git(['diff', f'--src-prefix={project.path}/', f'--dst-prefix={project.path}/', '--exit-code'] + color + diff_commit, extra_args=user_args, capture_stdout=True, capture_stderr=True, check=False) # We cannot trust --exit-code alone, for instance merge # conflicts return 0 with (at least) git version 2.46.0. See # west issue #731 some_diff = cp.returncode == 1 or (cp.returncode == 0 and len(cp.stdout) > 0) if not some_diff: no_diff += 1 if some_diff or self.verbosity >= Verbosity.DBG: self.banner(f'diff for {project.name_and_path}:') self.inf(cp.stdout.decode('utf-8')) self.inf(cp.stderr.decode('utf-8')) if cp.returncode > 1: failed.append(project) if failed: self._handle_failed(args, failed) elif self.verbosity <= Verbosity.INF: self.inf(f"Empty diff in {no_diff} projects.") class Status(_ProjectCommand): def __init__(self): super().__init__( 'status', '"git status" for one or more projects', '''Runs "git status" for each of the specified projects. Unknown arguments are passed to "git status". Note: If you are looking to find untracked files and directories in the workspace use "west manifest --untracked".''', accepts_unknown_args=True, ) def do_add_parser(self, parser_adder): parser = self._parser(parser_adder, epilog=ACTIVE_CLONED_PROJECTS_HELP) parser.add_argument('projects', metavar='PROJECT', nargs='*', help='''projects (by name or path) to operate on; defaults to active cloned projects''') parser.add_argument('-a', '--all', action='store_true', help='include output for inactive projects') return parser def do_run(self, args, user_args): self.die_if_no_git() failed = [] for project in self._cloned_projects(args, only_active=not args.all): # 'git status' output for all projects is noisy when there # are lots of projects. # # We avoid this problem in 2 steps: # # 1. Check if we need to print any output for the # project. # # 2. If so, run 'git status' on the project. Otherwise, # skip output for the project entirely. # # In verbose mode, we always print output. try: if not (self.verbosity >= Verbosity.DBG or self._has_nonempty_status(project)): continue self.banner(f'status of {project.name_and_path}:') project.git('status', extra_args=user_args) except subprocess.CalledProcessError: failed.append(project) self._handle_failed(args, failed) class Update(_ProjectCommand): def __init__(self): super().__init__( 'update', 'update projects described in west manifest', textwrap.dedent('''\ Updates active projects defined in the manifest file as follows: 1. Clone the project if necessary 2. If necessary, fetch the project's revision from its remote (see "fetching behavior" below) 3. Reset the manifest-rev branch to the current manifest revision 4. Check out the new manifest-rev commit as a detached HEAD (the default), or keep/rebase existing checked out branches (see "checked out branch behavior") You must have already created a west workspace with "west init". This command does not alter the manifest repository's contents.''') ) def do_add_parser(self, parser_adder): parser = self._parser(parser_adder) parser.add_argument('--stats', action='store_true', help='''print performance statistics for update operations''') group = parser.add_argument_group( title='local project clone caches', description=textwrap.dedent('''\ Projects are usually initialized by fetching from their URLs, but they can also be cloned from caches on the local file system.''')) group.add_argument('--name-cache', help='''cached repositories are in subdirectories matching the names of projects to update''') group.add_argument('--path-cache', help='''cached repositories are in the same relative paths as the workspace being updated''') group = parser.add_argument_group( title='fetching behavior', description='By default, west update tries to avoid fetching.') group.add_argument('-f', '--fetch', dest='fetch_strategy', choices=['always', 'smart'], help='''how to fetch projects when updating: "always" fetches every project before update, while "smart" (default) skips fetching projects whose revisions are SHAs or tags available locally''') group.add_argument('-o', '--fetch-opt', action='append', default=[], help='''additional option to pass to 'git fetch' if fetching is necessary (e.g. 'o=--depth=1'); may be given more than once''') group.add_argument('-n', '--narrow', action='store_true', help='''fetch just the project revision if fetching is necessary; do not pass --tags to git fetch (may not work for SHA revisions depending on the Git host)''') group = parser.add_argument_group( title='checked out branch behavior', description=textwrap.dedent('''\ By default, locally checked out branches are left behind when manifest-rev commits are checked out.''')) group.add_argument('-k', '--keep-descendants', action='store_true', help='''if a checked out branch is a descendant of the new manifest-rev, leave it checked out instead (takes priority over --rebase)''') group.add_argument('-r', '--rebase', action='store_true', help='''rebase any checked out branch onto the new manifest-rev instead (leaving behind partial rebases on error)''') group = parser.add_argument_group( title='advanced options') group.add_argument('--group-filter', '--gf', action='append', default=[], metavar='FILTER', dest='group_filter', help='''proceed as if FILTER was appended to manifest.group-filter; may be given multiple times''') group.add_argument('--submodule-init-config', action='append', default=[], help='''git configuration option to set when running 'git submodule init' in '