Skip to content

Commit 67306db

Browse files
support 'west init --topdir'
Added new argument for west init to specify the west topdir when initializing a workspace from a remote or local manifest.
1 parent a3af732 commit 67306db

File tree

2 files changed

+256
-38
lines changed

2 files changed

+256
-38
lines changed

src/west/app/project.py

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,18 @@
2424
from west import util
2525
from west.commands import CommandError, Verbosity, WestCommand
2626
from west.configuration import Configuration
27-
from west.manifest import MANIFEST_REV_BRANCH as MANIFEST_REV
28-
from west.manifest import QUAL_MANIFEST_REV_BRANCH as QUAL_MANIFEST_REV
29-
from west.manifest import QUAL_REFS_WEST as QUAL_REFS
3027
from west.manifest import (
28+
_WEST_YML,
3129
ImportFlag,
3230
Manifest,
3331
ManifestImportFailed,
3432
ManifestProject,
3533
Submodule,
3634
_manifest_content_at,
3735
)
36+
from west.manifest import MANIFEST_REV_BRANCH as MANIFEST_REV
37+
from west.manifest import QUAL_MANIFEST_REV_BRANCH as QUAL_MANIFEST_REV
38+
from west.manifest import QUAL_REFS_WEST as QUAL_REFS
3839
from west.manifest import is_group as is_project_group
3940

