diff --git a/src/west/manifest.py b/src/west/manifest.py index 9d714cc3..513e41b3 100644 --- a/src/west/manifest.py +++ b/src/west/manifest.py @@ -2496,7 +2496,7 @@ def _load_project(self, pd: dict, url_bases: dict[str, str], defaults: _defaults path=path, submodules=self._load_submodules(pd.get('submodules'), f'project {name}'), clone_depth=pd.get('clone-depth'), - west_commands=pd.get('west-commands'), + west_commands=Path(pd.get('west-commands')).as_posix() if pd.get('west-commands') else None, topdir=self.topdir, remote_name=remote, groups=groups, @@ -2628,7 +2628,7 @@ def _import_path_from_project(self, project: Project, path: str) -> None: return for data in imported: - self._import_data_from_project(project, data, None) + self._import_data_from_project(project, data, path) _logger.debug(f'done resolving import {path} for {project}') @@ -2647,16 +2647,22 @@ def _import_map_from_project(self, project: Project, imp: dict) -> None: _logger.debug(f'done resolving import {imap} for {project}') def _import_data_from_project( - self, project: Project, data: Any, imap: _import_map | None + self, project: Project, data: Any, imap_or_mfpath: _import_map | str ) -> None: # Destructively add the imported data into our 'projects' map. - - if imap is not None: - imap_filter = _compose_imap_filters(self._ctx.imap_filter, _imap_filter(imap)) - imap_path_prefix = imap.path_prefix - else: - imap_filter = self._ctx.imap_filter - imap_path_prefix = '.' + match imap_or_mfpath: + case _import_map(): + imap_filter = _compose_imap_filters( + self._ctx.imap_filter, _imap_filter(imap_or_mfpath) + ) + imap_path_prefix = imap_or_mfpath.path_prefix + mfst_path = imap_or_mfpath.file + case str(): + imap_filter = self._ctx.imap_filter + imap_path_prefix = '.' + mfst_path = imap_or_mfpath + case _: + raise AssertionError(f'imap_or_mfpath has unexpected type {type(imap_or_mfpath)}') child_ctx = self._ctx._replace( imap_filter=imap_filter, @@ -2674,13 +2680,21 @@ def _import_data_from_project( try: submanifest = Manifest(topdir=self.topdir, internal_import_ctx=child_ctx) except RecursionError as e: - raise _ManifestImportDepth(None, imap.file if imap else None) from e + raise _ManifestImportDepth(None, mfst_path) from e # Patch up any extension commands in the imported data # by allocating them to the project. - project.west_commands = _west_commands_merge( - project.west_commands, submanifest._ctx.manifest_west_commands - ) + + # If the manifest was imported from a project subdirectory + # (manifest_path is a relative path within the project), + # we need to adjust the west_commands paths to be relative + # to the project root, not to the manifest subdirectory. + mfst_dir = Path(mfst_path).parent if _is_yml(mfst_path) else Path(mfst_path) + west_commands_to_merge = [ + (mfst_dir / cmd).as_posix() for cmd in submanifest._ctx.manifest_west_commands + ] + + project.west_commands = _west_commands_merge(project.west_commands, west_commands_to_merge) def _import_content_from_project(self, project: Project, path: str) -> ImportedContentType: if not (self._ctx.import_flags & ImportFlag.FORCE_PROJECTS) and project.is_cloned(): diff --git a/tests/test_manifest.py b/tests/test_manifest.py index eceb72ad..7e7212f7 100644 --- a/tests/test_manifest.py +++ b/tests/test_manifest.py @@ -2095,6 +2095,67 @@ def test_import_project_submanifest_commands_both(manifest_repo): assert p1.west_commands == expected +def test_import_project_submanifest_commands_from_project_subdirectory(manifest_repo): + # When a manifest is imported from a project subdirectory (e.g., mf_subdir/west.yml), + # and that manifest defines west-commands, the paths should be + # resolved relative to the manifest subdirectory. + # This tests _import_path_from_project with a string path. + + with open(manifest_repo / 'west.yml', 'w') as f: + f.write('''\ + manifest: + projects: + - name: p1 + url: url-placeholder + import: mf_subdir/west.yml + ''') + + p1 = manifest_repo / '..' / 'p1' + create_repo(p1) + create_branch(p1, 'manifest-rev', checkout=True) + add_commit( + p1, + 'add mf_subdir/west.yml with west-commands', + files={ + 'mf_subdir/west.yml': '''\ + manifest: + projects: + - name: p2 + url: url-placeholder2 + self: + west-commands: p2subdir/west-commands.yml + ''', + }, + ) + checkout_branch(p1, 'master') + + # Case A: import as a string path to the submanifest file. + p1_proj = MF().get_projects(['p1'])[0] + # The west_commands path should be 'mf_subdir/p2subdir/west-commands.yml', + # not 'west-commands.yml', to be resolved correctly + # relative to the project root. See issue #725. + expected = ['mf_subdir/p2subdir/west-commands.yml'] + assert p1_proj.west_commands == expected + + # Case B: import using an import-map whose 'file' is a directory. + # Re-write the top-level manifest to use an import map instead of + # a string; the imported manifest still lives at mf_subdir/west.yml. + with open(manifest_repo / 'west.yml', 'w') as f: + f.write('''\ + manifest: + projects: + - name: p1 + url: url-placeholder + import: + file: mf_subdir + ''') + + # Reload and check the west_commands were resolved the same way. + p1_proj = MF().get_projects(['p1'])[0] + expected = ['mf_subdir/p2subdir/west-commands.yml'] + assert p1_proj.west_commands == expected + + def test_import_map_error_handling(): # Make sure we handle expected errors when loading import: # values that are maps.