diff --git a/README.rst b/README.rst index d1cb8bb..62169f2 100644 --- a/README.rst +++ b/README.rst @@ -15,10 +15,10 @@ Basic Usage ----------- West lets you manage multiple Git repositories under a single directory using a -single file, called the *west manifest file*, or *manifest* for short. The -manifest file is named ``west.yml``. You use ``west init`` to set up this -directory, then ``west update`` to fetch and/or update the repositories named -in the manifest. +single file, called the *west manifest file*, or *manifest* for short. +By default the manifest file is named ``west.yml``. +You use ``west init`` to set up this directory, then ``west update`` to fetch +and/or update the repositories named in the manifest. By default, west uses `upstream Zephyr's manifest file `_, but west @@ -45,7 +45,8 @@ What just happened: creating working trees in the installation directory ``zephyrproject``. Use ``west init -m`` to specify another manifest repository. Use ``--mr`` to -use a revision other than ``master``. +use a revision other than ``master``. Use ``--mf`` to use a manifest file other +than ``west.yml``. Additional Commands ------------------- diff --git a/src/west/app/main.py b/src/west/app/main.py index 0174ac5..5b6423c 100755 --- a/src/west/app/main.py +++ b/src/west/app/main.py @@ -181,7 +181,7 @@ class WestApp: list(self.mle.args))) elif isinst(FileNotFoundError): # This should ordinarily only happen when the top - # level west.yml is not found. + # level manifest is not found. log.die(f"file not found: {self.mle.filename}") elif isinst(_ManifestImportDepth): log.die('failed, likely due to manifest import loop') diff --git a/src/west/app/project.py b/src/west/app/project.py index f758e4d..bdf4165 100644 --- a/src/west/app/project.py +++ b/src/west/app/project.py @@ -124,7 +124,7 @@ class Init(_ProjectCommand): 1. Creates a .west directory and clones a manifest repository from a git URL to a temporary subdirectory of .west, .west/. - 2. Parses the manifest file, .west//west.yml. + 2. Parses the manifest file, .west//. This file's contents can specify manifest.path, the location of the manifest repository in the workspace, like so: @@ -161,6 +161,8 @@ class Init(_ProjectCommand): parser.add_argument('--mr', '--manifest-rev', dest='manifest_rev', help='''manifest revision to check out and use; 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 an existing local manifest repository instead of cloning one; cannot be combined with @@ -212,13 +214,15 @@ class Init(_ProjectCommand): log.die('--mr cannot be used with -l') manifest_dir = Path(args.directory or os.getcwd()) - manifest_file = manifest_dir / 'west.yml' + 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(): - log.die(f'can\'t init: no "west.yml" found in {manifest_dir}') + log.die(f'can\'t init: no {manifest_filename} found in ' + f'{manifest_dir}') log.banner('Initializing from existing manifest repository', rel_manifest) @@ -226,6 +230,7 @@ class Init(_ProjectCommand): self.create(west_dir) os.chdir(topdir) update_config('manifest', 'path', os.fspath(rel_manifest)) + update_config('manifest', 'file', manifest_filename, topdir=topdir) return topdir @@ -257,9 +262,11 @@ class Init(_ProjectCommand): raise # Verify the manifest file exists. - temp_manifest = tempdir / 'west.yml' + temp_manifest_filename = args.manifest_file or 'west.yml' + temp_manifest = tempdir / temp_manifest_filename if not temp_manifest.is_file(): - log.die(f'can\'t init: no "west.yml" found in {tempdir}\n' + log.die(f'can\'t init: no {temp_manifest_filename} found in ' + f'{tempdir}\n' f' Hint: check --manifest-url={manifest_url} and ' f'--manifest-rev={manifest_rev}\n' f' You may need to remove {west_dir} before retrying.') @@ -290,6 +297,8 @@ class Init(_ProjectCommand): log.die(e) log.small_banner('setting manifest.path to', manifest_path) update_config('manifest', 'path', manifest_path, topdir=topdir) + update_config('manifest', 'file', temp_manifest_filename, + topdir=topdir) return topdir @@ -640,15 +649,15 @@ class Update(_ProjectCommand): def __init__(self): super().__init__( 'update', - 'update projects described in west.yml', + 'update projects described in west manifest', textwrap.dedent('''\ Updates each project repository to the revision specified in - the manifest file, west.yml, as follows: + the manifest file, as follows: 1. Fetch the project's remote to ensure the manifest revision is available locally 2. Reset the manifest-rev branch to the revision in - west.yml + the manifest 3. Check out the new manifest-rev commit as a detached HEAD (but see "checked out branches") diff --git a/src/west/manifest.py b/src/west/manifest.py index e3e759a..3bec390 100644 --- a/src/west/manifest.py +++ b/src/west/manifest.py @@ -17,7 +17,7 @@ import shlex import subprocess import sys from typing import Any, Callable, Dict, Iterable, List, NoReturn, \ - NamedTuple, Optional, TYPE_CHECKING, Union + NamedTuple, Optional, Tuple, TYPE_CHECKING, Union from packaging.version import parse as parse_version import pykwalify.core @@ -138,7 +138,7 @@ def _west_commands_merge(wc1: List[str], wc2: List[str]) -> List[str]: return wc1 or wc2 def _mpath(cp: Optional[configparser.ConfigParser] = None, - topdir: Optional[PathType] = None) -> str: + topdir: Optional[PathType] = None) -> Tuple[str, str]: # Return the value of the manifest.path configuration option # in *cp*, a ConfigParser. If not given, create a new one and # load configuration options with the given *topdir* as west @@ -152,7 +152,10 @@ def _mpath(cp: Optional[configparser.ConfigParser] = None, cfg.read_config(configfile=cfg.ConfigFile.LOCAL, config=cp, topdir=topdir) try: - return cp.get('manifest', 'path') + path = cp.get('manifest', 'path') + filename = cp.get('manifest', 'file', fallback=_WEST_YML) + + return (path, filename) except (configparser.NoOptionError, configparser.NoSectionError) as e: raise MalformedConfig('no "manifest.path" config option is set') from e @@ -308,10 +311,11 @@ def manifest_path() -> str: - `MalformedConfig` if the configuration file has no ``manifest.path`` key - - ``FileNotFoundError`` if no ``west.yml`` exists in - ``manifest.path`` + - ``FileNotFoundError`` if no manifest file exists as determined by + ``manifest.path`` and ``manifest.file`` ''' - ret = os.path.join(util.west_topdir(), _mpath(), _WEST_YML) + (mpath, mname) = _mpath() + ret = os.path.join(util.west_topdir(), mpath, mname) # It's kind of annoying to manually instantiate a FileNotFoundError. # This seems to be the best way. if not os.path.isfile(ret): @@ -897,7 +901,7 @@ class Manifest: - If neither *source_file* nor *topdir* is given, the file system is searched for *topdir*. That workspace's ``manifest.path`` configuration option is used to find - *source_file*, ``topdir//west.yml``. + *source_file*, ``topdir//``. - If only *source_file* is given, *topdir* is found starting there. The directory containing *source_file* @@ -932,10 +936,10 @@ class Manifest: # neither source_file nor topdir: search the filesystem # for the workspace and use its manifest.path. topdir = util.west_topdir() - mpath = _mpath(topdir=topdir) + (mpath, mname) = _mpath(topdir=topdir) kwargs.update({ 'topdir': topdir, - 'source_file': os.path.join(topdir, mpath, _WEST_YML), + 'source_file': os.path.join(topdir, mpath, mname), 'manifest_path': mpath }) else: @@ -960,8 +964,8 @@ class Manifest: # Read manifest.path from topdir/.west/config, and use it # to locate source_file. - mpath = _mpath(topdir=topdir) - source_file = os.path.join(topdir, mpath, _WEST_YML) + (mpath, mname) = _mpath(topdir=topdir) + source_file = os.path.join(topdir, mpath, mname) kwargs.update({ 'source_file': source_file, 'manifest_path': mpath, @@ -1747,7 +1751,7 @@ class Manifest: except FileNotFoundError: # We may need to fetch a new manifest-rev, e.g. if # revision is a branch that didn't used to have a - # west.yml, but now does. + # manifest, but now does. content = self._importer(project, path) except subprocess.CalledProcessError: # We may need a new manifest-rev, e.g. if revision is diff --git a/tests/test_project.py b/tests/test_project.py index 52ae7fc..1522daa 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -589,6 +589,70 @@ def test_init_local_missing_west_yml_failure(repos_tmpdir): cmd(f'init -l "{zephyr_install_dir}"') +def test_init_local_with_manifest_filename(repos_tmpdir): + # Test 'west init --mf -l' on a local repo + + manifest = repos_tmpdir / 'repos' / 'zephyr' + workspace = repos_tmpdir / 'workspace' + zephyr_install_dir = workspace / 'zephyr' + + # Do a local clone of manifest repo + clone(str(manifest), str(zephyr_install_dir)) + os.rename(str(zephyr_install_dir / 'west.yml'), + str(zephyr_install_dir / 'project.yml')) + + # fails because west.yml is missing + with pytest.raises(subprocess.CalledProcessError): + cmd(f'init -l "{zephyr_install_dir}"') + + # create a manifest with a syntax error so we can test if it's being parsed + with open(zephyr_install_dir / 'west.yml', 'w') as f: + f.write('[') + + cwd = os.getcwd() + cmd(f'init -l "{zephyr_install_dir}"') + + # init with a local manifest doesn't parse the file, so let's access it + workspace.chdir() + with pytest.raises(subprocess.CalledProcessError): + cmd('list') + + os.chdir(cwd) + shutil.move(workspace / '.west', workspace / '.west-syntaxerror') + + # success + cmd(f'init --mf project.yml -l "{zephyr_install_dir}"') + workspace.chdir() + config.read_config() + cmd('update') + + +def test_init_with_manifest_filename(repos_tmpdir): + # Test 'west init --mf' on a normal repo + + west_tmpdir = repos_tmpdir / 'workspace' + manifest = repos_tmpdir / 'repos' / 'zephyr' + + with open(manifest / 'west.yml', 'r') as f: + manifest_data = f.read() + + # also creates a west.yml with a syntax error to verify west doesn't even + # try to load the file + add_commit(str(manifest), 'rename manifest', + files={'west.yml': '[', 'project.yml': manifest_data}) + + # syntax error + with pytest.raises(subprocess.CalledProcessError): + cmd(f'init -m "{manifest}" "{west_tmpdir}"') + shutil.move(west_tmpdir, repos_tmpdir / 'workspace-syntaxerror') + + # success + cmd(f'init -m "{manifest}" --mf project.yml "{west_tmpdir}"') + west_tmpdir.chdir() + config.read_config() + cmd('update') + + def test_extension_command_execution(west_init_tmpdir): with pytest.raises(subprocess.CalledProcessError): cmd('test-extension')