4041
#
@@ -155,19 +156,61 @@ def __init__(self):
155156
'init',
156157
'create a west workspace',
157158
f'''\
158-
Creates a west workspace.
159-
160-
With -l, creates a workspace around an existing local repository;
161-
without -l, creates a workspace by cloning a manifest repository
162-
by URL.
159+
Initialize a west workspace, either from a remote manifest repository or from
160+
a local manifest.
163161
164-
With -m, clones the repository at that URL and uses it as the
165-
manifest repository. If --mr is not given, the remote's default
166-
branch will be used, if it exists.
167-
168-
With neither, -m {MANIFEST_URL_DEFAULT} is assumed.
169-
170-
Warning: 'west init' renames and/or deletes temporary files inside the
162+
Arguments
163+
---------
164+
--mf
165+
The relative path to the manifest file within the manifest repository
166+
(remote or local). Config option manifest.file will be set to this value.
167+
Defaults to `{_WEST_YML}` if not provided.
168+
169+
--topdir
170+
Specifies the directory where west should create the workspace.
171+
The `.west` folder will be created inside this directory.
172+
173+
174+
1. Remote Manifest Repository (default)
175+
---------------------------------------
176+
West clones a remote repository (provided via `-m / --manifest-url`) into a new
177+
workspace. The repositoy has to contain a west manifest.
178+
Inside the new workspace, the local config option `manifest.path` is set to
179+
point at the cloned directory (relative path to the workspace).
180+
181+
If no `-m / --manifest-url` is provided, west uses Zephyr URL by default:
182+
{MANIFEST_URL_DEFAULT}.
183+
184+
The west workspace (topdir) is determined as follows:
185+
- `-t / --topdir` if provided
186+
- `[directory]` if provided
187+
- the current working directory (if neither `--topdir` nor `[directory]` are
188+
provided)
189+
190+
Note:
191+
- If both `-t / --topdir` and `[directory]` are provided, `-t / --topdir`
192+
specifies the workspace directory, while `[directory]` specifies a subfolder
193+
where the manifest repository is cloned inside the workspace.
194+
- With `--mr`, you can specify the revision (branch, tag, or sha) of the
195+
remote repository that will be cloned. It defaults to the repository's
196+
default branch (if available).
197+
198+
2. Local Manifest
199+
-----------------
200+
If `-l / --local` is given, west initializes a new workspace from an already
201+
existing local manifest. The directory containing the manifest can be provided
202+
in positional argument `[manifest_directory]` (it defaults to current working
203+
directory if no value is provided).
204+
Inside the new workspace, the local config option `manifest.path` is set to
205+
point at the directory containing the manifest (relative path to the workspace)
206+
207+
Note: The west workspace is created in the `manifest_directory`'s parent
208+
if no other directory is provided in `--topdir`.
209+
210+
211+
Known Issues
212+
------------
213+
'west init' renames and/or deletes temporary files inside the
171214
workspace being created. This fails on some filesystems when some
172215
development tool or any other program is trying to read/index these
173216
temporary files at the same time. For instance, it is required to stop
@@ -192,9 +235,10 @@ def do_add_parser(self, parser_adder):
192235
parser = self._parser(
193236
parser_adder,
194237
usage='''
195-
196-
%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [directory]
197-
%(prog)s -l [--mf FILE] directory
238+
remote repository:
239+
%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [-t WORKSPACE_DIR] [directory]
240+
local manifest:
241+
%(prog)s -l [-t WORKSPACE_DIR] [--mf FILE] [manifest_directory]
198242
''',
199243
)
200244

@@ -203,14 +247,16 @@ def do_add_parser(self, parser_adder):
203247
parser.add_argument(
204248
'-m',
205249
'--manifest-url',
206-
help='''manifest repository URL to clone;
250+
metavar='URL',
251+
help='''remote manifest repository URL to clone;
207252
cannot be combined with -l''',
208253
)
209254
parser.add_argument(
210255
'-o',
211256
'--clone-opt',
212257
action='append',
213258
default=[],
259+
metavar='GIT_CLONE_OPTION',
214260
help='''additional option to pass to 'git clone'
215261
(e.g. '-o=--depth=1'); may be given more than once;
216262
cannot be combined with -l''',
@@ -219,21 +265,33 @@ def do_add_parser(self, parser_adder):
219265
'--mr',
220266
'--manifest-rev',
221267
dest='manifest_rev',
268+
metavar='REVISION',
222269
help='''manifest repository branch or tag name
223270
to check out first; cannot be combined with -l''',
224271
)
225-
parser.add_argument(
226-
'--mf', '--manifest-file', dest='manifest_file', help='manifest file name to use'
227-
)
228272
parser.add_argument(
229273
'-l',
230274
'--local',
231275
action='store_true',
232-
help='''use "directory" as an existing local
233-
manifest repository instead of cloning one from
234-
MANIFEST_URL; .west is created next to "directory"
235-
in this case, and manifest.path points at
236-
"directory"''',
276+
help='''initialize from an already existing local
277+
manifest instead of cloning a remote manifest.''',
278+
)
279+
parser.add_argument(
280+
'--mf',
281+
'--manifest-file',
282+
dest='manifest_file',
283+
metavar='FILE',
284+
help=f'''manifest file to use. It is the relative
285+
path of the manifest file within the repository
286+
(remote or local). Defaults to {_WEST_YML}.''',
287+
)
288+
parser.add_argument(
289+
'-t',
290+
'--topdir',
291+
dest='topdir',
292+
metavar='WORKSPACE_DIR',
293+
help='''the directory of the west workspace, where
294+
.west will be created in.''',
237295
)
238296
parser.add_argument(
239297
'--rename-delay',
@@ -249,9 +307,10 @@ def do_add_parser(self, parser_adder):
249307
'directory',
250308
nargs='?',
251309
default=None,
252-
help='''with -l, the path to the local manifest repository;
253-
without it, the directory to create the workspace in (defaulting
254-
to the current working directory in this case)''',
310+
metavar='directory',
311+
help='''with --local: the path to the local manifest repository
312+
which contains a west.yml;
313+
otherwise: the directory to create the workspace in''',
255314
)
256315

257316
return parser
@@ -302,15 +361,19 @@ def local(self, args) -> Path:
302361
#
303362
# https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent
304363
manifest_dir = Path(args.directory or os.getcwd()).resolve()
305-
manifest_filename = args.manifest_file or 'west.yml'
364+
manifest_filename = args.manifest_file or _WEST_YML
306365
manifest_file = manifest_dir / manifest_filename
307-
topdir = manifest_dir.parent
308-
rel_manifest = manifest_dir.name
309-
west_dir = topdir / WEST_DIR
310-
311366
if not manifest_file.is_file():
312367
self.die(f'can\'t init: no {manifest_filename} found in {manifest_dir}')
313368

