diff --git a/docs/en/api-guides/tools/idf-tools.rst b/docs/en/api-guides/tools/idf-tools.rst index 769b6f391e..b10a39d913 100644 --- a/docs/en/api-guides/tools/idf-tools.rst +++ b/docs/en/api-guides/tools/idf-tools.rst @@ -109,7 +109,7 @@ Any mirror server can be used provided the URL matches the ``github.com`` downlo * ``check``: For each tool, checks whether the tool is available in the system path and in ``IDF_TOOLS_PATH``. -* ``install-python-env``: Create a Python virtual environment in the ``${IDF_TOOLS_PATH}/python_env`` directory and install there the required Python packages. An optional ``--features`` argument allows one to specify a comma-separated list of features. For each feature a requirements file must exist. For example, feature ``XY`` is a valid feature if ``${IDF_PATH}/tools/requirements/requirements.XY.txt`` is an existing file with a list of Python packages to be installed. There is one mandatory ``core`` feature ensuring core functionality of ESP-IDF (build, flash, monitor, debug in console). There can be an arbitrary number of optional features. The selected list of features is stored in ``idf-env.json``. The requirement files contain a list of the desired Python packages to be installed and ``espidf.constraints.*.txt`` downloaded from https://dl.espressif.com and stored in ``${IDF_TOOLS_PATH}`` the package version requirements for a given ESP-IDF version. +* ``install-python-env``: Create a Python virtual environment in the ``${IDF_TOOLS_PATH}/python_env`` directory and install there the required Python packages. An optional ``--features`` argument allows one to specify a comma-separated list of features to be added or removed. Feature that begins with ``-`` will be removed and features with ``+`` or without any sign will be added. Example syntax for removing feature ``XY`` is ``--features=-XY`` and for adding ``--features=+XY`` or ``--features=XY``. If both removing and adding options are provided with the same feature, no operation is performed. For each feature a requirements file must exist. For example, feature ``XY`` is a valid feature if ``${IDF_PATH}/tools/requirements/requirements.XY.txt`` is an existing file with a list of Python packages to be installed. There is one mandatory ``core`` feature ensuring core functionality of ESP-IDF (build, flash, monitor, debug in console). There can be an arbitrary number of optional features. The selected list of features is stored in ``idf-env.json``. The requirement files contain a list of the desired Python packages to be installed and ``espidf.constraints.*.txt`` downloaded from https://dl.espressif.com and stored in ``${IDF_TOOLS_PATH}`` the package version requirements for a given ESP-IDF version. * ``check-python-dependencies``: Checks if all required Python packages are installed. Packages from ``${IDF_PATH}/tools/requirements/requirements.*.txt`` files selected by the feature list of ``idf-env.json`` are checked with the package versions specified in the ``espidf.constraints.*.txt`` file. The constraint file will be downloaded from https://dl.espressif.com if this step hasn't been done already in the last day. diff --git a/tools/idf_tools.py b/tools/idf_tools.py index 1fd16e25fa..8ef44f2217 100755 --- a/tools/idf_tools.py +++ b/tools/idf_tools.py @@ -1022,9 +1022,20 @@ class IDFRecord: def features(self) -> List[str]: return self._features - def extend_features(self, features: List[str]) -> None: - # Features can be only updated, but always maintain existing features. - self._features = list(set(features + self._features)) + def update_features(self, add: Tuple[str, ...] = (), remove: Tuple[str, ...] = ()) -> None: + # Update features, but maintain required feature 'core' + # If the same feature is present in both argument's tuples, do not update this feature + add_set = set(add) + remove_set = set(remove) + # Remove duplicates + features_to_add = add_set.difference(remove_set) + features_to_remove = remove_set.difference(add_set) + + features = set(self._features) + features.update(features_to_add) + features.difference_update(features_to_remove) + features.add('core') + self._features = list(features) @property def targets(self) -> List[str]: @@ -1051,7 +1062,7 @@ class IDFRecord: # When some of these key attributes, which are irreplaceable with default values, are not found, raise VallueError raise ValueError('Inconsistent record') - idf_record_obj.extend_features(record_dict.get('features', [])) + idf_record_obj.update_features(record_dict.get('features', [])) idf_record_obj.extend_targets(record_dict.get('targets', [])) unset = record_dict.get('unset') @@ -1328,13 +1339,18 @@ def feature_to_requirements_path(feature): # type: (str) -> str return os.path.join(global_idf_path or '', 'tools', 'requirements', 'requirements.{}.txt'.format(feature)) -def add_and_check_features(idf_env_obj, features_str): # type: (IDFEnv, str) -> list[str] +def process_and_check_features(idf_env_obj, features_str): # type: (IDFEnv, str) -> list[str] new_features = [] + remove_features = [] for new_feature_candidate in features_str.split(','): - if os.path.isfile(feature_to_requirements_path(new_feature_candidate)): - new_features += [new_feature_candidate] - - idf_env_obj.get_active_idf_record().extend_features(new_features) + if new_feature_candidate.startswith('-'): + remove_features += [new_feature_candidate.lstrip('-')] + else: + new_feature_candidate = new_feature_candidate.lstrip('+') + # Feature to be added needs to be checked if is valid + if os.path.isfile(feature_to_requirements_path(new_feature_candidate)): + new_features += [new_feature_candidate] + idf_env_obj.get_active_idf_record().update_features(tuple(new_features), tuple(remove_features)) return idf_env_obj.get_active_idf_record().features @@ -1847,7 +1863,7 @@ def get_wheels_dir(): # type: () -> Optional[str] def get_requirements(new_features): # type: (str) -> list[str] idf_env_obj = IDFEnv.get_idf_env() - features = add_and_check_features(idf_env_obj, new_features) + features = process_and_check_features(idf_env_obj, new_features) idf_env_obj.save() return [feature_to_requirements_path(feature) for feature in features] diff --git a/tools/install_util.py b/tools/install_util.py index c2e01c56ec..a9820016b1 100644 --- a/tools/install_util.py +++ b/tools/install_util.py @@ -13,13 +13,20 @@ from itertools import chain def action_extract_features(args: str) -> None: """ - Command line arguments starting with "--enable-" are features. This function selects those and prints them. + Command line arguments starting with "--enable-" or "--disable" are features. This function selects those, add them signs '+' or '-' and prints them. """ - features = ['core'] # "core" features should be always installed + features = ['+core'] # "core" features should be always installed if args: - arg_prefix = '--enable-' - features += [arg[len(arg_prefix):] for arg in args.split() if arg.startswith(arg_prefix)] + arg_enable_prefix = '--enable-' + arg_disable_prefix = '--disable-' + # features to be enabled has prefix '+', disabled has prefix '-' + for arg in args.split(): + if arg.startswith(arg_enable_prefix): + features.append('+' + arg[len(arg_enable_prefix):]) + elif arg.startswith(arg_disable_prefix): + features.append('-' + arg[len(arg_disable_prefix):]) + features = list(set(features)) print(','.join(features)) diff --git a/tools/test_idf_tools/test_idf_tools_python_env.py b/tools/test_idf_tools/test_idf_tools_python_env.py index 7ff528db0c..985ae140a0 100644 --- a/tools/test_idf_tools/test_idf_tools_python_env.py +++ b/tools/test_idf_tools/test_idf_tools_python_env.py @@ -61,6 +61,13 @@ class TestPythonInstall(unittest.TestCase): self.assertIn(REQ_CORE, output) self.assertIn(REQ_GDBGUI, output) + # Argument that begins with '-' can't stand alone to be parsed as value + output = self.run_idf_tools(['install-python-env', '--features=-gdbgui']) + # After removing the gdbgui should not be present + self.assertIn(CONSTR, output) + self.assertIn(REQ_CORE, output) + self.assertNotIn(REQ_GDBGUI, output) + def test_no_constraints(self): # type: () -> None output = self.run_idf_tools(['install-python-env', '--no-constraints']) self.assertNotIn(CONSTR, output)