New options for system prune command: remove unnecessary core and development platform packages // Resolve #923

This commit is contained in:
Ivan Kravets
2021-01-23 23:20:53 +02:00
parent 92655c30c1
commit 59b02120b6
8 changed files with 247 additions and 44 deletions

View File

@ -11,16 +11,30 @@ PlatformIO Core 5
5.0.5 (2021-??-??) 5.0.5 (2021-??-??)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~
* Significantly speedup PlatformIO Home loading time by migrating to native Python 3 Asynchronous I/O * **Build System**
* Improved listing of `multicast DNS services <https://docs.platformio.org/page/core/userguide/device/cmd_list.html>`_
* Check for debugging server's "ready_pattern" in "stderr" - Upgraded build engine to the SCons 4.1 (`release notes <https://scons.org/scons-410-is-available.html>`_)
* Upgraded build engine to the SCons 4.1 (`release notes <https://scons.org/scons-410-is-available.html>`_) - Fixed an issue with Python 3.8+ on Windows when a network drive is used (`issue #3417 <https://github.com/platformio/platformio-core/issues/3417>`_)
* Disabled automatic removal of unnecessary development platform packages (`issue #3708 <https://github.com/platformio/platformio-core/issues/3708>`_, `issue #3770 <https://github.com/platformio/platformio-core/issues/3770>`_) - Fixed an issue when "strict" compatibility mode was not used for a library with custom "platforms" field in `library.json <https://docs.platformio.org/page/librarymanager/config.html>`__ manifest (`issue #3806 <https://github.com/platformio/platformio-core/issues/3806>`_)
* Fixed a "UnicodeDecodeError: 'utf-8' codec can't decode byte" when using J-Link for firmware uploading on Linux (`issue #3804 <https://github.com/platformio/platformio-core/issues/3804>`_)
* Fixed an issue with Python 3.8+ on Windows when a network drive is used (`issue #3417 <https://github.com/platformio/platformio-core/issues/3417>`_) * **Package Management System**
* Fixed an issue when "strict" compatibility mode was not used for a library with custom "platforms" field in `library.json <https://docs.platformio.org/page/librarymanager/config.html>`__ manifest (`issue #3806 <https://github.com/platformio/platformio-core/issues/3806>`_)
* Fixed an issue with compiler driver for ".ccls" language server (`issue #3808 <https://github.com/platformio/platformio-core/issues/3808>`_) - New options for `system prune <https://docs.platformio.org/page/core/userguide/system/cmd_prune.html>`__ command:
* Fixed an issue when unnecessary packages were removed in ``update --dry-run`` mode (`issue #3809 <https://github.com/platformio/platformio-core/issues/3809>`_)
+ ``--dry-run`` option to show data that will be removed
+ ``--core-packages`` option to remove unnecessary core packages
+ ``--platform-packages`` option to remove unnecessary development platform packages (`issue #923 <https://github.com/platformio/platformio-core/issues/923>`_)
- Disabled automatic removal of unnecessary development platform packages (`issue #3708 <https://github.com/platformio/platformio-core/issues/3708>`_, `issue #3770 <https://github.com/platformio/platformio-core/issues/3770>`_)
- Fixed an issue when unnecessary packages were removed in ``update --dry-run`` mode (`issue #3809 <https://github.com/platformio/platformio-core/issues/3809>`_)
* **Miscellaneous**
- Significantly speedup PlatformIO Home loading time by migrating to native Python 3 Asynchronous I/O
- Improved listing of `multicast DNS services <https://docs.platformio.org/page/core/userguide/device/cmd_list.html>`_
- Check for debugging server's "ready_pattern" in "stderr"
- Fixed a "UnicodeDecodeError: 'utf-8' codec can't decode byte" when using J-Link for firmware uploading on Linux (`issue #3804 <https://github.com/platformio/platformio-core/issues/3804>`_)
- Fixed an issue with a compiler driver for ".ccls" language server (`issue #3808 <https://github.com/platformio/platformio-core/issues/3808>`_)
5.0.4 (2020-12-30) 5.0.4 (2020-12-30)
~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~

2
docs

Submodule docs updated: 04a05d7f34...23165e88d7

