diff --git a/docs b/docs index 0c41108f..4c8e6f1f 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 0c41108fe3d8e577bf6b4cf5074a08ad104571f0 +Subproject commit 4c8e6f1fbfb26e183c28f825de8b0fa0099d233b diff --git a/platformio/package/commands/install.py b/platformio/package/commands/install.py index cc5ced44..cfa54a5a 100644 --- a/platformio/package/commands/install.py +++ b/platformio/package/commands/install.py @@ -21,7 +21,9 @@ from platformio import fs from platformio.package.manager.library import LibraryPackageManager from platformio.package.manager.platform import PlatformPackageManager from platformio.package.manager.tool import ToolPackageManager +from platformio.package.meta import PackageSpec from platformio.project.config import ProjectConfig +from platformio.project.savedeps import save_project_dependencies @click.command( @@ -37,13 +39,18 @@ from platformio.project.config import ProjectConfig @click.option("-p", "--platform", "platforms", multiple=True) @click.option("-t", "--tool", "tools", multiple=True) @click.option("-l", "--library", "libraries", multiple=True) +@click.option( + "--no-save", + is_flag=True, + help="Prevent saving specified packages to `platformio.ini`", +) @click.option("--skip-dependencies", is_flag=True, help="Skip package dependencies") @click.option("-g", "--global", is_flag=True, help="Install package globally") @click.option( "--storage-dir", default=None, type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), - help="Custom package storage directory", + help="Custom Package Manager storage for global packages", ) @click.option("-f", "--force", is_flag=True, help="Reinstall package if it exists") @click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting") @@ -168,21 +175,33 @@ def _install_project_env_libraries(project_env, options): if options.get("libraries"): if not options.get("silent"): lm.set_log_level(logging.DEBUG) - for spec in options.get("libraries", []): - lm.install( + specs_to_save = [] + for library in options.get("libraries", []): + spec = PackageSpec(library) + pkg = lm.install( spec, skip_dependencies=options.get("skip_dependencies"), force=options.get("force"), ) + specs_to_save.append(_pkg_to_save_spec(pkg, spec)) + if not options.get("no_save") and specs_to_save: + save_project_dependencies( + os.getcwd(), + specs_to_save, + scope="lib_deps", + action="add", + environments=[project_env], + ) return not already_up_to_date if options.get("platforms") or options.get("tools"): return False # if not custom platforms/tools, install declared libraries - for spec in config.get(f"env:{project_env}", "lib_deps"): + for library in config.get(f"env:{project_env}", "lib_deps"): + spec = PackageSpec(library) # skip built-in dependencies - if "/" not in spec: + if not spec.external and not spec.owner: continue if not lm.get_package(spec): already_up_to_date = False @@ -192,3 +211,19 @@ def _install_project_env_libraries(project_env, options): force=options.get("force"), ) return not already_up_to_date + + +def _pkg_to_save_spec(pkg, user_spec): + assert isinstance(user_spec, PackageSpec) + if user_spec.external: + return user_spec + return PackageSpec( + owner=pkg.metadata.spec.owner, + name=pkg.metadata.spec.name, + requirements=user_spec.requirements + or ( + ("^%s" % pkg.metadata.version) + if not pkg.metadata.version.build + else pkg.metadata.version + ), + ) diff --git a/platformio/project/savedeps.py b/platformio/project/savedeps.py new file mode 100644 index 00000000..2b2fe8f3 --- /dev/null +++ b/platformio/project/savedeps.py @@ -0,0 +1,71 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os + +from platformio.compat import ci_strings_are_equal +from platformio.package.meta import PackageSpec +from platformio.project.config import ProjectConfig +from platformio.project.exception import InvalidProjectConfError + + +def save_project_dependencies( + project_dir, specs, scope, action="add", environments=None +): + config = ProjectConfig.get_instance(os.path.join(project_dir, "platformio.ini")) + config.validate(environments) + for env in config.envs(): + if environments and env not in environments: + continue + config.expand_interpolations = False + candidates = [] + try: + candidates = ignore_deps_by_specs(config.get("env:" + env, scope), specs) + except InvalidProjectConfError: + pass + if action == "add": + candidates.extend(spec.as_dependency() for spec in specs) + if candidates: + result = [] + for item in candidates: + item = item.strip() + if item and item not in result: + result.append(item) + config.set("env:" + env, scope, result) + elif config.has_option("env:" + env, scope): + config.remove_option("env:" + env, scope) + config.save() + + +def ignore_deps_by_specs(deps, specs): + result = [] + for dep in deps: + ignore_conditions = [] + depspec = PackageSpec(dep) + if depspec.external: + ignore_conditions.append(depspec in specs) + else: + for spec in specs: + if depspec.owner: + ignore_conditions.append( + ci_strings_are_equal(depspec.owner, spec.owner) + and ci_strings_are_equal(depspec.name, spec.name) + ) + else: + ignore_conditions.append( + ci_strings_are_equal(depspec.name, spec.name) + ) + if not any(ignore_conditions): + result.append(dep) + return result diff --git a/tests/commands/pkg/test_install.py b/tests/commands/pkg/test_install.py index e2ea7094..fc853da3 100644 --- a/tests/commands/pkg/test_install.py +++ b/tests/commands/pkg/test_install.py @@ -89,14 +89,18 @@ def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): ) validate_cliresult(result) with fs.cd(str(project_dir)): + config = ProjectConfig() lm = LibraryPackageManager( - os.path.join(ProjectConfig().get("platformio", "libdeps_dir"), "devkit") + os.path.join(config.get("platformio", "libdeps_dir"), "devkit") ) assert pkgs_to_names(lm.get_installed()) == ["DallasTemperature", "OneWire"] assert pkgs_to_names(ToolPackageManager().get_installed()) == [ "framework-arduino-avr-attiny", "toolchain-atmelavr", ] + assert config.get("env:devkit", "lib_deps") == [ + "milesburton/DallasTemperature@^3.9.1" + ] def test_unknown_project_dependencies( @@ -175,6 +179,25 @@ def test_custom_project_libraries( # do not expect any platforms/tools assert not os.path.exists(config.get("platformio", "platforms_dir")) assert not os.path.exists(config.get("platformio", "packages_dir")) + # check saved deps + assert config.get("env:devkit", "lib_deps") == [ + "bblanchon/ArduinoJson@^6.19.2", + ] + + # install library without saving to config + result = clirunner.invoke( + package_install_cmd, + ["-e", "devkit", "-l", "nanopb/Nanopb@^0.4.6", "--no-save"], + ) + validate_cliresult(result) + config = ProjectConfig() + lm = LibraryPackageManager( + os.path.join(config.get("platformio", "libdeps_dir"), "devkit") + ) + assert pkgs_to_names(lm.get_installed()) == ["ArduinoJson", "Nanopb"] + assert config.get("env:devkit", "lib_deps") == [ + "bblanchon/ArduinoJson@^6.19.2", + ] # unknown libraries result = clirunner.invoke( diff --git a/tests/project/test_savedeps.py b/tests/project/test_savedeps.py new file mode 100644 index 00000000..582c745d --- /dev/null +++ b/tests/project/test_savedeps.py @@ -0,0 +1,120 @@ +# Copyright (c) 2014-present PlatformIO +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from platformio.package.meta import PackageSpec +from platformio.project.config import ProjectConfig +from platformio.project.savedeps import save_project_dependencies + +PROJECT_CONFIG_TPL = """ +[env] +board = uno +framework = arduino + +[env:bare] + +[env:release] +platform = platformio/atmelavr +lib_deps = + milesburton/DallasTemperature@^3.8 + +[env:debug] +platform = platformio/atmelavr@^3.4.0 +lib_deps = + milesburton/DallasTemperature@^3.9.1 + bblanchon/ArduinoJson +""" + + +def test_save_libraries(tmp_path): + project_dir = tmp_path / "project" + project_dir.mkdir() + (project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL) + specs = [ + PackageSpec("milesburton/DallasTemperature@^3.9"), + PackageSpec("adafruit/Adafruit GPS Library@^1.6.0"), + PackageSpec("https://github.com/nanopb/nanopb.git"), + ] + + # add to the sepcified environment + save_project_dependencies( + str(project_dir), specs, scope="lib_deps", action="add", environments=["debug"] + ) + config = ProjectConfig.get_instance(str(project_dir / "platformio.ini")) + assert config.get("env:debug", "lib_deps") == [ + "bblanchon/ArduinoJson", + "milesburton/DallasTemperature@^3.9", + "adafruit/Adafruit GPS Library@^1.6.0", + "https://github.com/nanopb/nanopb.git", + ] + assert config.get("env:bare", "lib_deps") == [] + assert config.get("env:release", "lib_deps") == [ + "milesburton/DallasTemperature@^3.8" + ] + + # add to the the all environments + save_project_dependencies(str(project_dir), specs, scope="lib_deps", action="add") + config = ProjectConfig.get_instance(str(project_dir / "platformio.ini")) + assert config.get("env:debug", "lib_deps") == [ + "bblanchon/ArduinoJson", + "milesburton/DallasTemperature@^3.9", + "adafruit/Adafruit GPS Library@^1.6.0", + "https://github.com/nanopb/nanopb.git", + ] + assert config.get("env:bare", "lib_deps") == [ + "milesburton/DallasTemperature@^3.9", + "adafruit/Adafruit GPS Library@^1.6.0", + "https://github.com/nanopb/nanopb.git", + ] + assert config.get("env:release", "lib_deps") == [ + "milesburton/DallasTemperature@^3.9", + "adafruit/Adafruit GPS Library@^1.6.0", + "https://github.com/nanopb/nanopb.git", + ] + + # remove deps from env + save_project_dependencies( + str(project_dir), + [PackageSpec("milesburton/DallasTemperature")], + scope="lib_deps", + action="remove", + environments=["release"], + ) + config = ProjectConfig.get_instance(str(project_dir / "platformio.ini")) + assert config.get("env:release", "lib_deps") == [ + "adafruit/Adafruit GPS Library@^1.6.0", + "https://github.com/nanopb/nanopb.git", + ] + # invalid requirements + save_project_dependencies( + str(project_dir), + [PackageSpec("adafruit/Adafruit GPS Library@^9.9.9")], + scope="lib_deps", + action="remove", + environments=["release"], + ) + config = ProjectConfig.get_instance(str(project_dir / "platformio.ini")) + assert config.get("env:release", "lib_deps") == [ + "https://github.com/nanopb/nanopb.git", + ] + + # remove deps from all envs + save_project_dependencies( + str(project_dir), specs, scope="lib_deps", action="remove" + ) + config = ProjectConfig.get_instance(str(project_dir / "platformio.ini")) + assert config.get("env:debug", "lib_deps") == [ + "bblanchon/ArduinoJson", + ] + assert config.get("env:bare", "lib_deps") == [] + assert config.get("env:release", "lib_deps") == []