Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 134 additions & 41 deletions src/west/app/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@
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 (
_WEST_YML,
ImportFlag,
Manifest,
ManifestImportFailed,
ManifestProject,
Submodule,
_manifest_content_at,
)
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 is_group as is_project_group
from west.util import expand_path

Expand Down Expand Up @@ -156,19 +157,58 @@ def __init__(self):
'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.
Initialize a west workspace (topdir) from a west manifest (`west.yml`) by
creating a `.west` directory in the topdir and a local configuration file
`.west/config`.
The West manifest can come from either a git repository (that will be cloned
during workspace initialization) or from an already existing local directory.

With neither, -m {MANIFEST_URL_DEFAULT} is assumed.

Warning: 'west init' renames and/or deletes temporary files inside the
Arguments
---------
--mf / --manifest-file
The relative path to the manifest file within the manifest repository
or directory. Config option `manifest.file` will be set to this value.
Defaults to '{_WEST_YML}' if not provided.

-t / --topdir
Specifies the directory where west should create the workspace.
The `.west` folder will be created inside this directory.
If it is an already existing directory, it must not contain a .west folder.


1. Using a Manifest Repository (default)
----------------------------------------
West clones the given repository (provided via `-m / --manifest-url`).
Note, that the repository must contain a west manifest.

If no `-m / --manifest-url` is provided, west uses Zephyr URL by default:
{MANIFEST_URL_DEFAULT}.

The topdir (where `.west` is created) is determined as follows:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It really sucks we have to keep such a complicated logic for backwards compatibility)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify the logic, we can just deprecate using a positional argument instead of the new --topdir. This will make the local and cloning case more consistent with each other.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose to add the --topdir feature here, and do this deprecation in a separate PR.

Copy link
Copy Markdown
Collaborator

@marc-hb marc-hb Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed for a separate PR, glad you're ok with the deprecation idea. Can you please add a comment in the code now? "# This case will be deprecated later". And in the corresponding test code if any.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found this, funny enough: https://interrupt.memfault.com/blog/practical_zephyr_west

Notice that we no longer specify the current directory in the call to west init using “.”. In fact, the directory - optionally passed as the last argument to west init - is interpreted differently by West when using the --local flag or the -m arguments:

With --local, the directory specifies the path to the local manifest repository.
Without the --local flag, the directory refers to the topdir and thus the folder in which to create the workspace (defaulting to the current working directory in this case).
Awkward, but this approach is probably used due to legacy reasons. If no --local flag is used and no repository is specified using -m, West defaults to using the Zephyr repository.

- argument `-t / --topdir` if provided
- otherwise: the positional argument `directory` if provided
- otherwise: the current working directory

If both `-t / --topdir` and `directory` are provided, `-t / --topdir`
specifies the workspace directory, while positional argument `directory`
specifies the subfolder within the workspace where the manifest repository is
cloned during initialization (defaults to <directory>/zephyr/.git).
With `--mr`, the revision (branch, tag, or sha) of the repository can be
specified that will be used. It defaults to the repository's default branch.

2. Using a Local Manifest
-------------------------
If `-l / --local` is given, west initializes a workspace from an already
existing `west.yml`. Its location can be specified in the `manifest_directory`
positional argument, which defaults to current working directory.

The topdir (where `.west` is created) is determined as follows:
- argument `-t / --topdir` if provided
- otherwise: `manifest_directory`'s parent

Known Issues
------------
'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
Expand All @@ -193,9 +233,11 @@ def do_add_parser(self, parser_adder):
parser = self._parser(
parser_adder,
usage='''
init with a repository:
%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [-t WORKSPACE_DIR] [directory]

%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [directory]
%(prog)s -l [--mf FILE] directory
init with an existing local manifest:
%(prog)s -l [-t WORKSPACE_DIR] [--mf FILE] [manifest_directory]
''',
)

Expand All @@ -204,6 +246,7 @@ def do_add_parser(self, parser_adder):
parser.add_argument(
'-m',
'--manifest-url',
metavar='URL',
help='''manifest repository URL to clone;
cannot be combined with -l''',
)
Expand All @@ -212,6 +255,7 @@ def do_add_parser(self, parser_adder):
'--clone-opt',
action='append',
default=[],
metavar='GIT_CLONE_OPTION',
help='''additional option to pass to 'git clone'
(e.g. '-o=--depth=1'); may be given more than once;
cannot be combined with -l''',
Expand All @@ -220,21 +264,36 @@ def do_add_parser(self, parser_adder):
'--mr',
'--manifest-rev',
dest='manifest_rev',
metavar='REVISION',
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"''',
help='''initialize workspace from an already existing local
manifest instead of cloning a manifest;
cannot be combined with -m''',
)
parser.add_argument(
'--mf',
'--manifest-file',
dest='manifest_file',
metavar='FILE',
help=f'''manifest file to use. It is the relative
path to the manifest file within the manifest_directory.
Defaults to {_WEST_YML}.''',
)
parser.add_argument(
'-t',
'--topdir',
dest='topdir',
metavar='WORKSPACE_DIR',
help='''the workspace directory where .west directory
will be created (WORKSPACE_DIR/.west);
the workspace directory may already exist
and WORKSPACE_DIR/.west must not exist''',
)
parser.add_argument(
'--rename-delay',
Expand All @@ -250,9 +309,15 @@ def do_add_parser(self, parser_adder):
'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)''',
metavar='directory',
help='''With --local: the path to a local directory which contains
a west manifest (west.yml);
Otherwise, if no --topdir is provided:
the location of the west workspace where .west directory
will be created (<directory>/.west)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
will be created (<directory>/.west)
will be created (<directory>/.west). Deprecated: use --topdir.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose to do the deprecation in a separate PR.

Otherwise:
the location where west will clone the manifest repository
''',
)

