Implement "pio pkg uninstall" command // Issue #3373

This commit is contained in:
Ivan Kravets
2022-03-16 16:23:09 +02:00
parent d2adca8d68
commit 463a16a68f
7 changed files with 671 additions and 5 deletions

2
docs

Submodule docs updated: 184bc075ed...7aa5b89963

View File

@ -19,6 +19,7 @@ from platformio.package.commands.install import package_install_cmd
from platformio.package.commands.outdated import package_outdated_cmd
from platformio.package.commands.pack import package_pack_cmd
from platformio.package.commands.publish import package_publish_cmd
from platformio.package.commands.uninstall import package_uninstall_cmd
from platformio.package.commands.unpublish import package_unpublish_cmd
@ -27,6 +28,7 @@ from platformio.package.commands.unpublish import package_unpublish_cmd
commands=[
package_exec_cmd,
package_install_cmd,
package_uninstall_cmd,
package_outdated_cmd,
package_pack_cmd,
package_publish_cmd,

View File

@ -0,0 +1,239 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# 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 logging
import os
import click
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 pkg_to_save_spec, save_project_dependencies
@click.command(
"uninstall", short_help="Uninstall the project dependencies or custom packages"
)
@click.option(
"-d",
"--project-dir",
default=os.getcwd,
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
)
@click.option("-e", "--environment", "environments", multiple=True)
@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 removing specified packages from `platformio.ini`",
)
@click.option("--skip-dependencies", is_flag=True, help="Skip package dependencies")
@click.option("-g", "--global", is_flag=True, help="Uninstall global packages")
@click.option(
"--storage-dir",
default=None,
type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True),
help="Custom Package Manager storage for global packages",
)
@click.option("-s", "--silent", is_flag=True, help="Suppress progress reporting")
def package_uninstall_cmd(**options):
if options.get("global"):
uninstall_global_dependencies(options)
else:
uninstall_project_dependencies(options)
def uninstall_global_dependencies(options):
pm = PlatformPackageManager(options.get("storage_dir"))
tm = ToolPackageManager(options.get("storage_dir"))
lm = LibraryPackageManager(options.get("storage_dir"))
for obj in (pm, tm, lm):
obj.set_log_level(logging.WARN if options.get("silent") else logging.DEBUG)
for spec in options.get("platforms"):
pm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
for spec in options.get("tools"):
tm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
for spec in options.get("libraries", []):
lm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
def uninstall_project_dependencies(options):
environments = options["environments"]
with fs.cd(options["project_dir"]):
config = ProjectConfig.get_instance()
config.validate(environments)
for env in config.envs():
if environments and env not in environments:
continue
if not options["silent"]:
click.echo(
"Resolving %s environment packages..." % click.style(env, fg="cyan")
)
already_up_to_date = not uninstall_project_env_dependencies(env, options)
if not options["silent"] and already_up_to_date:
click.secho("Already up-to-date.", fg="green")
def uninstall_project_env_dependencies(project_env, options=None):
options = options or {}
uninstalled_conds = []
# custom platforms
if options.get("platforms"):
uninstalled_conds.append(
_uninstall_project_env_custom_platforms(project_env, options)
)
# custom tools
if options.get("tools"):
uninstalled_conds.append(
_uninstall_project_env_custom_tools(project_env, options)
)
# custom ibraries
if options.get("libraries"):
uninstalled_conds.append(
_uninstall_project_env_custom_libraries(project_env, options)
)
# declared dependencies
if not uninstalled_conds:
uninstalled_conds = [
_uninstall_project_env_platform(project_env, options),
_uninstall_project_env_libraries(project_env, options),
]
return any(uninstalled_conds)
def _uninstall_project_env_platform(project_env, options):
config = ProjectConfig.get_instance()
pm = PlatformPackageManager()
if options.get("silent"):
pm.set_log_level(logging.WARN)
spec = config.get(f"env:{project_env}", "platform")
if not spec:
return False
already_up_to_date = True
if not pm.get_package(spec):
return None
PlatformPackageManager().uninstall(
spec,
project_env=project_env,
skip_dependencies=options.get("skip_dependencies"),
)
return not already_up_to_date
def _uninstall_project_env_custom_platforms(project_env, options):
already_up_to_date = True
pm = PlatformPackageManager()
if not options.get("silent"):
pm.set_log_level(logging.DEBUG)
for spec in options.get("platforms"):
if pm.get_package(spec):
already_up_to_date = False
pm.uninstall(
spec,
project_env=project_env,
skip_dependencies=options.get("skip_dependencies"),
)
return not already_up_to_date
def _uninstall_project_env_custom_tools(project_env, options):
already_up_to_date = True
tm = ToolPackageManager()
if not options.get("silent"):
tm.set_log_level(logging.DEBUG)
specs_to_save = []
for tool in options.get("tools"):
spec = PackageSpec(tool)
if tm.get_package(spec):
already_up_to_date = False
pkg = tm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
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="platform_packages",
action="remove",
environments=[project_env],
)
return not already_up_to_date
def _uninstall_project_env_libraries(project_env, options):
already_up_to_date = True
config = ProjectConfig.get_instance()
lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), project_env)
)
if options.get("silent"):
lm.set_log_level(logging.WARN)
for library in config.get(f"env:{project_env}", "lib_deps"):
spec = PackageSpec(library)
# skip built-in dependencies
if not spec.external and not spec.owner:
continue
if lm.get_package(spec):
already_up_to_date = False
lm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
return not already_up_to_date
def _uninstall_project_env_custom_libraries(project_env, options):
already_up_to_date = True
config = ProjectConfig.get_instance()
lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), project_env)
)
if not options.get("silent"):
lm.set_log_level(logging.DEBUG)
specs_to_save = []
for library in options.get("libraries") or []:
spec = PackageSpec(library)
if lm.get_package(spec):
already_up_to_date = False
pkg = lm.uninstall(
spec,
skip_dependencies=options.get("skip_dependencies"),
)
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="remove",
environments=[project_env],
)
return not already_up_to_date