View File

@ -13,7 +13,6 @@
# limitations under the License. # limitations under the License.
import json import json
import os
import platform import platform
import subprocess import subprocess
import sys import sys
@ -27,11 +26,15 @@ from platformio.commands.system.completion import (
install_completion_code, install_completion_code,
uninstall_completion_code, uninstall_completion_code,
) )
from platformio.commands.system.prune import (
prune_cached_data,
prune_core_packages,
prune_platform_packages,
)
from platformio.package.manager.library import LibraryPackageManager from platformio.package.manager.library import LibraryPackageManager
from platformio.package.manager.platform import PlatformPackageManager from platformio.package.manager.platform import PlatformPackageManager
from platformio.package.manager.tool import ToolPackageManager from platformio.package.manager.tool import ToolPackageManager
from platformio.project.config import ProjectConfig from platformio.project.config import ProjectConfig
from platformio.project.helpers import get_project_cache_dir
@click.group("system", short_help="Miscellaneous system commands") @click.group("system", short_help="Miscellaneous system commands")
@ -99,22 +102,49 @@ def system_info(json_output):
@cli.command("prune", short_help="Remove unused data") @cli.command("prune", short_help="Remove unused data")
@click.option("--force", "-f", is_flag=True, help="Do not prompt for confirmation") @click.option("--force", "-f", is_flag=True, help="Do not prompt for confirmation")
def system_prune(force): @click.option(
click.secho("WARNING! This will remove:", fg="yellow") "--dry-run", is_flag=True, help="Do not prune, only show data that will be removed"
click.echo(" - cached API requests") )
click.echo(" - cached package downloads") @click.option("--cache", is_flag=True, help="Prune only cached data")
click.echo(" - temporary data") @click.option(
if not force: "--core-packages", is_flag=True, help="Prune only unnecessary core packages"
click.confirm("Do you want to continue?", abort=True) )
@click.option(
"--platform-packages",
is_flag=True,
help="Prune only unnecessary development platform packages",
)
def system_prune(force, dry_run, cache, core_packages, platform_packages):
if dry_run:
click.secho(
"Dry run mode (do not prune, only show data that will be removed)",
fg="yellow",
)
click.echo()
reclaimed_total = 0 reclaimed_cache = 0
cache_dir = get_project_cache_dir() reclaimed_core_packages = 0
if os.path.isdir(cache_dir): reclaimed_platform_packages = 0
reclaimed_total += fs.calculate_folder_size(cache_dir) prune_all = not any([cache, core_packages, platform_packages])
fs.rmtree(cache_dir)
if cache or prune_all:
reclaimed_cache = prune_cached_data(force, dry_run)
click.echo()
if core_packages or prune_all:
reclaimed_core_packages = prune_core_packages(force, dry_run)
click.echo()
if platform_packages or prune_all:
reclaimed_platform_packages = prune_platform_packages(force, dry_run)
click.echo()
click.secho( click.secho(
"Total reclaimed space: %s" % fs.humanize_file_size(reclaimed_total), fg="green" "Total reclaimed space: %s"
% fs.humanize_file_size(
reclaimed_cache + reclaimed_core_packages + reclaimed_platform_packages
),
fg="green",
) )

View File

