scripts: west_commands: Add packages command

Add a new west command as helper for package management of Zephyr
and modules.

The first package manager to get supported is pip, where west projects
can list individual packages or requirements files in their module file.

A convenience --install argument is added to install the packages instead.

Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
This commit is contained in:
Pieter De Gendt 2024-11-02 16:46:56 +01:00 committed by Benjamin Cabé
commit 2ad915284b
3 changed files with 182 additions and 0 deletions

View file

@ -84,3 +84,8 @@ west-commands:
- name: sdk
class: Sdk
help: manage Zephyr SDK
- file: scripts/west_commands/packages.py
commands:
- name: packages
class: Packages
help: manage packages for Zephyr

View file

@ -0,0 +1,164 @@
# 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 <manager>' to list all dependencies
available from a given package manager, already
installed and not. These can be filtered by module,
see 'west packages <manager> --help' for details.
"""
),
)
parser.add_argument(
"-m",
"--module",
action="append",
default=[],
dest="modules",
metavar="<module>",
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="<manager>",
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.",
)
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():
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]))

View file

@ -161,6 +161,19 @@ mapping:
type: seq
sequence:
- type: str
package-managers:
required: false
type: map
mapping:
pip:
required: false
type: map
mapping:
requirement-files:
required: false
type: seq
sequence:
- type: str
'''
MODULE_YML_PATH = PurePath('zephyr/module.yml')