View File

@ -79,7 +79,7 @@ class PackageManagerUninstallMixin(object):
dependencies = self.load_manifest(pkg).get("dependencies")
if not dependencies:
return
self.log.info(click.style("Removing dependencies...", fg="yellow"))
self.log.info("Removing dependencies...")
for dependency in dependencies:
spec = PackageSpec(
owner=dependency.get("owner"),

View File

@ -79,18 +79,23 @@ class PlatformPackageManager(BasePackageManager): # pylint: disable=too-many-an
p.on_installed()
return pkg
def uninstall(self, spec, skip_dependencies=False):
def uninstall( # pylint: disable=arguments-differ
self, spec, skip_dependencies=False, project_env=None
):
pkg = self.get_package(spec)
if not pkg or not pkg.metadata:
raise UnknownPackageError(spec)
p = PlatformFactory.new(pkg)
# set logging level for underlying tool manager
p.pm.set_log_level(self.log.getEffectiveLevel())
if project_env:
p.configure_project_packages(project_env)
if not skip_dependencies:
p.uninstall_packages()
assert super(PlatformPackageManager, self).uninstall(
pkg, skip_dependencies=True
)
if not skip_dependencies:
p.on_uninstalled()
p.on_uninstalled()
return pkg
def update( # pylint: disable=arguments-differ, too-many-arguments

View File

@ -114,6 +114,10 @@ class PlatformPackagesMixin(object):
return result
def uninstall_packages(self):
for pkg in self.get_installed_packages():
self.pm.uninstall(pkg)
def update_packages(self, only_check=False):
for pkg in self.get_installed_packages():
self.pm.update(

View File

@ -0,0 +1,416 @@
# Copyright (c) 2014-present PlatformIO <contact@platformio.org>
#
# 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.
# pylint: disable=unused-argument
import os
import pytest
from platformio import fs
from platformio.package.commands.install import package_install_cmd
from platformio.package.commands.uninstall import package_uninstall_cmd
from platformio.package.manager.library import LibraryPackageManager
from platformio.package.manager.platform import PlatformPackageManager
from platformio.package.manager.tool import ToolPackageManager
from platformio.project.config import ProjectConfig
PROJECT_CONFIG_TPL = """
[env]
platform = platformio/atmelavr@^3.4.0
lib_deps = milesburton/DallasTemperature@^3.9.1
[env:baremetal]
board = uno
[env:devkit]
framework = arduino
board = attiny88
"""
def pkgs_to_names(pkgs):
return [pkg.metadata.name for pkg in pkgs]
def test_global_packages(
clirunner, validate_cliresult, func_isolated_pio_core, tmp_path
):
# libraries
result = clirunner.invoke(
package_install_cmd,
[
"--global",
"-l",
"marvinroger/Homie@^3.0.1",
],
)
validate_cliresult(result)
assert "Warning! Could not install dependency {'name': 'Hash'" in result.output
assert pkgs_to_names(LibraryPackageManager().get_installed()) == [
"ArduinoJson",
"AsyncMqttClient",
"AsyncTCP",
"Bounce2",
"ESP Async WebServer",
"ESPAsyncTCP",
"Homie",
]
# uninstall all deps
result = clirunner.invoke(
package_uninstall_cmd,
[
"--global",
"-l",
"Homie",
],
)
validate_cliresult(result)
assert not pkgs_to_names(LibraryPackageManager().get_installed())
# skip dependencies
validate_cliresult(
clirunner.invoke(
package_install_cmd,
[
"--global",
"-l",
"marvinroger/Homie@^3.0.1",
],
)
)
result = clirunner.invoke(
package_uninstall_cmd,
["--global", "-l", "marvinroger/Homie@^3.0.1", "--skip-dependencies"],
)
validate_cliresult(result)
assert pkgs_to_names(LibraryPackageManager().get_installed()) == [
"ArduinoJson",
"AsyncMqttClient",
"AsyncTCP",
"Bounce2",
"ESP Async WebServer",
"ESPAsyncTCP",
]
# remove specific dependency
result = clirunner.invoke(
package_uninstall_cmd,
[
"--global",
"-l",
"ESP Async WebServer",
],
)
validate_cliresult(result)
assert pkgs_to_names(LibraryPackageManager().get_installed()) == [
"ArduinoJson",
"AsyncMqttClient",
"Bounce2",
]
# custom storage
storage_dir = tmp_path / "custom_lib_storage"
storage_dir.mkdir()
result = clirunner.invoke(
package_install_cmd,
[
"--global",
"--storage-dir",
str(storage_dir),
"-l",
"marvinroger/Homie@^3.0.1",
"--skip-dependencies",
],
)
validate_cliresult(result)
assert pkgs_to_names(LibraryPackageManager(storage_dir).get_installed()) == [
"Homie"
]
result = clirunner.invoke(
package_uninstall_cmd,
[
"--global",
"--storage-dir",
str(storage_dir),
"-l",
"marvinroger/Homie@^3.0.1",
],
)
validate_cliresult(result)
assert not pkgs_to_names(LibraryPackageManager(storage_dir).get_installed())
# tools
result = clirunner.invoke(
package_install_cmd,
["--global", "-t", "platformio/framework-arduino-avr-attiny@^1.5.2"],
)
validate_cliresult(result)
assert pkgs_to_names(ToolPackageManager().get_installed()) == [
"framework-arduino-avr-attiny"
]
result = clirunner.invoke(
package_uninstall_cmd,
["--global", "-t", "framework-arduino-avr-attiny"],
)
validate_cliresult(result)
assert not pkgs_to_names(ToolPackageManager().get_installed())
# platforms
result = clirunner.invoke(
package_install_cmd,
["--global", "-p", "platformio/atmelavr@^3.4.0"],
)
validate_cliresult(result)
assert pkgs_to_names(PlatformPackageManager().get_installed()) == ["atmelavr"]
assert pkgs_to_names(ToolPackageManager().get_installed()) == ["toolchain-atmelavr"]
result = clirunner.invoke(
package_uninstall_cmd,
["--global", "-p", "platformio/atmelavr@^3.4.0"],
)
validate_cliresult(result)
assert not pkgs_to_names(PlatformPackageManager().get_installed())
assert not pkgs_to_names(ToolPackageManager().get_installed())
def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
result = clirunner.invoke(
package_install_cmd,
["-d", str(project_dir)],
)
validate_cliresult(result)
with fs.cd(str(project_dir)):
config = ProjectConfig()
lm = LibraryPackageManager(
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"
]
# try again
result = clirunner.invoke(
package_install_cmd,
["-d", str(project_dir)],
)
validate_cliresult(result)
assert "Already up-to-date" in result.output
# uninstall
result = clirunner.invoke(
package_uninstall_cmd,
["-d", str(project_dir)],
)
validate_cliresult(result)
with fs.cd(str(project_dir)):
config = ProjectConfig()
lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), "devkit")
)
assert not pkgs_to_names(lm.get_installed())
assert not pkgs_to_names(ToolPackageManager().get_installed())
assert config.get("env:devkit", "lib_deps") == [
"milesburton/DallasTemperature@^3.9.1"
]
def test_custom_project_libraries(
clirunner, validate_cliresult, func_isolated_pio_core, tmp_path
):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
spec = "bblanchon/ArduinoJson@^6.19.2"
result = clirunner.invoke(
package_install_cmd,
["-d", str(project_dir), "-e", "devkit", "-l", spec],
)
validate_cliresult(result)
assert "Already up-to-date" not in result.output
with fs.cd(str(project_dir)):
# check folders
config = ProjectConfig()
lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), "devkit")
)
assert pkgs_to_names(lm.get_installed()) == ["ArduinoJson"]
# 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",
]
# uninstall
result = clirunner.invoke(
package_uninstall_cmd,
["-e", "devkit", "-l", spec],
)
validate_cliresult(result)
config = ProjectConfig()
lm = LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), "devkit")
)
assert not pkgs_to_names(lm.get_installed())
# 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") == [
"milesburton/DallasTemperature@^3.9.1"
]
# install library without saving to config
result = clirunner.invoke(
package_install_cmd,
["-e", "devkit", "-l", spec, "--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"]
assert config.get("env:devkit", "lib_deps") == [
"milesburton/DallasTemperature@^3.9.1",
]
result = clirunner.invoke(
package_uninstall_cmd,
["-e", "devkit", "-l", spec, "--no-save"],
)
validate_cliresult(result)
config = ProjectConfig()
assert config.get("env:devkit", "lib_deps") == [
"milesburton/DallasTemperature@^3.9.1",
]
# unknown libraries
result = clirunner.invoke(
package_uninstall_cmd, ["-l", "platformio/unknown_library"]
)
with pytest.raises(AssertionError, match="UnknownPackageError"):
validate_cliresult(result)
def test_custom_project_tools(
clirunner, validate_cliresult, func_isolated_pio_core, tmp_path
):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
spec = "platformio/tool-openocd"
result = clirunner.invoke(
package_install_cmd,
["-d", str(project_dir), "-e", "devkit", "-t", spec],
)
validate_cliresult(result)
with fs.cd(str(project_dir)):
config = ProjectConfig()
assert pkgs_to_names(ToolPackageManager().get_installed()) == ["tool-openocd"]
assert not LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), "devkit")
).get_installed()
# do not expect any platforms
assert not os.path.exists(config.get("platformio", "platforms_dir"))
# check saved deps
assert config.get("env:devkit", "platform_packages") == [
"platformio/tool-openocd@^2.1100.211028",
]
# uninstall
result = clirunner.invoke(
package_uninstall_cmd,
["-e", "devkit", "-t", spec],
)
validate_cliresult(result)
assert not pkgs_to_names(ToolPackageManager().get_installed())
# check saved deps
assert not ProjectConfig().get("env:devkit", "platform_packages")
# install tool without saving to config
result = clirunner.invoke(
package_install_cmd,
["-e", "devkit", "-t", "platformio/tool-esptoolpy@1.20310.0"],
)
validate_cliresult(result)
assert pkgs_to_names(ToolPackageManager().get_installed()) == [
"tool-esptoolpy",
]
assert ProjectConfig().get("env:devkit", "platform_packages") == [
"platformio/tool-esptoolpy@1.20310.0",
]
# uninstall
result = clirunner.invoke(
package_uninstall_cmd,
["-e", "devkit", "-t", "platformio/tool-esptoolpy@^1", "--no-save"],
)
validate_cliresult(result)
assert not pkgs_to_names(ToolPackageManager().get_installed())
assert ProjectConfig().get("env:devkit", "platform_packages") == [
"platformio/tool-esptoolpy@1.20310.0",
]
# unknown tool
result = clirunner.invoke(
package_uninstall_cmd, ["-t", "platformio/unknown_tool"]
)
with pytest.raises(AssertionError, match="UnknownPackageError"):
validate_cliresult(result)
def test_custom_project_platforms(
clirunner, validate_cliresult, func_isolated_pio_core, tmp_path
):
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "platformio.ini").write_text(PROJECT_CONFIG_TPL)
spec = "platformio/atmelavr@^3.4.0"
result = clirunner.invoke(
package_install_cmd,
["-d", str(project_dir), "-e", "devkit", "-p", spec],
)
validate_cliresult(result)
with fs.cd(str(project_dir)):
config = ProjectConfig()
assert pkgs_to_names(PlatformPackageManager().get_installed()) == ["atmelavr"]
assert not LibraryPackageManager(
os.path.join(config.get("platformio", "libdeps_dir"), "devkit")
).get_installed()
assert pkgs_to_names(ToolPackageManager().get_installed()) == [
"framework-arduino-avr-attiny",
"toolchain-atmelavr",
]
# uninstall
result = clirunner.invoke(
package_uninstall_cmd,
["-e", "devkit", "-p", spec],
)
validate_cliresult(result)
assert not pkgs_to_names(PlatformPackageManager().get_installed())
assert not pkgs_to_names(ToolPackageManager().get_installed())
# unknown platform
with pytest.raises(
AssertionError,
match="Could not find the package with 'unknown_platform' requirements",
):
validate_cliresult(
clirunner.invoke(package_uninstall_cmd, ["-p", "unknown_platform"])
)