369+
topdir = Path(args.topdir or manifest_dir.parent).resolve()
370+
371+
if not manifest_file.is_relative_to(topdir):
372+
self.die(f'{manifest_file} must be relative to west topdir')
373+
374+
rel_manifest = manifest_dir.relative_to(topdir)
375+
west_dir = topdir / WEST_DIR
376+
314377
self.banner('Initializing from existing manifest repository', rel_manifest)
315378
self.small_banner(f'Creating {west_dir} and local configuration file')
316379
self.create(west_dir)
@@ -322,8 +385,21 @@ def local(self, args) -> Path:
322385
return topdir
323386

324387
def bootstrap(self, args) -> Path:
325-
topdir = Path(abspath(args.directory or os.getcwd()))
326-
self.banner('Initializing in', topdir)
388+
subdir = '.'
389+
if args.topdir:
390+
topdir = Path(abspath(args.topdir))
391+
if args.directory:
392+
if not Path(abspath(args.directory)).is_relative_to(topdir):
393+
self.die(
394+
f"directory '{args.directory}' must be relative "
395+
f"to west topdir '{args.topdir}'"
396+
)
397+
subdir = os.path.relpath(args.directory, topdir)
398+
elif args.directory:
399+
topdir = Path(abspath(args.directory))
400+
else:
401+
topdir = Path(abspath(os.getcwd()))
402+
self.banner(f'Initializing in {topdir}')
327403

328404
manifest_url = args.manifest_url or MANIFEST_URL_DEFAULT
329405
if args.manifest_rev:
@@ -378,7 +454,7 @@ def bootstrap(self, args) -> Path:
378454
raise
379455

