diff --git a/HISTORY.rst b/HISTORY.rst index 84046623..a862278a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -24,9 +24,10 @@ PlatformIO Core 5 * `pio pkg uninstall `_ - uninstall the project dependencies or custom packages * `pio pkg update `__ - update the project dependencies or custom packages - - Added support for multi-licensed packages in `library.json `__ using SPDX Expressions (`issue #4037 `_) - - Automatically install dependencies of the local (private) libraries (`issue #2910 `_) - - Added support for dependencies declared in a "tool" type package + - Added support for `"scripts" `__ in package manifest (`issue #485 `_) + - Added support for `multi-licensed `__ packages using SPDX Expressions (`issue #4037 `_) + - Added support for `"dependencies" `__ declared in a "tool" package manifest + - Automatically install dependencies of the local (private) project libraries (`issue #2910 `_) - Ignore files according to the patterns declared in ".gitignore" when using the `pio package pack `__ command (`issue #4188 `_) - Dropped automatic updates of global libraries and development platforms (`issue #4179 `_) - Dropped support for the "pythonPackages" field in "platform.json" manifest in favor of `Extra Python Dependencies `__ diff --git a/docs b/docs index 1e6df4bb..a0c38a91 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 1e6df4bb839b370b1951abf3e26b7712c78f91c5 +Subproject commit a0c38a913845ca86a7d2495d7c9b2524dd948c73 diff --git a/platformio/package/manager/_install.py b/platformio/package/manager/_install.py index cd715e23..ed0157a5 100644 --- a/platformio/package/manager/_install.py +++ b/platformio/package/manager/_install.py @@ -102,6 +102,8 @@ class PackageManagerInstallMixin(object): % (spec.humanize(), util.get_systype()) ) + self.call_pkg_script(pkg, "postinstall") + self.log.info( click.style( "{name}@{version} has been installed!".format(**pkg.metadata.as_dict()), diff --git a/platformio/package/manager/_uninstall.py b/platformio/package/manager/_uninstall.py index 77495018..529e2432 100644 --- a/platformio/package/manager/_uninstall.py +++ b/platformio/package/manager/_uninstall.py @@ -40,6 +40,8 @@ class PackageManagerUninstallMixin(object): % (click.style(pkg.metadata.name, fg="cyan"), pkg.metadata.version) ) + self.call_pkg_script(pkg, "preuninstall") + # firstly, remove dependencies if not skip_dependencies: self.uninstall_dependencies(pkg) diff --git a/platformio/package/manager/base.py b/platformio/package/manager/base.py index 85858b91..b3db0ce0 100644 --- a/platformio/package/manager/base.py +++ b/platformio/package/manager/base.py @@ -14,12 +14,13 @@ import logging import os +import subprocess from datetime import datetime import click import semantic_version -from platformio import util +from platformio import fs, util from platformio.commands import PlatformioCLI from platformio.compat import ci_strings_are_equal from platformio.package.exception import ManifestException, MissingPackageManifestError @@ -37,6 +38,7 @@ from platformio.package.meta import ( PackageSpec, PackageType, ) +from platformio.proc import get_pythonexe_path from platformio.project.helpers import get_project_cache_dir @@ -302,3 +304,31 @@ class BasePackageManager( # pylint: disable=too-many-public-methods,too-many-in name=dependency.get("name"), requirements=dependency.get("version"), ) + + def call_pkg_script(self, pkg, event): + manifest = None + try: + manifest = self.load_manifest(pkg) + except MissingPackageManifestError: + pass + scripts = (manifest or {}).get("scripts") + if not scripts or not isinstance(scripts, dict): + return + cmd = scripts.get(event) + if not cmd: + return + shell = False + if not isinstance(cmd, list): + shell = True + cmd = [cmd] + os.environ["PIO_PYTHON_EXE"] = get_pythonexe_path() + with fs.cd(pkg.path): + if os.path.isfile(cmd[0]) and cmd[0].endswith(".py"): + cmd = [os.environ["PIO_PYTHON_EXE"]] + cmd + subprocess.run( + " ".join(cmd) if shell else cmd, + cwd=pkg.path, + shell=shell, + env=os.environ, + check=True, + ) diff --git a/platformio/package/manifest/schema.py b/platformio/package/manifest/schema.py index e0a87475..39e57a54 100644 --- a/platformio/package/manifest/schema.py +++ b/platformio/package/manifest/schema.py @@ -152,6 +152,21 @@ class ExampleSchema(StrictSchema): files = StrictListField(fields.Str, required=True) +# Fields + + +class ScriptField(fields.Field): + def _deserialize(self, value, attr, data, **kwargs): + if isinstance(value, (str, list)): + return value + raise ValidationError( + "Script value must be a command (string) or list of arguments" + ) + + +# Scheme + + class ManifestSchema(BaseSchema): # Required fields name = fields.Str( @@ -173,6 +188,10 @@ class ManifestSchema(BaseSchema): license = fields.Str(validate=validate.Length(min=1, max=255)) repository = fields.Nested(RepositorySchema) dependencies = fields.Nested(DependencySchema, many=True) + scripts = fields.Dict( + keys=fields.Str(validate=validate.OneOf(["postinstall", "preuninstall"])), + values=ScriptField(), + ) # library.json export = fields.Nested(ExportSchema) diff --git a/tests/commands/pkg/test_list.py b/tests/commands/pkg/test_list.py index 4c7ce5e3..c917879e 100644 --- a/tests/commands/pkg/test_list.py +++ b/tests/commands/pkg/test_list.py @@ -50,20 +50,20 @@ def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): ) validate_cliresult(result) assert all(token in result.output for token in ("baremetal", "devkit")) - assert result.output.count("Platform atmelavr@3.4.0") == 2 + assert result.output.count("Platform atmelavr @ 3.4.0") == 2 assert ( result.output.count( - "toolchain-atmelavr@1.70300.191015 (required: " + "toolchain-atmelavr @ 1.70300.191015 (required: " "platformio/toolchain-atmelavr @ ~1.70300.0)" ) == 2 ) assert result.output.count("Libraries") == 1 assert ( - "ArduinoJson@6.19.0+sha.9693fd2 (required: " + "ArduinoJson @ 6.19.0+sha.9693fd2 (required: " "git+https://github.com/bblanchon/ArduinoJson.git#v6.19.0)" ) in result.output - assert "OneWire@2" in result.output + assert "OneWire @ 2" in result.output # test "baremetal" result = clirunner.invoke( @@ -71,7 +71,7 @@ def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): ["-d", str(project_dir), "-e", "baremetal"], ) validate_cliresult(result) - assert "Platform atmelavr@3" in result.output + assert "Platform atmelavr @ 3" in result.output assert "Libraries" not in result.output # filter by "tool" package @@ -100,7 +100,7 @@ def test_project(clirunner, validate_cliresult, isolated_pio_core, tmp_path): def test_global_packages(clirunner, validate_cliresult, isolated_pio_core, tmp_path): result = clirunner.invoke(package_list_cmd, ["-g"]) validate_cliresult(result) - assert "atmelavr@3" in result.output + assert "atmelavr @ 3" in result.output assert "framework-arduino-avr-attiny" in result.output # only tools diff --git a/tests/package/test_manager.py b/tests/package/test_manager.py index 89132b07..ab9aa6c3 100644 --- a/tests/package/test_manager.py +++ b/tests/package/test_manager.py @@ -17,6 +17,7 @@ import logging import os import time +from pathlib import Path import pytest import semantic_version @@ -290,6 +291,44 @@ def test_install_force(isolated_pio_core, tmpdir_factory): assert pkg.metadata.version.major > 5 +def test_scripts(isolated_pio_core, tmp_path: Path): + pkg_dir = tmp_path / "foo" + scripts_dir = pkg_dir / "scripts" + scripts_dir.mkdir(parents=True) + (scripts_dir / "script.py").write_text( + """ +import sys +from pathlib import Path + +action = "postinstall" if len(sys.argv) == 1 else sys.argv[1] +Path("%s.flag" % action).touch() + +if action == "preuninstall": + Path("../%s.flag" % action).touch() +""" + ) + (pkg_dir / "library.json").write_text( + """ +{ + "name": "foo", + "version": "1.0.0", + "scripts": { + "postinstall": "scripts/script.py", + "preuninstall2": ["scripts/script.py", "preuninstall"] + } +} +""" + ) + + storage_dir = tmp_path / "storage" + lm = LibraryPackageManager(str(storage_dir)) + lm.set_log_level(logging.ERROR) + lm.install("file://%s" % str(pkg_dir)) + assert os.path.isfile(os.path.join(lm.get_package("foo").path, "postinstall.flag")) + lm.uninstall("foo") + (storage_dir / "preuninstall.flag").is_file() + + def test_get_installed(isolated_pio_core, tmpdir_factory): storage_dir = tmpdir_factory.mktemp("storage") pm = ToolPackageManager(str(storage_dir)) diff --git a/tests/package/test_manifest.py b/tests/package/test_manifest.py index 3e97c83e..c16ce04b 100644 --- a/tests/package/test_manifest.py +++ b/tests/package/test_manifest.py @@ -322,6 +322,9 @@ def test_library_json_schema(): "frameworks": "arduino", "platforms": "*", "license": "MIT", + "scripts": { + "postinstall": "script.py" + }, "examples": [ { "name": "JsonConfigFile", @@ -372,6 +375,7 @@ def test_library_json_schema(): "frameworks": ["arduino"], "platforms": ["*"], "license": "MIT", + "scripts": {"postinstall": "script.py"}, "examples": [ { "name": "JsonConfigFile",