return parser
Expand Down Expand Up @@ -305,17 +370,27 @@ def local(self, args) -> Path:
# 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_dir = Path(args.directory or '.').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 {manifest_dir}')

self.banner('Initializing from existing manifest repository', rel_manifest)
topdir = Path(args.topdir or manifest_dir.parent).resolve()

if not manifest_file.is_relative_to(topdir):
self.die(f'{manifest_file} must be relative to west topdir {topdir}')

try:
already = util.west_topdir(manifest_dir, fall_back=False)
self.die_already(already)
except util.WestNotFound:
pass

rel_manifest = manifest_dir.relative_to(topdir)
west_dir = topdir / WEST_DIR

self.banner('Initializing with existing manifest', rel_manifest)
self.small_banner(f'Creating {west_dir} and local configuration file')
self.create(west_dir)
os.chdir(topdir)
Expand All @@ -326,8 +401,24 @@ def local(self, args) -> Path:
return topdir

def bootstrap(self, args) -> Path:
topdir = Path(abspath(args.directory or os.getcwd()))
self.banner('Initializing in', topdir)
manifest_subdir = Path()
if args.topdir:
topdir = Path(args.topdir).resolve()
if args.directory:
directory = Path(args.directory)
if not directory.is_absolute():
directory = directory.resolve()
if not directory.is_relative_to(topdir):
self.die(
f"directory '{args.directory}' must be inside west topdir '{args.topdir}'"
)
manifest_subdir = directory.relative_to(topdir)
elif args.directory:
topdir = Path(args.directory)
else:
topdir = Path()
topdir = topdir.resolve()
self.banner(f'Initializing in {topdir}')

manifest_url = args.manifest_url or MANIFEST_URL_DEFAULT
if args.manifest_rev:
Expand Down Expand Up @@ -382,7 +473,7 @@ def bootstrap(self, args) -> Path:
raise

# Verify the manifest file exists.
temp_manifest_filename = args.manifest_file or 'west.yml'
temp_manifest_filename = args.manifest_file or _WEST_YML
temp_manifest = tempdir / temp_manifest_filename
if not temp_manifest.is_file():
self.die(
Expand All @@ -399,15 +490,17 @@ def bootstrap(self, args) -> Path:
manifest = Manifest.from_data(
temp_manifest.read_text(encoding=Manifest.encoding), import_flags=ImportFlag.IGNORE
)

if manifest.yaml_path:
manifest_path = manifest.yaml_path
yaml_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
yaml_path = PurePath(urlparse(manifest_url).path).name

manifest_path = Path(manifest_subdir) / yaml_path
manifest_abspath = topdir / manifest_path

# Some filesystems like NTFS can't rename files in use.
Expand All @@ -434,7 +527,7 @@ def bootstrap(self, args) -> Path:
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.path', str(manifest_path))
self.config.set('manifest.file', temp_manifest_filename)

return topdir
Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,3 +650,15 @@ def check_proj_consistency(actual, expected):
assert actual.clone_depth == expected.clone_depth
assert actual.revision == expected.revision
assert actual.west_commands == expected.west_commands


def tree(path: Path, prefix="") -> str:
Copy link
Copy Markdown
Collaborator

@marc-hb marc-hb Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think https://anytree.readthedocs.io/en/latest/intro.html provides this for free and it is already a Zephyr requirement.

Don't get me wrong: I am NOT suggesting to add new west dependency but only to add a new west TEST dependency

lines = []
entries = sorted(path.iterdir(), key=lambda p: (p.is_file(), p.name))
for i, entry in enumerate(entries):
connector = "└── " if i == len(entries) - 1 else "├── "
lines.append(prefix + connector + entry.name)
if entry.is_dir():
indent = " " if i == len(entries) - 1 else "│ "
lines.extend(tree(entry, prefix + indent))
return "\n".join(lines)
Loading
Loading