380456
# Verify the manifest file exists.
381-
temp_manifest_filename = args.manifest_file or 'west.yml'
457+
temp_manifest_filename = args.manifest_file or _WEST_YML
382458
temp_manifest = tempdir / temp_manifest_filename
383459
if not temp_manifest.is_file():
384460
self.die(
@@ -404,6 +480,7 @@ def bootstrap(self, args) -> Path:
404480
# is PurePosixPath.
405481
manifest_path = PurePath(urlparse(manifest_url).path).name
406482

483+
manifest_path = str(Path(subdir) / manifest_path)
407484
manifest_abspath = topdir / manifest_path
408485

409486
# Some filesystems like NTFS can't rename files in use.

tests/test_project.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,6 +1862,147 @@ def test_init_local_with_manifest_filename(repos_tmpdir):
18621862
cmd('update')
18631863

18641864

1865+
TEST_CASES_INIT = [
1866+
# (local_dir, topdir, directory, manifest_file, expected_error)
1867+
######################################
1868+
# REMOTE MANIFEST
1869+
######################################
1870+
# init from remote repository (without any parameters)
1871+
(None, None, None, None, None),
1872+
# specify topdir in current directory
1873+
(None, Path('.'), None, None, None),
1874+
# specify topdir in a subfolder
1875+
(None, Path('subdir'), None, None, None),
1876+
# use deprecated [directory] to specify topdir
1877+
(None, None, Path('subdir'), None, None),
1878+
# specify topdir in a sibling
1879+
(None, Path('..') / 'sibling', None, None, None),
1880+
# specify topdir and [directory]
1881+
(None, Path('subdir'), Path('subdir') / 'extra' / 'level', None, None),
1882+
# error cases
1883+
# specify a non-existent manifest file
1884+
(None, Path('.'), None, Path('non-existent.yml'), subprocess.CalledProcessError),
1885+
# specify topdir and [directory] (but directory is not inside topdir)
1886+
(None, Path('subdir'), Path('sibling'), None, subprocess.CalledProcessError),
1887+
######################################
1888+
# LOCAL MANIFEST
1889+
######################################
1890+
# init workspace in current working directory (without --topdir)
1891+
(Path('workspace') / 'zephyr', None, Path('workspace') / 'zephyr', None, None),
1892+
# init workspace in current working directory
1893+
(Path('workspace') / 'zephyr', Path('.'), Path('workspace') / 'zephyr', None, None),
1894+
# init workspace in a subfolder of current working directory
1895+
(Path('workspace') / 'zephyr', Path('workspace'), Path('workspace') / 'zephyr', None, None),
1896+
# init workspace in current working directory by providing a manifest file
1897+
(Path('workspace') / 'zephyr', Path('.'), Path('workspace'), Path('zephyr') / 'west.yml', None),
1898+
# init workspace in itself by providing manifest file
1899+
(
1900+
Path('workspace') / 'zephyr',
1901+
Path('.'),
1902+
Path('.'),
1903+
Path('workspace') / 'zephyr' / 'west.yml',
1904+
None,
1905+
),
1906+
# init workspace in a subfolder by providing manifest file
1907+
(
1908+
Path('workspace') / 'subdir' / 'zephyr',
1909+
Path('workspace'),
1910+
Path('workspace') / 'subdir',
1911+
Path('zephyr') / 'west.yml',
1912+
None,
1913+
),
1914+
# init workspace in a subfolder by providing manifest file
1915+
(
1916+
Path('workspace') / 'subdir' / 'zephyr',
1917+
Path('workspace'),
1918+
Path('workspace'),
1919+
Path('subdir') / 'zephyr' / 'west.yml',
1920+
None,
1921+
),
1922+
# error cases
1923+
# init workspace without a directory
1924+
(Path('workspace') / 'zephyr', Path('.'), None, None, subprocess.CalledProcessError),
1925+
# init workspace in a sibling repository path
1926+
(
1927+
Path('workspace') / 'zephyr',
1928+
Path('sibling'),
1929+
Path('workspace') / 'zephyr',
1930+
None,
1931+
subprocess.CalledProcessError,
1932+
),
1933+
# init workspace from non-existent manifest
1934+
(
1935+
Path('workspace') / 'zephyr',
1936+
Path('.'),
1937+
Path('non-existent.yml'),
1938+
None,
1939+
subprocess.CalledProcessError,
1940+
),
1941+
# init workspace from a manifest not inside the workspace
1942+
(Path('..') / 'zephyr', Path('.'), Path('..') / 'zephyr', None, subprocess.CalledProcessError),
1943+
]
1944+
1945+
1946+
@pytest.mark.parametrize("test_case", TEST_CASES_INIT)
1947+
def test_init(repos_tmpdir, test_case):
1948+
repos_tmpdir.chdir()
1949+
flags = []
1950+
1951+
local_dir, topdir, directory, manifest_file, expected_error = test_case
1952+
1953+
# prepare local manifest in local_dir
1954+
if local_dir:
1955+
# place the local manifest to given path
1956+
clone(str(repos_tmpdir / 'repos' / 'zephyr'), str(local_dir))
1957+
flags += ['-l']
1958+
else:
1959+
# clone from remote manifest
1960+
flags += ["-m", repos_tmpdir / 'repos' / 'zephyr']
1961+
1962+
# extend west init flags according to specified test case
1963+
if topdir:
1964+
flags += ['-t', topdir]
1965+
if manifest_file:
1966+
flags += ['--mf', manifest_file]
1967+
if directory:
1968+
flags += [directory]
1969+
1970+
# initialize west workspace
1971+
if not expected_error:
1972+
cmd(['init'] + flags)
1973+
else:
1974+
cmd_raises(['init'] + flags, expected_error)
1975+
return
1976+
1977+
# go to west workspace and check for correct config
1978+
if local_dir:
1979+
# topdir is either specified or default (directory.parent)
1980+
workspace = topdir or directory.parent
1981+
else:
1982+
# topdir is either specified, directory or default (cwd)
1983+
workspace = topdir or directory or Path.cwd()
1984+
1985+
os.chdir(workspace)
1986+
actual = cmd('config manifest.path')
1987+
if local_dir:
1988+
assert Path(actual.rstrip()) == directory.relative_to(workspace)
1989+
else:
1990+
if topdir and directory:
1991+
# zephyr is cloned into specified subdirectory in workspace
1992+
expected = os.path.relpath(directory, topdir)
1993+
assert Path(actual.rstrip()) == Path(expected) / 'zephyr'
1994+
else:
1995+
# zephyr is cloned directly to workspace
1996+
assert Path(actual.rstrip()) == Path('zephyr')
1997+
1998+
manifest_file = manifest_file or Path('west.yml')
1999+
actual = cmd('config manifest.file')
2000+
assert Path(actual.rstrip()) == Path(manifest_file)
2001+
2002+
# update must run successful
2003+
cmd('update')
2004+
2005+
18652006
def test_init_local_with_empty_path(repos_tmpdir):
18662007
# Test "west init -l ." + "west update".
18672008
# Regression test for:

0 commit comments

Comments
 (0)