@ -0,0 +1,84 @@
# 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 os
from operator import itemgetter
import click
from tabulate import tabulate
from platformio import fs
from platformio.package.manager.core import remove_unnecessary_core_packages
from platformio.package.manager.platform import remove_unnecessary_platform_packages
from platformio.project.helpers import get_project_cache_dir
def prune_cached_data(force, dry_run):
reclaimed_space = 0
click.secho("Prune cached data:", bold=True)
click.echo(" - cached API requests")
click.echo(" - cached package downloads")
click.echo(" - temporary data")
cache_dir = get_project_cache_dir()
if os.path.isdir(cache_dir):
reclaimed_space += fs.calculate_folder_size(cache_dir)
if not dry_run:
if not force:
click.confirm("Do you want to continue?", abort=True)
fs.rmtree(cache_dir)
click.secho("Space on disk: %s" % fs.humanize_file_size(reclaimed_space))
return reclaimed_space
def prune_core_packages(force, dry_run):
click.secho("Prune unnecessary core packages:", bold=True)
return _prune_packages(force, dry_run, remove_unnecessary_core_packages)
def prune_platform_packages(force, dry_run):
click.secho("Prune unnecessary development platform packages:", bold=True)
return _prune_packages(force, dry_run, remove_unnecessary_platform_packages)
def _prune_packages(force, dry_run, handler):
click.echo("Calculating...")
items = [
(
pkg,
fs.calculate_folder_size(pkg.path),
)
for pkg in handler(dry_run=True)
]
items = sorted(items, key=itemgetter(1), reverse=True)
reclaimed_space = sum([item[1] for item in items])
if items:
click.echo(
tabulate(
[
(
pkg.metadata.spec.humanize(),
str(pkg.metadata.version),
fs.humanize_file_size(size),
)
for (pkg, size) in items
],
headers=["Package", "Version", "Size"],
)
)
if not dry_run:
if not force:
click.confirm("Do you want to continue?", abort=True)
handler(dry_run=False)
click.secho("Space on disk: %s" % fs.humanize_file_size(reclaimed_space))
return reclaimed_space

View File

@ -27,6 +27,17 @@ from platformio.package.meta import PackageItem, PackageSpec
from platformio.proc import get_pythonexe_path from platformio.proc import get_pythonexe_path
def get_installed_core_packages():
result = []
pm = ToolPackageManager()
for name, requirements in __core_packages__.items():
spec = PackageSpec(owner="platformio", name=name, requirements=requirements)
pkg = pm.get_package(spec)
if pkg:
result.append(pkg)
return result
def get_core_package_dir(name, auto_install=True): def get_core_package_dir(name, auto_install=True):
if name not in __core_packages__: if name not in __core_packages__:
raise exception.PlatformioException("Please upgrade PlatformIO Core") raise exception.PlatformioException("Please upgrade PlatformIO Core")
@ -40,7 +51,7 @@ def get_core_package_dir(name, auto_install=True):
if not auto_install: if not auto_install:
return None return None
assert pm.install(spec) assert pm.install(spec)
_remove_unnecessary_packages() remove_unnecessary_core_packages()
return pm.get_package(spec).path return pm.get_package(spec).path
@ -54,24 +65,40 @@ def update_core_packages(only_check=False, silent=False):
if not silent or pm.outdated(pkg, spec).is_outdated(): if not silent or pm.outdated(pkg, spec).is_outdated():
pm.update(pkg, spec, only_check=only_check) pm.update(pkg, spec, only_check=only_check)
if not only_check: if not only_check:
_remove_unnecessary_packages() remove_unnecessary_core_packages()
return True return True
def _remove_unnecessary_packages(): def remove_unnecessary_core_packages(dry_run=False):
candidates = []
pm = ToolPackageManager() pm = ToolPackageManager()
best_pkg_versions = {} best_pkg_versions = {}
for name, requirements in __core_packages__.items(): for name, requirements in __core_packages__.items():
spec = PackageSpec(owner="platformio", name=name, requirements=requirements) spec = PackageSpec(owner="platformio", name=name, requirements=requirements)
pkg = pm.get_package(spec) pkg = pm.get_package(spec)
if not pkg: if not pkg:
continue continue
best_pkg_versions[pkg.metadata.name] = pkg.metadata.version best_pkg_versions[pkg.metadata.name] = pkg.metadata.version
for pkg in pm.get_installed(): for pkg in pm.get_installed():
if pkg.metadata.name not in best_pkg_versions: skip_conds = [
continue os.path.isfile(os.path.join(pkg.path, ".piokeep")),
if pkg.metadata.version != best_pkg_versions[pkg.metadata.name]: pkg.metadata.spec.owner != "platformio",
pm.uninstall(pkg) pkg.metadata.name not in best_pkg_versions,
pkg.metadata.name in best_pkg_versions
and pkg.metadata.version == best_pkg_versions[pkg.metadata.name],
]
if not any(skip_conds):
candidates.append(pkg)
if dry_run:
return candidates
for pkg in candidates:
pm.uninstall(pkg)
return candidates
def inject_contrib_pysite(verify_openssl=False): def inject_contrib_pysite(verify_openssl=False):

