Skip to content

Commit 99377aa

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

File tree

2 files changed

+222
-35
lines changed

2 files changed

+222
-35
lines changed

src/west/app/project.py

Lines changed: 89 additions & 35 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,49 @@ def __init__(self):
155156
'init',
156157
'create a west workspace',
157158
f'''\
158-
Creates a west workspace.
159+
Initialize a west workspace.
160+
161+
West supports two ways of creating a workspace:
159162
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.
163+
1. Remote Manifest Repositories
164+
----------------------------
165+
If `-m` is given, west clones the manifest repository from the specified URL
166+
into the workspace. Config option manifest.path will point at this cloned
167+
directory.
163168
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.
169+
- If `-m` is not provided, west defaults to the Zephyr manifest repository:
170+
{MANIFEST_URL_DEFAULT}.
171+
- With `--mr`, you can specify the revision (branch, tag, or sha) of the
172+
manifest repository.
173+
- If `--mr` is omitted, the repository's default branch (if available) is used.
167174
168-
With neither, -m {MANIFEST_URL_DEFAULT} is assumed.
175+
Note: The west workspace will be created in the current working directory, if
176+
neither --topdir nor [directory] are provided.
169177
170-
Warning: 'west init' renames and/or deletes temporary files inside the
178+
2. Local Manifest
179+
--------------
180+
If `--local` is given, west initializes a workspace from an already existing
181+
local manifest. The directory containing the manifest can be provided in
182+
positional argument [directory] (default: current working directory).
183+
Config option manifest.path will point at this directory.
184+
185+
Note: The west workspace will be created in the provided directory's parent, if
186+
no other directory is provided in --topdir.
187+
188+
Arguments
189+
---------
190+
--mf
191+
The relative path to the manifest file within the manifest repository
192+
(remote or local). Config option manifest.file will be set to this value.
193+
Defaults to `{_WEST_YML}` if not provided.
194+
195+
--topdir
196+
Specifies the directory where west should create the workspace.
197+
The `.west` folder will be created inside this directory.
198+
199+
Known Issues
200+
------------
201+
'west init' renames and/or deletes temporary files inside the
171202
workspace being created. This fails on some filesystems when some
172203
development tool or any other program is trying to read/index these
173204
temporary files at the same time. For instance, it is required to stop
@@ -192,9 +223,10 @@ def do_add_parser(self, parser_adder):
192223
parser = self._parser(
193224
parser_adder,
194225
usage='''
195-
196-
%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [directory]
197-
%(prog)s -l [--mf FILE] directory
226+
remote repository:
227+
%(prog)s [-m URL] [--mr REVISION] [--mf FILE] [-o=GIT_CLONE_OPTION] [-t WORKSPACE_DIR] [directory]
228+
local manifest:
229+
%(prog)s -l [-t WORKSPACE_DIR] [--mf FILE] directory
198230
''',
199231
)
200232

@@ -203,14 +235,16 @@ def do_add_parser(self, parser_adder):
203235
parser.add_argument(
204236
'-m',
205237
'--manifest-url',
206-
help='''manifest repository URL to clone;
238+
metavar='URL',
239+
help='''remote manifest repository URL to clone;
207240
cannot be combined with -l''',
208241
)
209242
parser.add_argument(
210243
'-o',
211244
'--clone-opt',
212245
action='append',
213246
default=[],
247+
metavar='GIT_CLONE_OPTION',
214248
help='''additional option to pass to 'git clone'
215249
(e.g. '-o=--depth=1'); may be given more than once;
216250
cannot be combined with -l''',
@@ -219,21 +253,33 @@ def do_add_parser(self, parser_adder):
219253
'--mr',
220254
'--manifest-rev',
221255
dest='manifest_rev',
256+
metavar='REVISION',
222257
help='''manifest repository branch or tag name
223258
to check out first; cannot be combined with -l''',
224259
)
225-
parser.add_argument(
226-
'--mf', '--manifest-file', dest='manifest_file', help='manifest file name to use'
227-
)
228260
parser.add_argument(
229261
'-l',
230262
'--local',
231263
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"''',
264+
help='''initialize from an already existing local
265+
manifest instead of cloning a remote manifest.''',
266+
)
267+
parser.add_argument(
268+
'--mf',
269+
'--manifest-file',
270+
dest='manifest_file',
271+
metavar='FILE',
272+
help=f'''manifest file to use. It is the relative
273+
path of the manifest file within the repository
274+
(remote or local). Defaults to {_WEST_YML}.''',
275+
)
276+
parser.add_argument(
277+
'-t',
278+
'--topdir',
279+
dest='topdir',
280+
metavar='WORKSPACE_DIR',
281+
help='''the directory of the west workspace, where
282+
.west will be created in.''',
237283
)
238284
parser.add_argument(
239285
'--rename-delay',
@@ -249,9 +295,10 @@ def do_add_parser(self, parser_adder):
249295
'directory',
250296
nargs='?',
251297
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)''',
298+
metavar='directory',
299+
help='''with --local: the path to the local manifest repository
300+
which contains a west.yml;
301+
otherwise: the directory to create the workspace in''',
255302
)
256303

257304
return parser
@@ -302,15 +349,19 @@ def local(self, args) -> Path:
302349
#
303350
# https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent
304351
manifest_dir = Path(args.directory or os.getcwd()).resolve()
305-
manifest_filename = args.manifest_file or 'west.yml'
352+
manifest_filename = args.manifest_file or _WEST_YML
306353
manifest_file = manifest_dir / manifest_filename
307-
topdir = manifest_dir.parent
308-
rel_manifest = manifest_dir.name
309-
west_dir = topdir / WEST_DIR
310-
311354
if not manifest_file.is_file():
312355
self.die(f'can\'t init: no {manifest_filename} found in {manifest_dir}')
313356

357+
topdir = Path(args.topdir or manifest_dir.parent).resolve()
358+
359+
if not manifest_file.is_relative_to(topdir):
360+
self.die(f'{manifest_file} must be relative to west topdir')
361+
362+
rel_manifest = manifest_dir.relative_to(topdir)
363+
west_dir = topdir / WEST_DIR
364+
314365
self.banner('Initializing from existing manifest repository', rel_manifest)
315366
self.small_banner(f'Creating {west_dir} and local configuration file')
316367
self.create(west_dir)
@@ -322,8 +373,11 @@ def local(self, args) -> Path:
322373
return topdir
323374

324375
def bootstrap(self, args) -> Path:
325-
topdir = Path(abspath(args.directory or os.getcwd()))
326-
self.banner('Initializing in', topdir)
376+
if args.topdir and args.directory:
377+
self.die('--topdir cannot be combined with positional argument [directory]')
378+
379+
topdir = Path(abspath(args.topdir or args.directory or os.getcwd()))
380+
self.banner(f'Initializing in {topdir}')
327381

328382
manifest_url = args.manifest_url or MANIFEST_URL_DEFAULT
329383
if args.manifest_rev:
@@ -378,7 +432,7 @@ def bootstrap(self, args) -> Path:
378432
raise
379433

380434
# Verify the manifest file exists.
381-
temp_manifest_filename = args.manifest_file or 'west.yml'
435+
temp_manifest_filename = args.manifest_file or _WEST_YML
382436
temp_manifest = tempdir / temp_manifest_filename
383437
if not temp_manifest.is_file():
384438
self.die(

tests/test_project.py

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

18641864

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

0 commit comments

Comments
 (0)