# Copyright (c) 2024 Basalte bv # # SPDX-License-Identifier: Apache-2.0 import argparse import os import subprocess import sys import textwrap from itertools import chain from pathlib import Path from west.commands import WestCommand from zephyr_ext_common import ZEPHYR_BASE sys.path.append(os.fspath(Path(__file__).parent.parent)) import zephyr_module def in_venv() -> bool: return sys.prefix != sys.base_prefix class Packages(WestCommand): def __init__(self): super().__init__( "packages", "manage packages for Zephyr", "List and Install packages for Zephyr and modules", accepts_unknown_args=True, ) def do_add_parser(self, parser_adder): parser = parser_adder.add_parser( self.name, help=self.help, description=self.description, formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent( """ Listing packages: Run 'west packages ' to list all dependencies available from a given package manager, already installed and not. These can be filtered by module, see 'west packages --help' for details. """ ), ) parser.add_argument( "-m", "--module", action="append", default=[], dest="modules", metavar="", help="Zephyr module to run the 'packages' command for. " "Use 'zephyr' if the 'packages' command should run for Zephyr itself. " "Option can be passed multiple times. " "If this option is not given, the 'packages' command will run for Zephyr " "and all modules.", ) subparsers_gen = parser.add_subparsers( metavar="", dest="manager", help="select a manager.", required=True, ) pip_parser = subparsers_gen.add_parser( "pip", help="manage pip packages", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=textwrap.dedent( """ Manage pip packages: Run 'west packages pip' to print all requirement files needed by Zephyr and modules. The output is compatible with the requirements file format itself. """ ), ) pip_parser.add_argument( "--install", action="store_true", help="Install pip requirements instead of listing them. " "A single 'pip install' command is built and executed. " "Additional pip arguments can be passed after a -- separator " "from the original 'west packages pip --install' command. For example pass " "'--dry-run' to pip not to actually install anything, but print what would be.", ) pip_parser.add_argument( "--ignore-venv-check", action="store_true", help="Ignore the virtual environment check. " "This is useful when running 'west packages pip --install' " "in a CI environment where the virtual environment is not set up.", ) return parser def do_run(self, args, unknown): if len(unknown) > 0 and unknown[0] != "--": self.die( f'Unknown argument "{unknown[0]}"; ' 'arguments for the manager should be passed after "--"' ) # Store the zephyr modules for easier access self.zephyr_modules = zephyr_module.parse_modules(ZEPHYR_BASE, self.manifest) if args.modules: # Check for unknown module names module_names = [m.meta.get("name") for m in self.zephyr_modules] module_names.append("zephyr") for m in args.modules: if m not in module_names: self.die(f'Unknown zephyr module "{m}"') if args.manager == "pip": return self.do_run_pip(args, unknown[1:]) # Unreachable but print an error message if an implementation is missing. self.die(f'Unsupported package manager: "{args.manager}"') def do_run_pip(self, args, manager_args): requirements = [] if not args.modules or "zephyr" in args.modules: requirements.append(ZEPHYR_BASE / "scripts/requirements.txt") for module in self.zephyr_modules: module_name = module.meta.get("name") if args.modules and module_name not in args.modules: if args.install: self.dbg(f"Skipping module {module_name}") continue # Get the optional pip section from the package managers pip = module.meta.get("package-managers", {}).get("pip") if pip is None: if args.install: self.dbg(f"Nothing to install for {module_name}") continue # Add requirements files requirements += [Path(module.project) / r for r in pip.get("requirement-files", [])] if args.install: if not in_venv() and not args.ignore_venv_check: self.die("Running pip install outside of a virtual environment") if len(requirements) > 0: subprocess.check_call( [sys.executable, "-m", "pip", "install"] + list(chain.from_iterable([("-r", r) for r in requirements])) + manager_args ) else: self.inf("Nothing to install") return if len(manager_args) > 0: self.die(f'west packages pip does not support unknown arguments: "{manager_args}"') self.inf("\n".join([f"-r {r}" for r in requirements]))