View File

@ -12,10 +12,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
from platformio import util from platformio import util
from platformio.clients.http import HTTPClientError, InternetIsOffline from platformio.clients.http import HTTPClientError, InternetIsOffline
from platformio.package.exception import UnknownPackageError from platformio.package.exception import UnknownPackageError
from platformio.package.manager.base import BasePackageManager from platformio.package.manager.base import BasePackageManager
from platformio.package.manager.core import get_installed_core_packages
from platformio.package.manager.tool import ToolPackageManager from platformio.package.manager.tool import ToolPackageManager
from platformio.package.meta import PackageType from platformio.package.meta import PackageType
from platformio.platform.exception import IncompatiblePlatform, UnknownBoard from platformio.platform.exception import IncompatiblePlatform, UnknownBoard
@ -164,3 +167,37 @@ class PlatformPackageManager(BasePackageManager): # pylint: disable=too-many-an
): ):
return manifest return manifest
raise UnknownBoard(id_) raise UnknownBoard(id_)
#
# Helpers
#
def remove_unnecessary_platform_packages(dry_run=False):
candidates = []
required = set()
core_packages = get_installed_core_packages()
for platform in PlatformPackageManager().get_installed():
p = PlatformFactory.new(platform)
for pkg in p.get_installed_packages(with_optional=True):
required.add(pkg)
pm = ToolPackageManager()
for pkg in pm.get_installed():
skip_conds = [
pkg.metadata.spec.url,
os.path.isfile(os.path.join(pkg.path, ".piokeep")),
pkg in required,
pkg in core_packages,
]
if not any(skip_conds):
candidates.append(pkg)
if dry_run:
return candidates
for pkg in candidates:
pm.uninstall(pkg)
return candidates

View File

@ -405,7 +405,12 @@ class PackageItem(object):
) )
def __eq__(self, other): def __eq__(self, other):
return all([self.path == other.path, self.metadata == other.metadata]) if not self.path or not other.path:
return self.path == other.path
return os.path.realpath(self.path) == os.path.realpath(other.path)
def __hash__(self):
return hash(os.path.realpath(self.path))
def exists(self): def exists(self):
return os.path.isdir(self.path) return os.path.isdir(self.path)

View File

@ -17,18 +17,18 @@ from platformio.package.meta import PackageSpec
class PlatformPackagesMixin(object): class PlatformPackagesMixin(object):
def get_package_spec(self, name): def get_package_spec(self, name, version=None):
version = self.packages[name].get("version", "") version = version or self.packages[name].get("version")
if any(c in version for c in (":", "/", "@")): if version and any(c in version for c in (":", "/", "@")):
return PackageSpec("%s=%s" % (name, version)) return PackageSpec("%s=%s" % (name, version))
return PackageSpec( return PackageSpec(
owner=self.packages[name].get("owner"), name=name, requirements=version owner=self.packages[name].get("owner"), name=name, requirements=version
) )
def get_package(self, name): def get_package(self, name, spec=None):
if not name: if not name:
return None return None
return self.pm.get_package(self.get_package_spec(name)) return self.pm.get_package(spec or self.get_package_spec(name))
def get_package_dir(self, name): def get_package_dir(self, name):
pkg = self.get_package(name) pkg = self.get_package(name)
@ -38,12 +38,18 @@ class PlatformPackagesMixin(object):
pkg = self.get_package(name) pkg = self.get_package(name)
return str(pkg.metadata.version) if pkg else None return str(pkg.metadata.version) if pkg else None
def get_installed_packages(self): def get_installed_packages(self, with_optional=False):
result = [] result = []
for name in self.packages: for name, options in self.packages.items():
pkg = self.get_package(name) versions = [options.get("version")]
if pkg: if with_optional:
result.append(pkg) versions.extend(options.get("optionalVersions", []))
for version in versions:
if not version:
continue
pkg = self.get_package(name, self.get_package_spec(name, version))
if pkg:
result.append(pkg)
return result return result
def dump_used_packages(self): def dump_used_packages(self):