diff --git a/src/west/app/project.py b/src/west/app/project.py index db98f9e9..5a94b420 100644 --- a/src/west/app/project.py +++ b/src/west/app/project.py @@ -638,6 +638,8 @@ def do_add_parser(self, parser_adder): group = parser.add_argument_group('options for --resolve and --freeze') group.add_argument('-o', '--out', help='output file, default is standard output') + group.add_argument('--active-only', action='store_true', + help='only resolve active projects') return parser @@ -649,11 +651,13 @@ def do_run(self, args, user_args): if args.validate: pass # nothing more to do elif args.resolve: - self._die_if_manifest_project_filter('resolve') - self._dump(args, manifest.as_yaml(**dump_kwargs)) + if not args.active_only: + self._die_if_manifest_project_filter('resolve') + self._dump(args, manifest.as_yaml(active_only=args.active_only, **dump_kwargs)) elif args.freeze: - self._die_if_manifest_project_filter('freeze') - self._dump(args, manifest.as_frozen_yaml(**dump_kwargs)) + if not args.active_only: + self._die_if_manifest_project_filter('freeze') + self._dump(args, manifest.as_frozen_yaml(active_only=args.active_only, **dump_kwargs)) elif args.untracked: self._untracked() elif args.path: @@ -666,7 +670,9 @@ def _die_if_manifest_project_filter(self, action): if self.config.get('manifest.project-filter') is not None: self.die(f'"west manifest --{action}" is not (yet) supported ' 'when the manifest.project-filter option is set. ' - 'Please clear the project-filter configuration ' + f'Add --active-only to {action} only the projects ' + 'currently active in the workspace. Alternatively, ' + 'please clear the project-filter configuration ' 'option and re-run this command, or contact the ' 'west developers if you have a use case for resolving ' 'the manifest while projects are made inactive by the ' diff --git a/src/west/manifest.py b/src/west/manifest.py index ea1bbb26..36a9b3d0 100644 --- a/src/west/manifest.py +++ b/src/west/manifest.py @@ -1599,16 +1599,20 @@ def get_projects(self, return ret def _as_dict_helper( - self, pdict: Optional[Callable[[Project], dict]] = None) \ - -> dict: + self, + pdict: Optional[Callable[[Project], dict]] = None, + pfilter: Optional[Callable[[Project], bool]] = None, + ) -> dict: # pdict: returns a Project's dict representation. # By default, it's Project.as_dict. + # pfilter: filter to apply on listing the projects. + # By default, no filter is applied. if pdict is None: pdict = Project.as_dict projects = list(self.projects) del projects[MANIFEST_PROJECT_INDEX] - project_dicts = [pdict(p) for p in projects] + project_dicts = [pdict(p) for p in projects if pfilter is None or pfilter(p)] # This relies on insertion-ordered dictionaries for # predictability, which is a CPython 3.6 implementation detail @@ -1622,22 +1626,26 @@ def _as_dict_helper( return r - def as_dict(self) -> dict: + def as_dict(self, active_only: bool = False) -> dict: '''Returns a dict representing self, fully resolved. The value is "resolved" in that the result is as if all projects had been defined in a single manifest without any import attributes. + + :param active_only: Do not resolve inactive projects ''' - return self._as_dict_helper() + return self._as_dict_helper(pfilter=self.is_active if active_only else None) - def as_frozen_dict(self) -> dict: + def as_frozen_dict(self, active_only: bool = False) -> dict: '''Returns a dict representing self, but frozen. The value is "frozen" in that all project revisions are the full SHAs pointed to by `QUAL_MANIFEST_REV_BRANCH` references. Raises ``RuntimeError`` if a project SHA can't be resolved. + + :param active_only: Do not freeze inactive projects ''' def pdict(p): if not p.is_cloned(): @@ -1653,7 +1661,7 @@ def pdict(p): d['revision'] = sha return d - return self._as_dict_helper(pdict=pdict) + return self._as_dict_helper(pdict=pdict, pfilter=self.is_active if active_only else None) def _dump_yaml(self, to_dump: dict, **kwargs) -> str: ''' Dumps dictionary to YAML using the multi-line string representer. @@ -1672,18 +1680,19 @@ def mls_representer(dumper, data): yaml.add_representer(_MLS, mls_representer, Dumper=yaml.SafeDumper) return yaml.safe_dump(to_dump, **kwargs) - def as_yaml(self, **kwargs) -> str: + def as_yaml(self, active_only: bool = False, **kwargs) -> str: '''Returns a YAML representation for self, fully resolved. The value is "resolved" in that the result is as if all projects had been defined in a single manifest without any import attributes. + :param active_only: Do not resolve inactive projects :param kwargs: passed to yaml.safe_dump() ''' - return self._dump_yaml(self.as_dict(), **kwargs) + return self._dump_yaml(self.as_dict(active_only=active_only), **kwargs) - def as_frozen_yaml(self, **kwargs) -> str: + def as_frozen_yaml(self, active_only: bool = False, **kwargs) -> str: '''Returns a YAML representation for self, but frozen. The value is "frozen" in that all project revisions are the @@ -1691,9 +1700,10 @@ def as_frozen_yaml(self, **kwargs) -> str: Raises ``RuntimeError`` if a project SHA can't be resolved. + :param active_only: Do not freeze inactive projects :param kwargs: passed to yaml.safe_dump() ''' - return self._dump_yaml(self.as_frozen_dict(), **kwargs) + return self._dump_yaml(self.as_frozen_dict(active_only=active_only), **kwargs) @property def projects(self) -> list[Project]: diff --git a/tests/test_project.py b/tests/test_project.py index 7447574b..c336deee 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -407,6 +407,77 @@ def test_manifest_freeze(west_update_tmpdir): '^ path: zephyr$'] _match_multiline_regex(expected_res, actual) +def test_manifest_freeze_active(west_update_tmpdir): + # We should be able to freeze manifests with inactive projects. + cmd('config manifest.group-filter -- -Kconfiglib-group') + + actual = cmd('manifest --freeze --active-only').splitlines() + # Same as test_manifest_freeze but without inactive projects + expected_res = ['^manifest:$', + '^ projects:$', + '^ - name: tagged_repo$', + '^ url: .*$', + '^ revision: [a-f0-9]{40}$', + '^ - name: net-tools$', + '^ description: Networking tools.$', + '^ url: .*$', + '^ revision: [a-f0-9]{40}$', + '^ clone-depth: 1$', + '^ west-commands: scripts/west-commands.yml$', + '^ self:$', + '^ path: zephyr$'] + _match_multiline_regex(expected_res, actual) + +def test_manifest_resolve(west_update_tmpdir): + # We should be able to resolve manifests. + actual = cmd('manifest --resolve').splitlines() + # Similar as test_manifest_freeze but with resolved projects + expected_res = ['^manifest:$', + '^ projects:$', + '^ - name: Kconfiglib$', + '^ description: |', + '^ Kconfiglib is an implementation of$', + '^ the Kconfig language written in Python.$', + '^ url: .*$', + '^ revision: zephyr$', + '^ path: subdir/Kconfiglib$', + '^ groups:$', + '^ - Kconfiglib-group$', + '^ submodules: true$', + '^ - name: tagged_repo$', + '^ url: .*$', + '^ revision: v1.0$', + '^ - name: net-tools$', + '^ description: Networking tools.$', + '^ url: .*$', + '^ revision: master$', + '^ clone-depth: 1$', + '^ west-commands: scripts/west-commands.yml$', + '^ self:$', + '^ path: zephyr$'] + _match_multiline_regex(expected_res, actual) + +def test_manifest_resolve_active(west_update_tmpdir): + # We should be able to resolve manifests with inactive projects. + cmd('config manifest.group-filter -- -Kconfiglib-group') + + actual = cmd('manifest --resolve --active-only').splitlines() + # Same as test_manifest_resolve but without inactive projects + expected_res = ['^manifest:$', + '^ projects:$', + '^ - name: tagged_repo$', + '^ url: .*$', + '^ revision: v1.0$', + '^ - name: net-tools$', + '^ description: Networking tools.$', + '^ url: .*$', + '^ revision: master$', + '^ clone-depth: 1$', + '^ west-commands: scripts/west-commands.yml$', + '^ self:$', + '^ path: zephyr$'] + _match_multiline_regex(expected_res, actual) + def test_compare(config_tmpdir, west_init_tmpdir): # 'west compare' with no projects cloned should still work, # and not print anything.