Significantly improve Package Manager // Resolve #913

* Handle dependencies when installing non-registry package/library (VCS, archive, local folder)
This commit is contained in:
Ivan Kravets
2017-03-08 17:24:58 +02:00
parent 41cea76603
commit 58942c3f38
13 changed files with 657 additions and 433 deletions

View File

@ -38,6 +38,7 @@ PlatformIO 3.0
* Escape project path when Glob matching is used * Escape project path when Glob matching is used
* Do not overwrite project configuration variables when system environment * Do not overwrite project configuration variables when system environment
variables are set variables are set
* Handle dependencies when installing non-registry package/library (VCS, archive, local folder)
* Fixed package installing with VCS branch for Python 2.7.3 * Fixed package installing with VCS branch for Python 2.7.3
(`issue #885 <https://github.com/platformio/platformio-core/issues/885>`_) (`issue #885 <https://github.com/platformio/platformio-core/issues/885>`_)

View File

@ -15,7 +15,7 @@
# pylint: disable=too-many-branches, too-many-locals # pylint: disable=too-many-branches, too-many-locals
import json import json
from os.path import join from os.path import isdir, join
from time import sleep from time import sleep
from urllib import quote from urllib import quote
@ -116,22 +116,23 @@ def lib_uninstall(lm, libraries):
@click.pass_obj @click.pass_obj
def lib_update(lm, libraries, only_check, json_output): def lib_update(lm, libraries, only_check, json_output):
if not libraries: if not libraries:
libraries = [] libraries = [manifest['__pkg_dir'] for manifest in lm.get_installed()]
for manifest in lm.get_installed():
if "@vcs-" in manifest['__pkg_dir']:
libraries.append("%s=%s" % (manifest['name'], manifest['url']))
else:
libraries.append(str(manifest.get("id", manifest['name'])))
if only_check and json_output: if only_check and json_output:
result = [] result = []
for library in libraries: for library in libraries:
name, requirements, url = lm.parse_pkg_name(library) pkg_dir = library if isdir(library) else None
latest = lm.outdated(name, requirements, url) requirements = None
url = None
if not pkg_dir:
name, requirements, url = lm.parse_pkg_input(library)
pkg_dir = lm.get_package_dir(name, requirements, url)
if not pkg_dir:
continue
latest = lm.outdated(pkg_dir, requirements)
if not latest: if not latest:
continue continue
manifest = lm.load_manifest( manifest = lm.load_manifest(pkg_dir)
lm.get_package_dir(name, requirements, url))
manifest['versionLatest'] = latest manifest['versionLatest'] = latest
result.append(manifest) result.append(manifest)
return click.echo(json.dumps(result)) return click.echo(json.dumps(result))
@ -167,6 +168,9 @@ def print_lib_item(item):
click.echo("Authors: %s" % ", ".join( click.echo("Authors: %s" % ", ".join(
item.get("authornames", item.get("authornames",
[a.get("name", "") for a in item.get("authors", [])]))) [a.get("name", "") for a in item.get("authors", [])])))
if "__src_url" in item:
click.secho("Source: %s" % item['__src_url'])
click.echo() click.echo()
@ -270,8 +274,7 @@ def get_builtin_libs(storage_names=None):
storage_names = storage_names or [] storage_names = storage_names or []
pm = PlatformManager() pm = PlatformManager()
for manifest in pm.get_installed(): for manifest in pm.get_installed():
p = PlatformFactory.newPlatform( p = PlatformFactory.newPlatform(manifest['__pkg_dir'])
pm.get_manifest_path(manifest['__pkg_dir']))
for storage in p.get_lib_storages(): for storage in p.get_lib_storages():
if storage_names and storage['name'] not in storage_names: if storage_names and storage['name'] not in storage_names:
continue continue
@ -308,7 +311,7 @@ def lib_builtin(storage, json_output):
@click.option("--json-output", is_flag=True) @click.option("--json-output", is_flag=True)
def lib_show(library, json_output): def lib_show(library, json_output):
lm = LibraryManager() lm = LibraryManager()
name, requirements, _ = lm.parse_pkg_name(library) name, requirements, _ = lm.parse_pkg_input(library)
lib_id = lm.get_pkg_id_by_name( lib_id = lm.get_pkg_id_by_name(
name, requirements, silent=json_output, interactive=not json_output) name, requirements, silent=json_output, interactive=not json_output)
lib = get_api_result("/lib/info/%d" % lib_id, cache_valid="1d") lib = get_api_result("/lib/info/%d" % lib_id, cache_valid="1d")

View File

@ -13,7 +13,7 @@
# limitations under the License. # limitations under the License.
import json import json
from os.path import dirname, isfile, join from os.path import dirname, isdir
import click import click
@ -94,10 +94,12 @@ def _get_installed_platform_data(platform,
# del data['version'] # del data['version']
# return data # return data
data['__pkg_dir'] = dirname(p.manifest_path) # overwrite VCS version and add extra fields
# if VCS cloned platform manifest = PlatformManager().load_manifest(dirname(p.manifest_path))
if not isfile(join(data['__pkg_dir'], "platform.json")): assert manifest
data['__pkg_dir'] = dirname(data['__pkg_dir']) for key in manifest:
if key == "version" or key.startswith("__"):
data[key] = manifest[key]
if with_boards: if with_boards:
data['boards'] = [c.get_brief_data() for c in p.get_boards().values()] data['boards'] = [c.get_brief_data() for c in p.get_boards().values()]
@ -214,7 +216,7 @@ def platform_list(json_output):
for manifest in pm.get_installed(): for manifest in pm.get_installed():
platforms.append( platforms.append(
_get_installed_platform_data( _get_installed_platform_data(
pm.get_manifest_path(manifest['__pkg_dir']), manifest['__pkg_dir'],
with_boards=False, with_boards=False,
expose_packages=False)) expose_packages=False))
if json_output: if json_output:
@ -336,22 +338,25 @@ def platform_update(platforms, only_packages, only_check, json_output):
pm = PlatformManager() pm = PlatformManager()
if not platforms: if not platforms:
platforms = [] platforms = []
for manifest in pm.get_installed(): platforms = [manifest['__pkg_dir'] for manifest in pm.get_installed()]
if "@vcs-" in manifest['__pkg_dir']:
platforms.append("%s=%s" % (manifest['name'], manifest['url']))
else:
platforms.append(manifest['name'])
if only_check and json_output: if only_check and json_output:
result = [] result = []
for platform in platforms: for platform in platforms:
name, requirements, url = pm.parse_pkg_name(platform) pkg_dir = platform if isdir(platform) else None
latest = pm.outdated(name, requirements, url) requirements = None
url = None
if not pkg_dir:
name, requirements, url = pm.parse_pkg_input(platform)
pkg_dir = pm.get_package_dir(name, requirements, url)
if not pkg_dir:
continue
latest = pm.outdated(pkg_dir, requirements)
if not latest: if not latest:
continue continue
data = _get_installed_platform_data( data = _get_installed_platform_data(
name, with_boards=False, expose_packages=False) pkg_dir, with_boards=False, expose_packages=False)
data['versionLatest'] = latest or "Unknown" data['versionLatest'] = latest
result.append(data) result.append(data)
return click.echo(json.dumps(result)) return click.echo(json.dumps(result))
else: else:

View File

@ -163,8 +163,7 @@ def after_upgrade(ctx):
# update development platforms # update development platforms
pm = PlatformManager() pm = PlatformManager()
for manifest in pm.get_installed(): for manifest in pm.get_installed():
# pm.update(manifest['name'], "^" + manifest['version']) pm.update(manifest['__pkg_dir'])
pm.update(manifest['name'])
# update PlatformIO Plus tool if installed # update PlatformIO Plus tool if installed
pioplus_update() pioplus_update()
@ -262,7 +261,7 @@ def check_internal_updates(ctx, what):
outdated_items = [] outdated_items = []
for manifest in pm.get_installed(): for manifest in pm.get_installed():
if manifest['name'] not in outdated_items and \ if manifest['name'] not in outdated_items and \
pm.outdated(manifest['name']): pm.outdated(manifest['__pkg_dir']):
outdated_items.append(manifest['name']) outdated_items.append(manifest['name'])
if not outdated_items: if not outdated_items:

View File

@ -15,10 +15,8 @@
# pylint: disable=too-many-arguments, too-many-locals, too-many-branches # pylint: disable=too-many-arguments, too-many-locals, too-many-branches
import json import json
import os
import re import re
from glob import glob from glob import glob
from hashlib import md5
from os.path import isdir, join from os.path import isdir, join
import arrow import arrow
@ -61,8 +59,8 @@ class LibraryManager(BasePkgManager):
return None return None
def load_manifest(self, path): def load_manifest(self, pkg_dir):
manifest = BasePkgManager.load_manifest(self, path) manifest = BasePkgManager.load_manifest(self, pkg_dir)
if not manifest: if not manifest:
return manifest return manifest
@ -76,6 +74,9 @@ class LibraryManager(BasePkgManager):
manifest['authors'] = [{"name": manifest['author']}] manifest['authors'] = [{"name": manifest['author']}]
del manifest['author'] del manifest['author']
if "authors" in manifest and not isinstance(manifest['authors'], list):
manifest['authors'] = [manifest['authors']]
if "keywords" not in manifest: if "keywords" not in manifest:
keywords = [] keywords = []
for keyword in re.split(r"[\s/]+", for keyword in re.split(r"[\s/]+",
@ -123,31 +124,6 @@ class LibraryManager(BasePkgManager):
return manifest return manifest
def check_pkg_structure(self, pkg_dir):
try:
return BasePkgManager.check_pkg_structure(self, pkg_dir)
except exception.MissingPackageManifest:
# we will generate manifest automatically
# if library doesn't contain any
pass
manifest = {
"name": "Library_" + md5(pkg_dir).hexdigest()[:5],
"version": "0.0.0"
}
for root, dirs, files in os.walk(pkg_dir):
if len(dirs) == 1 and not files:
manifest['name'] = dirs[0]
continue
if dirs or files:
pkg_dir = root
break
with open(join(pkg_dir, self.manifest_names[0]), "w") as fp:
json.dump(manifest, fp)
return pkg_dir
@staticmethod @staticmethod
def normalize_dependencies(dependencies): def normalize_dependencies(dependencies):
if not dependencies: if not dependencies:
@ -239,7 +215,7 @@ class LibraryManager(BasePkgManager):
}, silent, interactive)['id']) }, silent, interactive)['id'])
def _install_from_piorepo(self, name, requirements): def _install_from_piorepo(self, name, requirements):
assert name.startswith("id=") assert name.startswith("id="), name
version = self.get_latest_repo_version(name, requirements) version = self.get_latest_repo_version(name, requirements)
if not version: if not version:
raise exception.UndefinedPackageVersion(requirements or "latest", raise exception.UndefinedPackageVersion(requirements or "latest",
@ -260,28 +236,23 @@ class LibraryManager(BasePkgManager):
silent=False, silent=False,
trigger_event=True, trigger_event=True,
interactive=False): interactive=False):
already_installed = False
_name, _requirements, _url = self.parse_pkg_name(name, requirements)
try: try:
_name, _requirements, _url = self.parse_pkg_input(name,
requirements)
if not _url: if not _url:
_name = "id=%d" % self.get_pkg_id_by_name( name = "id=%d" % self.get_pkg_id_by_name(
_name, _name,
_requirements, _requirements,
silent=silent, silent=silent,
interactive=interactive) interactive=interactive)
already_installed = self.get_package(_name, _requirements, _url) requirements = _requirements
pkg_dir = BasePkgManager.install( pkg_dir = BasePkgManager.install(self, name, requirements, silent,
self, _name trigger_event)
if not _url else name, _requirements, silent, trigger_event)
except exception.InternetIsOffline as e: except exception.InternetIsOffline as e:
if not silent: if not silent:
click.secho(str(e), fg="yellow") click.secho(str(e), fg="yellow")
return return
if already_installed:
return
manifest = self.load_manifest(pkg_dir) manifest = self.load_manifest(pkg_dir)
if "dependencies" not in manifest: if "dependencies" not in manifest:
return pkg_dir return pkg_dir

View File

@ -17,7 +17,7 @@ import hashlib
import json import json
import os import os
import shutil import shutil
from os.path import basename, dirname, getsize, isdir, isfile, islink, join from os.path import basename, getsize, isdir, isfile, islink, join
from tempfile import mkdtemp from tempfile import mkdtemp
import click import click
@ -127,16 +127,36 @@ class PkgRepoMixin(object):
class PkgInstallerMixin(object): class PkgInstallerMixin(object):
VCS_MANIFEST_NAME = ".piopkgmanager.json" SRC_MANIFEST_NAME = ".piopkgmanager.json"
FILE_CACHE_VALID = "1m" # 1 month FILE_CACHE_VALID = "1m" # 1 month
FILE_CACHE_MAX_SIZE = 1024 * 1024 FILE_CACHE_MAX_SIZE = 1024 * 1024
_INSTALLED_CACHE = {} MEMORY_CACHE = {}
def reset_cache(self): @staticmethod
if self.package_dir in PkgInstallerMixin._INSTALLED_CACHE: def cache_get(key, default=None):
del PkgInstallerMixin._INSTALLED_CACHE[self.package_dir] return PkgInstallerMixin.MEMORY_CACHE.get(key, default)
@staticmethod
def cache_set(key, value):
PkgInstallerMixin.MEMORY_CACHE[key] = value
@staticmethod
def cache_reset():
PkgInstallerMixin.MEMORY_CACHE = {}
def read_dirs(self, src_dir):
cache_key = "read_dirs-%s" % src_dir
result = self.cache_get(cache_key)
if result:
return result
result = [
join(src_dir, name) for name in sorted(os.listdir(src_dir))
if isdir(join(src_dir, name))
]
self.cache_set(cache_key, result)
return result
def download(self, url, dest_dir, sha1=None): def download(self, url, dest_dir, sha1=None):
cache_key_fname = app.ContentCache.key_from_args(url, "fname") cache_key_fname = app.ContentCache.key_from_args(url, "fname")
@ -171,26 +191,23 @@ class PkgInstallerMixin(object):
return fu.start() return fu.start()
@staticmethod @staticmethod
def generate_install_dirname(manifest): def get_install_dirname(manifest):
name = manifest['name'] name = manifest['name']
if "id" in manifest: if "id" in manifest:
name += "_ID%d" % manifest['id'] name += "_ID%d" % manifest['id']
return name return name
def get_vcs_manifest_path(self, pkg_dir): def get_src_manifest_path(self, pkg_dir):
for item in os.listdir(pkg_dir): for item in os.listdir(pkg_dir):
if not isdir(join(pkg_dir, item)): if not isdir(join(pkg_dir, item)):
continue continue
if isfile(join(pkg_dir, item, self.VCS_MANIFEST_NAME)): if isfile(join(pkg_dir, item, self.SRC_MANIFEST_NAME)):
return join(pkg_dir, item, self.VCS_MANIFEST_NAME) return join(pkg_dir, item, self.SRC_MANIFEST_NAME)
return None return None
def get_manifest_path(self, pkg_dir): def get_manifest_path(self, pkg_dir):
if not isdir(pkg_dir): if not isdir(pkg_dir):
return None return None
manifest_path = self.get_vcs_manifest_path(pkg_dir)
if manifest_path:
return manifest_path
for name in self.manifest_names: for name in self.manifest_names:
manifest_path = join(pkg_dir, name) manifest_path = join(pkg_dir, name)
if isfile(manifest_path): if isfile(manifest_path):
@ -198,73 +215,104 @@ class PkgInstallerMixin(object):
return None return None
def manifest_exists(self, pkg_dir): def manifest_exists(self, pkg_dir):
return self.get_manifest_path(pkg_dir) is not None return self.get_manifest_path(pkg_dir) or \
self.get_src_manifest_path(pkg_dir)
def load_manifest(self, path): # pylint: disable=too-many-branches def load_manifest(self, pkg_dir):
assert path cache_key = "load_manifest-%s" % pkg_dir
pkg_dir = path result = self.cache_get(cache_key)
if isdir(path): if result:
path = self.get_manifest_path(path) return result
if not path:
return None
else:
pkg_dir = dirname(pkg_dir)
is_vcs_pkg = False manifest_path = self.get_manifest_path(pkg_dir)
if isfile(path) and path.endswith(self.VCS_MANIFEST_NAME): if not manifest_path:
is_vcs_pkg = True return None
pkg_dir = dirname(dirname(path))
# return from cache # if non-registry packages: VCS or archive
if self.package_dir in PkgInstallerMixin._INSTALLED_CACHE: src_manifest_path = self.get_src_manifest_path(pkg_dir)
for manifest in PkgInstallerMixin._INSTALLED_CACHE[ src_manifest = None
self.package_dir]: if src_manifest_path:
if not is_vcs_pkg and manifest['__pkg_dir'] == pkg_dir: src_manifest = util.load_json(src_manifest_path)
return manifest
manifest = {} manifest = {}
if path.endswith(".json"): if manifest_path.endswith(".json"):
manifest = util.load_json(path) manifest = util.load_json(manifest_path)
elif path.endswith(".properties"): elif manifest_path.endswith(".properties"):
with codecs.open(path, encoding="utf-8") as fp: with codecs.open(manifest_path, encoding="utf-8") as fp:
for line in fp.readlines(): for line in fp.readlines():
if "=" not in line: if "=" not in line:
continue continue
key, value = line.split("=", 1) key, value = line.split("=", 1)
manifest[key.strip()] = value.strip() manifest[key.strip()] = value.strip()
else:
if src_manifest:
if "name" not in manifest: if "name" not in manifest:
manifest['name'] = basename(pkg_dir) manifest['name'] = src_manifest['name']
if "version" not in manifest: if "version" in src_manifest:
manifest['version'] = "0.0.0" manifest['version'] = src_manifest['version']
manifest['__src_url'] = src_manifest['url']
if "name" not in manifest:
manifest['name'] = basename(pkg_dir)
if "version" not in manifest:
manifest['version'] = "0.0.0"
manifest['__pkg_dir'] = pkg_dir manifest['__pkg_dir'] = pkg_dir
self.cache_set(cache_key, manifest)
return manifest return manifest
def get_installed(self): def get_installed(self):
if self.package_dir in PkgInstallerMixin._INSTALLED_CACHE:
return PkgInstallerMixin._INSTALLED_CACHE[self.package_dir]
items = [] items = []
for p in sorted(os.listdir(self.package_dir)): for pkg_dir in self.read_dirs(self.package_dir):
pkg_dir = join(self.package_dir, p)
if not isdir(pkg_dir):
continue
manifest = self.load_manifest(pkg_dir) manifest = self.load_manifest(pkg_dir)
if not manifest: if not manifest:
continue continue
assert "name" in manifest assert "name" in manifest
items.append(manifest) items.append(manifest)
PkgInstallerMixin._INSTALLED_CACHE[self.package_dir] = items
return items return items
def check_pkg_structure(self, pkg_dir): def get_package(self, name, requirements=None, url=None):
if self.manifest_exists(pkg_dir): pkg_id = int(name[3:]) if name.startswith("id=") else 0
return pkg_dir best = None
for manifest in self.get_installed():
if url:
if manifest.get("__src_url") != url:
continue
elif pkg_id and manifest.get("id") != pkg_id:
continue
elif not pkg_id and manifest['name'] != name:
continue
for root, _, _ in os.walk(pkg_dir): # strict version or VCS HASH
if requirements and requirements == manifest['version']:
return manifest
try:
if requirements and not semantic_version.Spec(
requirements).match(
semantic_version.Version(
manifest['version'], partial=True)):
continue
elif not best or (semantic_version.Version(
manifest['version'], partial=True) >
semantic_version.Version(
best['version'], partial=True)):
best = manifest
except ValueError:
pass
return best
def get_package_dir(self, name, requirements=None, url=None):
manifest = self.get_package(name, requirements, url)
return manifest.get("__pkg_dir") if manifest else None
def find_pkg_root(self, src_dir):
if self.manifest_exists(src_dir):
return src_dir
for root, _, _ in os.walk(src_dir):
if self.manifest_exists(root): if self.manifest_exists(root):
return root return root
raise exception.MissingPackageManifest(", ".join(self.manifest_names)) raise exception.MissingPackageManifest(", ".join(self.manifest_names))
def _install_from_piorepo(self, name, requirements): def _install_from_piorepo(self, name, requirements):
@ -291,18 +339,25 @@ class PkgInstallerMixin(object):
util.get_systype()) util.get_systype())
return pkg_dir return pkg_dir
def _install_from_url(self, name, url, requirements=None, sha1=None): def _install_from_url(self,
name,
url,
requirements=None,
sha1=None,
track=False):
pkg_dir = None pkg_dir = None
tmp_dir = mkdtemp("-package", "installing-", self.package_dir) tmp_dir = mkdtemp("-package", "installing-", self.package_dir)
src_manifest_dir = None
src_manifest = {"name": name, "url": url, "requirements": requirements}
try: try:
if url.startswith("file://"): if url.startswith("file://"):
url = url[7:] _url = url[7:]
if isfile(url): if isfile(_url):
self.unpack(url, tmp_dir) self.unpack(_url, tmp_dir)
else: else:
util.rmtree_(tmp_dir) util.rmtree_(tmp_dir)
shutil.copytree(url, tmp_dir) shutil.copytree(_url, tmp_dir)
elif url.startswith(("http://", "https://")): elif url.startswith(("http://", "https://")):
dlpath = self.download(url, tmp_dir, sha1) dlpath = self.download(url, tmp_dir, sha1)
assert isfile(dlpath) assert isfile(dlpath)
@ -311,29 +366,52 @@ class PkgInstallerMixin(object):
else: else:
vcs = VCSClientFactory.newClient(tmp_dir, url) vcs = VCSClientFactory.newClient(tmp_dir, url)
assert vcs.export() assert vcs.export()
with open(join(vcs.storage_dir, self.VCS_MANIFEST_NAME), src_manifest_dir = vcs.storage_dir
"w") as fp: src_manifest['version'] = vcs.get_current_revision()
json.dump({
"name": name, pkg_dir = self.find_pkg_root(tmp_dir)
"version": vcs.get_current_revision(),
"url": url, # write source data to a special manifest
"requirements": requirements if track:
}, fp) if not src_manifest_dir:
src_manifest_dir = join(pkg_dir, ".pio")
self._update_src_manifest(src_manifest, src_manifest_dir)
pkg_dir = self.check_pkg_structure(tmp_dir)
pkg_dir = self._install_from_tmp_dir(pkg_dir, requirements) pkg_dir = self._install_from_tmp_dir(pkg_dir, requirements)
finally: finally:
if isdir(tmp_dir): if isdir(tmp_dir):
util.rmtree_(tmp_dir) util.rmtree_(tmp_dir)
return pkg_dir return pkg_dir
def _install_from_tmp_dir(self, tmp_dir, requirements=None): def _update_src_manifest(self, data, src_dir):
tmp_manifest_path = self.get_manifest_path(tmp_dir) if not isdir(src_dir):
tmp_manifest = self.load_manifest(tmp_manifest_path) os.makedirs(src_dir)
src_manifest_path = join(src_dir, self.SRC_MANIFEST_NAME)
_data = data
if isfile(src_manifest_path):
_data.update(util.load_json(src_manifest_path))
with open(src_manifest_path, "w") as fp:
json.dump(_data, fp)
def _install_from_tmp_dir( # pylint: disable=too-many-branches
self, tmp_dir, requirements=None):
tmp_manifest = self.load_manifest(tmp_dir)
assert set(["name", "version"]) <= set(tmp_manifest.keys()) assert set(["name", "version"]) <= set(tmp_manifest.keys())
name = self.generate_install_dirname(tmp_manifest) pkg_dirname = self.get_install_dirname(tmp_manifest)
pkg_dir = join(self.package_dir, name) pkg_dir = join(self.package_dir, pkg_dirname)
cur_manifest = self.load_manifest(pkg_dir)
tmp_semver = None
cur_semver = None
try:
tmp_semver = semantic_version.Version(
tmp_manifest['version'], partial=True)
if cur_manifest:
cur_semver = semantic_version.Version(
cur_manifest['version'], partial=True)
except ValueError:
pass
# package should satisfy requirements # package should satisfy requirements
if requirements: if requirements:
@ -341,45 +419,51 @@ class PkgInstallerMixin(object):
"Package version %s doesn't satisfy requirements %s" % ( "Package version %s doesn't satisfy requirements %s" % (
tmp_manifest['version'], requirements)) tmp_manifest['version'], requirements))
try: try:
reqspec = semantic_version.Spec(requirements) assert tmp_semver and tmp_semver in semantic_version.Spec(
tmp_version = semantic_version.Version( requirements), mismatch_error
tmp_manifest['version'], partial=True) except (AssertionError, ValueError):
assert tmp_version in reqspec, mismatch_error
except ValueError:
assert tmp_manifest['version'] == requirements, mismatch_error assert tmp_manifest['version'] == requirements, mismatch_error
if self.manifest_exists(pkg_dir): # check if package already exists
cur_manifest_path = self.get_manifest_path(pkg_dir) if cur_manifest:
cur_manifest = self.load_manifest(cur_manifest_path) # 0-overwrite, 1-rename, 2-fix to a version
action = 0
if tmp_manifest_path.endswith(self.VCS_MANIFEST_NAME): if "__src_url" in cur_manifest:
if cur_manifest.get("url") != tmp_manifest['url']: if cur_manifest['__src_url'] != tmp_manifest.get("__src_url"):
pkg_dir = join(self.package_dir, "%s@vcs-%s" % ( action = 1
name, hashlib.md5(tmp_manifest['url']).hexdigest())) elif "__src_url" in tmp_manifest:
action = 2
else: else:
try: if tmp_semver and (not cur_semver or tmp_semver > cur_semver):
tmp_version = semantic_version.Version( action = 1
tmp_manifest['version'], partial=True) elif tmp_semver and cur_semver and tmp_semver != cur_semver:
cur_version = semantic_version.Version( action = 2
cur_manifest['version'], partial=True)
# if current package version < new package, backup it # rename
if tmp_version > cur_version: if action == 1:
os.rename(pkg_dir, target_dirname = "%s@%s" % (pkg_dirname,
join(self.package_dir, "%s@%s" % cur_manifest['version'])
(name, cur_manifest['version']))) if "__src_url" in cur_manifest:
elif tmp_version < cur_version: target_dirname = "%s@src-%s" % (
pkg_dir = join(self.package_dir, "%s@%s" % pkg_dirname,
(name, tmp_manifest['version'])) hashlib.md5(cur_manifest['__src_url']).hexdigest())
except ValueError: os.rename(pkg_dir, join(self.package_dir, target_dirname))
pkg_dir = join(self.package_dir, # fix to a version
"%s@%s" % (name, tmp_manifest['version'])) elif action == 2:
target_dirname = "%s@%s" % (pkg_dirname,
tmp_manifest['version'])
if "__src_url" in tmp_manifest:
target_dirname = "%s@src-%s" % (
pkg_dirname,
hashlib.md5(tmp_manifest['__src_url']).hexdigest())
pkg_dir = join(self.package_dir, target_dirname)
# remove previous/not-satisfied package # remove previous/not-satisfied package
if isdir(pkg_dir): if isdir(pkg_dir):
util.rmtree_(pkg_dir) util.rmtree_(pkg_dir)
os.rename(tmp_dir, pkg_dir) os.rename(tmp_dir, pkg_dir)
assert isdir(pkg_dir) assert isdir(pkg_dir)
self.cache_reset()
return pkg_dir return pkg_dir
@ -400,14 +484,20 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
click.echo("%s: %s" % (self.__class__.__name__, message), nl=nl) click.echo("%s: %s" % (self.__class__.__name__, message), nl=nl)
@staticmethod @staticmethod
def parse_pkg_name( # pylint: disable=too-many-branches def parse_pkg_input( # pylint: disable=too-many-branches
text, requirements=None): text, requirements=None):
text = str(text) text = str(text)
url_marker = "://" # git@github.com:user/package.git
if not any([ url_marker = text[:4]
requirements, "@" not in text, text.startswith("git@"), if url_marker not in ("git@", "git+") or ":" not in text:
url_marker in text url_marker = "://"
]):
req_conditions = [
not requirements, "@" in text,
(url_marker != "git@" and "://git@" not in text) or
text.count("@") > 1
]
if all(req_conditions):
text, requirements = text.rsplit("@", 1) text, requirements = text.rsplit("@", 1)
if text.isdigit(): if text.isdigit():
text = "id=" + text text = "id=" + text
@ -423,22 +513,18 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
url.startswith("http") and url.startswith("http") and
(url.split("#", 1)[0] if "#" in url else url).endswith(".git") (url.split("#", 1)[0] if "#" in url else url).endswith(".git")
] ]
if any(git_conditions): if any(git_conditions):
url = "git+" + url url = "git+" + url
# Handle Developer Mbed URL # Handle Developer Mbed URL
# (https://developer.mbed.org/users/user/code/package/) # (https://developer.mbed.org/users/user/code/package/)
elif url.startswith("https://developer.mbed.org"): if url.startswith("https://developer.mbed.org"):
url = "hg+" + url url = "hg+" + url
# git@github.com:user/package.git
if url.startswith("git@"):
url_marker = "git@"
if any([s in url for s in ("\\", "/")]) and url_marker not in url: if any([s in url for s in ("\\", "/")]) and url_marker not in url:
if isfile(url) or isdir(url): if isfile(url) or isdir(url):
url = "file://" + url url = "file://" + url
elif url.count("/") == 1 and not url.startswith("git@"): elif url.count("/") == 1 and "git" not in url_marker:
url = "git+https://github.com/" + url url = "git+https://github.com/" + url
# determine name # determine name
@ -448,55 +534,13 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
_url = _url[:-1] _url = _url[:-1]
name = basename(_url) name = basename(_url)
if "." in name and not name.startswith("."): if "." in name and not name.startswith("."):
name = name.split(".", 1)[0] name = name.rsplit(".", 1)[0]
if url_marker not in url: if url_marker not in url:
url = None url = None
return (name or text, requirements, url) return (name or text, requirements, url)
def get_package(self, name, requirements=None, url=None): def outdated(self, pkg_dir, requirements=None):
pkg_id = int(name[3:]) if name.startswith("id=") else 0
best = None
reqspec = None
if requirements:
try:
reqspec = semantic_version.Spec(requirements)
except ValueError:
pass
for manifest in self.get_installed():
if pkg_id and manifest.get("id") != pkg_id:
continue
elif not pkg_id and manifest['name'] != name:
continue
elif not reqspec and (requirements or url):
conds = [
requirements == manifest['version'], url and
url in manifest.get("url", "")
]
if not best or any(conds):
best = manifest
continue
try:
if reqspec and not reqspec.match(
semantic_version.Version(
manifest['version'], partial=True)):
continue
elif not best or (semantic_version.Version(
manifest['version'], partial=True) >
semantic_version.Version(
best['version'], partial=True)):
best = manifest
except ValueError:
pass
return best
def get_package_dir(self, name, requirements=None, url=None):
package = self.get_package(name, requirements, url)
return package.get("__pkg_dir") if package else None
def outdated(self, name, requirements=None, url=None):
""" """
Has 3 different results: Has 3 different results:
`None` - unknown package, VCS is fixed to commit `None` - unknown package, VCS is fixed to commit
@ -504,27 +548,26 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
`String` - a found latest version `String` - a found latest version
""" """
latest = None latest = None
package_dir = self.get_package_dir(name, requirements, url) manifest = self.load_manifest(pkg_dir)
if not package_dir or ("@" in package_dir and # skip a fixed package to a specific version
"@vcs-" not in package_dir): if "@" in pkg_dir and "__src_url" not in manifest:
return None return None
is_vcs_pkg = False if "__src_url" in manifest:
manifest_path = self.get_vcs_manifest_path(package_dir) try:
if manifest_path: vcs = VCSClientFactory.newClient(
is_vcs_pkg = True pkg_dir, manifest['__src_url'], silent=True)
manifest = self.load_manifest(manifest_path) except (AttributeError, exception.PlatformioException):
else: return None
manifest = self.load_manifest(package_dir)
if is_vcs_pkg:
vcs = VCSClientFactory.newClient(
package_dir, manifest['url'], silent=True)
if not vcs.can_be_updated: if not vcs.can_be_updated:
return None return None
latest = vcs.get_latest_revision() latest = vcs.get_latest_revision()
else: else:
try: try:
latest = self.get_latest_repo_version( latest = self.get_latest_repo_version(
name, requirements, silent=True) "id=%d" % manifest['id']
if "id" in manifest else manifest['name'],
requirements,
silent=True)
except (exception.PlatformioException, ValueError): except (exception.PlatformioException, ValueError):
return None return None
if not latest: if not latest:
@ -543,7 +586,7 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
silent=False, silent=False,
trigger_event=True, trigger_event=True,
interactive=False): # pylint: disable=unused-argument interactive=False): # pylint: disable=unused-argument
name, requirements, url = self.parse_pkg_name(name, requirements) name, requirements, url = self.parse_pkg_input(name, requirements)
package_dir = self.get_package_dir(name, requirements, url) package_dir = self.get_package_dir(name, requirements, url)
if not package_dir or not silent: if not package_dir or not silent:
@ -560,15 +603,16 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
return package_dir return package_dir
if url: if url:
pkg_dir = self._install_from_url(name, url, requirements) pkg_dir = self._install_from_url(
name, url, requirements, track=True)
else: else:
pkg_dir = self._install_from_piorepo(name, requirements) pkg_dir = self._install_from_piorepo(name, requirements)
if not pkg_dir or not self.manifest_exists(pkg_dir): if not pkg_dir or not self.manifest_exists(pkg_dir):
raise exception.PackageInstallError(name, requirements or "*", raise exception.PackageInstallError(name, requirements or "*",
util.get_systype()) util.get_systype())
self.reset_cache()
manifest = self.load_manifest(pkg_dir) manifest = self.load_manifest(pkg_dir)
assert manifest
if trigger_event: if trigger_event:
telemetry.on_event( telemetry.on_event(
@ -576,42 +620,45 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
action="Install", action="Install",
label=manifest['name']) label=manifest['name'])
click.secho( if not silent:
"{name} @ {version} has been successfully installed!".format( click.secho(
**manifest), "{name} @ {version} has been successfully installed!".format(
fg="green") **manifest),
fg="green")
return pkg_dir return pkg_dir
def uninstall(self, name, requirements=None, trigger_event=True): def uninstall(self, package, requirements=None, trigger_event=True):
name, requirements, url = self.parse_pkg_name(name, requirements) if isdir(package):
package_dir = self.get_package_dir(name, requirements, url) pkg_dir = package
if not package_dir: else:
click.secho( name, requirements, url = self.parse_pkg_input(package,
"%s @ %s is not installed" % (name, requirements or "*"), requirements)
fg="yellow") pkg_dir = self.get_package_dir(name, requirements, url)
return
manifest = self.load_manifest(package_dir) if not pkg_dir:
raise exception.UnknownPackage("%s @ %s" %
(package, requirements or "*"))
manifest = self.load_manifest(pkg_dir)
click.echo( click.echo(
"Uninstalling %s @ %s: \t" % (click.style( "Uninstalling %s @ %s: \t" % (click.style(
manifest['name'], fg="cyan"), manifest['version']), manifest['name'], fg="cyan"), manifest['version']),
nl=False) nl=False)
if isdir(package_dir): if islink(pkg_dir):
if islink(package_dir): os.unlink(pkg_dir)
os.unlink(package_dir) else:
else: util.rmtree_(pkg_dir)
util.rmtree_(package_dir) self.cache_reset()
self.reset_cache()
# unfix package with the same name # unfix package with the same name
package_dir = self.get_package_dir(manifest['name']) pkg_dir = self.get_package_dir(manifest['name'])
if package_dir and "@" in package_dir: if pkg_dir and "@" in pkg_dir:
os.rename(package_dir, os.rename(
join(self.package_dir, pkg_dir,
self.generate_install_dirname(manifest))) join(self.package_dir, self.get_install_dirname(manifest)))
self.reset_cache() self.cache_reset()
click.echo("[%s]" % click.style("OK", fg="green")) click.echo("[%s]" % click.style("OK", fg="green"))
@ -624,25 +671,23 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
def update( # pylint: disable=too-many-return-statements def update( # pylint: disable=too-many-return-statements
self, self,
name, package,
requirements=None, requirements=None,
only_check=False): only_check=False):
name, requirements, url = self.parse_pkg_name(name, requirements) if isdir(package):
package_dir = self.get_package_dir(name, requirements, url) pkg_dir = package
if not package_dir:
click.secho(
"%s @ %s is not installed" % (name, requirements or "*"),
fg="yellow")
return
is_vcs_pkg = False
if self.get_vcs_manifest_path(package_dir):
is_vcs_pkg = True
manifest_path = self.get_vcs_manifest_path(package_dir)
else: else:
manifest_path = self.get_manifest_path(package_dir) name, requirements, url = self.parse_pkg_input(package,
requirements)
pkg_dir = self.get_package_dir(name, requirements, url)
if not pkg_dir:
raise exception.UnknownPackage("%s @ %s" %
(package, requirements or "*"))
manifest = self.load_manifest(pkg_dir)
name = manifest['name']
manifest = self.load_manifest(manifest_path)
click.echo( click.echo(
"{} {:<40} @ {:<15}".format( "{} {:<40} @ {:<15}".format(
"Checking" if only_check else "Updating", "Checking" if only_check else "Updating",
@ -651,7 +696,8 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
if not util.internet_on(): if not util.internet_on():
click.echo("[%s]" % (click.style("Off-line", fg="yellow"))) click.echo("[%s]" % (click.style("Off-line", fg="yellow")))
return return
latest = self.outdated(name, requirements, url)
latest = self.outdated(pkg_dir, requirements)
if latest: if latest:
click.echo("[%s]" % (click.style(latest, fg="red"))) click.echo("[%s]" % (click.style(latest, fg="red")))
elif latest is False: elif latest is False:
@ -659,26 +705,18 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
else: else:
click.echo("[%s]" % (click.style("Skip", fg="yellow"))) click.echo("[%s]" % (click.style("Skip", fg="yellow")))
if only_check or latest is False or (not is_vcs_pkg and not latest): if only_check or not latest:
return return
if is_vcs_pkg: if "__src_url" in manifest:
vcs = VCSClientFactory.newClient(package_dir, manifest['url']) vcs = VCSClientFactory.newClient(pkg_dir, manifest['__vcs_url'])
if not vcs.can_be_updated:
click.secho(
"Skip update because repository is fixed "
"to %s revision" % manifest['version'],
fg="yellow")
return
assert vcs.update() assert vcs.update()
with open(manifest_path, "w") as fp: self._update_src_manifest(
manifest['version'] = vcs.get_current_revision() dict(version=vcs.get_current_revision()), vcs.storage_dir)
json.dump(manifest, fp)
else: else:
self.uninstall(name, manifest['version'], trigger_event=False) self.uninstall(pkg_dir, trigger_event=False)
self.install(name, latest, trigger_event=False) self.install(name, latest, trigger_event=False)
self.reset_cache()
telemetry.on_event( telemetry.on_event(
category=self.__class__.__name__, category=self.__class__.__name__,
action="Update", action="Update",

View File

@ -63,7 +63,7 @@ class PlatformManager(BasePkgManager):
trigger_event=True, trigger_event=True,
**_): # pylint: disable=too-many-arguments **_): # pylint: disable=too-many-arguments
platform_dir = BasePkgManager.install(self, name, requirements) platform_dir = BasePkgManager.install(self, name, requirements)
p = PlatformFactory.newPlatform(self.get_manifest_path(platform_dir)) p = PlatformFactory.newPlatform(platform_dir)
# @Hook: when 'update' operation (trigger_event is False), # @Hook: when 'update' operation (trigger_event is False),
# don't cleanup packages or install them # don't cleanup packages or install them
@ -75,10 +75,16 @@ class PlatformManager(BasePkgManager):
self.cleanup_packages(p.packages.keys()) self.cleanup_packages(p.packages.keys())
return True return True
def uninstall(self, name, requirements=None, trigger_event=True): def uninstall(self, package, requirements=None, trigger_event=True):
name, requirements, _ = self.parse_pkg_name(name, requirements) if isdir(package):
p = PlatformFactory.newPlatform(name, requirements) pkg_dir = package
BasePkgManager.uninstall(self, name, requirements) else:
name, requirements, url = self.parse_pkg_input(package,
requirements)
pkg_dir = self.get_package_dir(name, requirements, url)
p = PlatformFactory.newPlatform(pkg_dir)
BasePkgManager.uninstall(self, pkg_dir, requirements)
# @Hook: when 'update' operation (trigger_event is False), # @Hook: when 'update' operation (trigger_event is False),
# don't cleanup packages or install them # don't cleanup packages or install them
@ -90,18 +96,23 @@ class PlatformManager(BasePkgManager):
def update( # pylint: disable=arguments-differ def update( # pylint: disable=arguments-differ
self, self,
name, package,
requirements=None, requirements=None,
only_packages=False, only_check=False,
only_check=False): only_packages=False):
name, requirements, _ = self.parse_pkg_name(name, requirements) if isdir(package):
pkg_dir = package
else:
name, requirements, url = self.parse_pkg_input(package,
requirements)
pkg_dir = self.get_package_dir(name, requirements, url)
p = PlatformFactory.newPlatform(name, requirements) p = PlatformFactory.newPlatform(pkg_dir)
pkgs_before = pkgs_after = p.get_installed_packages().keys() pkgs_before = pkgs_after = p.get_installed_packages().keys()
if not only_packages: if not only_packages:
BasePkgManager.update(self, name, requirements, only_check) BasePkgManager.update(self, pkg_dir, requirements, only_check)
p = PlatformFactory.newPlatform(name, requirements) p = PlatformFactory.newPlatform(pkg_dir)
pkgs_after = p.get_installed_packages().keys() pkgs_after = p.get_installed_packages().keys()
p.update_packages(only_check) p.update_packages(only_check)
@ -115,11 +126,10 @@ class PlatformManager(BasePkgManager):
return True return True
def cleanup_packages(self, names): def cleanup_packages(self, names):
self.reset_cache() self.cache_reset()
deppkgs = {} deppkgs = {}
for manifest in PlatformManager().get_installed(): for manifest in PlatformManager().get_installed():
p = PlatformFactory.newPlatform(manifest['name'], p = PlatformFactory.newPlatform(manifest['__pkg_dir'])
manifest['version'])
for pkgname, pkgmanifest in p.get_installed_packages().items(): for pkgname, pkgmanifest in p.get_installed_packages().items():
if pkgname not in deppkgs: if pkgname not in deppkgs:
deppkgs[pkgname] = set() deppkgs[pkgname] = set()
@ -131,17 +141,15 @@ class PlatformManager(BasePkgManager):
continue continue
if (manifest['name'] not in deppkgs or if (manifest['name'] not in deppkgs or
manifest['version'] not in deppkgs[manifest['name']]): manifest['version'] not in deppkgs[manifest['name']]):
pm.uninstall( pm.uninstall(manifest['__pkg_dir'], trigger_event=False)
manifest['name'], manifest['version'], trigger_event=False)
self.reset_cache() self.cache_reset()
return True return True
def get_installed_boards(self): def get_installed_boards(self):
boards = [] boards = []
for manifest in self.get_installed(): for manifest in self.get_installed():
p = PlatformFactory.newPlatform( p = PlatformFactory.newPlatform(manifest['__pkg_dir'])
self.get_manifest_path(manifest['__pkg_dir']))
for config in p.get_boards().values(): for config in p.get_boards().values():
board = config.get_brief_data() board = config.get_brief_data()
if board not in boards: if board not in boards:
@ -183,7 +191,10 @@ class PlatformFactory(object):
@classmethod @classmethod
def newPlatform(cls, name, requirements=None): def newPlatform(cls, name, requirements=None):
platform_dir = None platform_dir = None
if name.endswith("platform.json") and isfile(name): if isdir(name):
platform_dir = name
name = PlatformManager().load_manifest(platform_dir)['name']
elif name.endswith("platform.json") and isfile(name):
platform_dir = dirname(name) platform_dir = dirname(name)
name = util.load_json(name)['name'] name = util.load_json(name)['name']
else: else:
@ -249,8 +260,8 @@ class PlatformPackagesMixin(object):
if self.is_valid_requirements(version): if self.is_valid_requirements(version):
package = self.pm.get_package(name, version) package = self.pm.get_package(name, version)
else: else:
package = self.pm.get_package(*self._parse_pkg_name(name, package = self.pm.get_package(*self._parse_pkg_input(name,
version)) version))
if package: if package:
items[name] = package items[name] = package
return items return items
@ -267,46 +278,47 @@ class PlatformPackagesMixin(object):
self.pm.update("%s=%s" % (name, version), requirements, self.pm.update("%s=%s" % (name, version), requirements,
only_check) only_check)
def are_outdated_packages(self): # def are_outdated_packages(self):
latest = None # latest = None
for name in self.get_installed_packages(): # for name in self.get_installed_packages():
version = self.packages[name].get("version", "") # version = self.packages[name].get("version", "")
if self.is_valid_requirements(version): # if self.is_valid_requirements(version):
latest = self.pm.outdated(name, version) # latest = self.pm.outdated(name, version)
else: # else:
requirements = None # requirements = None
if "@" in version: # if "@" in version:
version, requirements = version.rsplit("@", 1) # version, requirements = version.rsplit("@", 1)
latest = self.pm.outdated(name, requirements, version) # latest = self.pm.outdated(name, requirements, version)
if latest or latest is None: # if latest or latest is None:
return True # return True
return False # return False
def get_package_dir(self, name): def get_package_dir(self, name):
version = self.packages[name].get("version", "") version = self.packages[name].get("version", "")
if self.is_valid_requirements(version): if self.is_valid_requirements(version):
return self.pm.get_package_dir(name, version) return self.pm.get_package_dir(name, version)
else: else:
return self.pm.get_package_dir(*self._parse_pkg_name(name, return self.pm.get_package_dir(*self._parse_pkg_input(name,
version)) version))
def get_package_version(self, name): def get_package_version(self, name):
version = self.packages[name].get("version", "") version = self.packages[name].get("version", "")
if self.is_valid_requirements(version): if self.is_valid_requirements(version):
package = self.pm.get_package(name, version) package = self.pm.get_package(name, version)
else: else:
package = self.pm.get_package(*self._parse_pkg_name(name, version)) package = self.pm.get_package(*self._parse_pkg_input(name,
version))
return package['version'] if package else None return package['version'] if package else None
@staticmethod @staticmethod
def is_valid_requirements(requirements): def is_valid_requirements(requirements):
return requirements and "://" not in requirements return requirements and "://" not in requirements
def _parse_pkg_name(self, name, version): def _parse_pkg_input(self, name, version):
requirements = None requirements = None
if "@" in version: if "@" in version:
version, requirements = version.rsplit("@", 1) version, requirements = version.rsplit("@", 1)
return self.pm.parse_pkg_name("%s=%s" % (name, version), requirements) return self.pm.parse_pkg_input("%s=%s" % (name, version), requirements)
class PlatformRunMixin(object): class PlatformRunMixin(object):

View File

@ -141,7 +141,12 @@ class memoized(object):
def __get__(self, obj, objtype): def __get__(self, obj, objtype):
'''Support instance methods.''' '''Support instance methods.'''
return functools.partial(self.__call__, obj) fn = functools.partial(self.__call__, obj)
fn.reset = self._reset
return fn
def _reset(self):
self.cache = {}
def singleton(cls): def singleton(cls):

View File

@ -16,7 +16,7 @@ import json
import re import re
from os.path import basename from os.path import basename
from platformio import util from platformio import exception, util
from platformio.commands.init import cli as cmd_init from platformio.commands.init import cli as cmd_init
from platformio.commands.lib import cli as cmd_lib from platformio.commands.lib import cli as cmd_lib
@ -37,15 +37,35 @@ def test_search(clirunner, validate_cliresult):
def test_global_install_registry(clirunner, validate_cliresult, def test_global_install_registry(clirunner, validate_cliresult,
isolated_pio_home): isolated_pio_home):
result = clirunner.invoke(cmd_lib, [ result = clirunner.invoke(cmd_lib, [
"-g", "install", "58", "OneWire", "-g", "install", "58", "547@2.2.4", "DallasTemperature",
"http://dl.platformio.org/libraries/archives/3/5174.tar.gz", "http://dl.platformio.org/libraries/archives/3/5174.tar.gz",
"ArduinoJson@5.6.7", "ArduinoJson@~5.7.0" "ArduinoJson@5.6.7", "ArduinoJson@~5.7.0", "1089@fee16e880b"
]) ])
validate_cliresult(result) validate_cliresult(result)
# check lib with duplicate URL
result = clirunner.invoke(cmd_lib, [
"-g", "install",
"http://dl.platformio.org/libraries/archives/3/5174.tar.gz"
])
validate_cliresult(result)
assert "is already installed" in result.output
# check lib with duplicate ID
result = clirunner.invoke(cmd_lib, ["-g", "install", "305"])
validate_cliresult(result)
assert "is already installed" in result.output
# install unknown library
result = clirunner.invoke(cmd_lib, ["-g", "install", "Unknown"])
assert result.exit_code != 0
assert isinstance(result.exception, exception.LibNotFound)
items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()] items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()]
items2 = [ items2 = [
"DHT22_ID58", "ArduinoJson_ID64", "ArduinoJson_ID64@5.6.7", "ArduinoJson_ID64", "ArduinoJson_ID64@5.6.7", "DallasTemperature_ID54",
"OneWire_ID1", "ESPAsyncTCP_ID305" "DHT22_ID58", "ESPAsyncTCP_ID305", "NeoPixelBus_ID547", "OneWire_ID1",
"IRremoteESP8266_ID1089"
] ]
assert set(items1) == set(items2) assert set(items1) == set(items2)
@ -55,11 +75,29 @@ def test_global_install_archive(clirunner, validate_cliresult,
result = clirunner.invoke(cmd_lib, [ result = clirunner.invoke(cmd_lib, [
"-g", "install", "https://github.com/adafruit/Adafruit-ST7735-Library/" "-g", "install", "https://github.com/adafruit/Adafruit-ST7735-Library/"
"archive/master.zip", "archive/master.zip",
"http://www.airspayce.com/mikem/arduino/RadioHead/RadioHead-1.62.zip",
"https://github.com/bblanchon/ArduinoJson/archive/v5.8.2.zip",
"https://github.com/bblanchon/ArduinoJson/archive/v5.8.2.zip@5.8.2"
])
validate_cliresult(result)
# incorrect requirements
result = clirunner.invoke(cmd_lib, [
"-g", "install",
"https://github.com/bblanchon/ArduinoJson/archive/v5.8.2.zip@1.2.3"
])
assert result.exit_code != 0
# check lib with duplicate URL
result = clirunner.invoke(cmd_lib, [
"-g", "install",
"http://www.airspayce.com/mikem/arduino/RadioHead/RadioHead-1.62.zip" "http://www.airspayce.com/mikem/arduino/RadioHead/RadioHead-1.62.zip"
]) ])
validate_cliresult(result) validate_cliresult(result)
assert "is already installed" in result.output
items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()] items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()]
items2 = ["Adafruit ST7735 Library", "RadioHead"] items2 = ["Adafruit ST7735 Library", "RadioHead-1.62"]
assert set(items1) >= set(items2) assert set(items1) >= set(items2)
@ -71,14 +109,20 @@ def test_global_install_repository(clirunner, validate_cliresult,
"-g", "-g",
"install", "install",
"https://github.com/gioblu/PJON.git#3.0", "https://github.com/gioblu/PJON.git#3.0",
"https://github.com/gioblu/PJON.git#6.2",
"https://github.com/bblanchon/ArduinoJson.git",
"https://gitlab.com/ivankravets/rs485-nodeproto.git", "https://gitlab.com/ivankravets/rs485-nodeproto.git",
# "https://developer.mbed.org/users/simon/code/TextLCD/", # "https://developer.mbed.org/users/simon/code/TextLCD/",
"knolleary/pubsubclient" "knolleary/pubsubclient"
]) ])
validate_cliresult(result) validate_cliresult(result)
items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()] items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()]
items2 = ["PJON", "ESPAsyncTCP", "PubSubClient"] items2 = [
assert set(items2) & set(items1) "PJON", "PJON@src-79de467ebe19de18287becff0a1fb42d",
"ArduinoJson@src-69ebddd821f771debe7ee734d3c7fa81", "rs485-nodeproto",
"PubSubClient"
]
assert set(items1) >= set(items2)
def test_global_lib_list(clirunner, validate_cliresult, isolated_pio_home): def test_global_lib_list(clirunner, validate_cliresult, isolated_pio_home):
@ -89,13 +133,15 @@ def test_global_lib_list(clirunner, validate_cliresult, isolated_pio_home):
result = clirunner.invoke(cmd_lib, ["-g", "list", "--json-output"]) result = clirunner.invoke(cmd_lib, ["-g", "list", "--json-output"])
assert all([ assert all([
n in result.output n in result.output
for n in ("PJON", "git+https://github.com/knolleary/pubsubclient") for n in ("PJON", "git+https://github.com/knolleary/pubsubclient",
"https://github.com/bblanchon/ArduinoJson/archive/v5.8.2.zip"
)
]) ])
items1 = [i['name'] for i in json.loads(result.output)] items1 = [i['name'] for i in json.loads(result.output)]
items2 = [ items2 = [
"OneWire", "DHT22", "PJON", "ESPAsyncTCP", "ArduinoJson", "OneWire", "DHT22", "PJON", "ESPAsyncTCP", "ArduinoJson",
"pubsubclient", "rs485-nodeproto", "Adafruit ST7735 Library", "PubSubClient", "rs485-nodeproto", "Adafruit ST7735 Library",
"RadioHead" "RadioHead-1.62", "DallasTemperature", "NeoPixelBus", "IRremoteESP8266"
] ]
assert set(items1) == set(items2) assert set(items1) == set(items2)
@ -106,31 +152,71 @@ def test_global_lib_update_check(clirunner, validate_cliresult,
cmd_lib, ["-g", "update", "--only-check", "--json-output"]) cmd_lib, ["-g", "update", "--only-check", "--json-output"])
validate_cliresult(result) validate_cliresult(result)
output = json.loads(result.output) output = json.loads(result.output)
assert set(["ArduinoJson", "ESPAsyncTCP", "RadioHead"]) == set( assert set(["ArduinoJson", "IRremoteESP8266", "NeoPixelBus"]) == set(
[l['name'] for l in output]) [l['name'] for l in output])
def test_global_lib_update(clirunner, validate_cliresult, isolated_pio_home): def test_global_lib_update(clirunner, validate_cliresult, isolated_pio_home):
# update library using package directory
result = clirunner.invoke(
cmd_lib,
["-g", "update", "NeoPixelBus", "--only-check", "--json-output"])
validate_cliresult(result)
oudated = json.loads(result.output)
assert len(oudated) == 1
assert "__pkg_dir" in oudated[0]
result = clirunner.invoke(cmd_lib,
["-g", "update", oudated[0]['__pkg_dir']])
validate_cliresult(result)
assert "Uninstalling NeoPixelBus @ 2.2.4" in result.output
# update rest libraries
result = clirunner.invoke(cmd_lib, ["-g", "update"]) result = clirunner.invoke(cmd_lib, ["-g", "update"])
validate_cliresult(result) validate_cliresult(result)
assert "[Up-to-date]" in result.output validate_cliresult(result)
assert result.output.count("[Skip]") == 5
assert result.output.count("[Up-to-date]") == 9
assert "Uninstalling ArduinoJson @ 5.7.3" in result.output assert "Uninstalling ArduinoJson @ 5.7.3" in result.output
assert "Uninstalling IRremoteESP8266 @ fee16e880b" in result.output
# update unknown library
result = clirunner.invoke(cmd_lib, ["-g", "update", "Unknown"])
assert result.exit_code != 0
assert isinstance(result.exception, exception.UnknownPackage)
def test_global_lib_uninstall(clirunner, validate_cliresult, def test_global_lib_uninstall(clirunner, validate_cliresult,
isolated_pio_home): isolated_pio_home):
# uninstall using package directory
result = clirunner.invoke(cmd_lib, ["-g", "list", "--json-output"])
validate_cliresult(result)
items = json.loads(result.output)
result = clirunner.invoke(cmd_lib,
["-g", "uninstall", items[0]['__pkg_dir']])
validate_cliresult(result)
assert "Uninstalling Adafruit ST7735 Library" in result.output
# uninstall the rest libraries
result = clirunner.invoke(cmd_lib, [ result = clirunner.invoke(cmd_lib, [
"-g", "uninstall", "1", "ArduinoJson@!=5.6.7", "TextLCD", "-g", "uninstall", "1", "ArduinoJson@!=5.6.7",
"Adafruit ST7735 Library" "https://github.com/bblanchon/ArduinoJson.git", "IRremoteESP8266@>=0.2"
]) ])
validate_cliresult(result) validate_cliresult(result)
items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()] items1 = [d.basename for d in isolated_pio_home.join("lib").listdir()]
items2 = [ items2 = [
"DHT22_ID58", "ArduinoJson_ID64", "ESPAsyncTCP_ID305", "ArduinoJson", "ArduinoJson_ID64@5.6.7", "DallasTemperature_ID54",
"pubsubclient", "PJON", "rs485-nodeproto", "RadioHead_ID124" "DHT22_ID58", "ESPAsyncTCP_ID305", "NeoPixelBus_ID547", "PJON",
"PJON@src-79de467ebe19de18287becff0a1fb42d", "PubSubClient",
"RadioHead-1.62", "rs485-nodeproto"
] ]
assert set(items1) == set(items2) assert set(items1) == set(items2)
# uninstall unknown library
result = clirunner.invoke(cmd_lib, ["-g", "uninstall", "Unknown"])
assert result.exit_code != 0
assert isinstance(result.exception, exception.UnknownPackage)
def test_lib_show(clirunner, validate_cliresult, isolated_pio_home): def test_lib_show(clirunner, validate_cliresult, isolated_pio_home):
result = clirunner.invoke(cmd_lib, ["show", "64"]) result = clirunner.invoke(cmd_lib, ["show", "64"])
@ -142,31 +228,24 @@ def test_lib_show(clirunner, validate_cliresult, isolated_pio_home):
assert "OneWire" in result.output assert "OneWire" in result.output
def test_project_lib_complex(clirunner, validate_cliresult, tmpdir): def test_lib_builtin(clirunner, validate_cliresult, isolated_pio_home):
with tmpdir.as_cwd(): result = clirunner.invoke(cmd_lib, ["builtin"])
# init validate_cliresult(result)
result = clirunner.invoke(cmd_init) result = clirunner.invoke(cmd_lib, ["builtin", "--json-output"])
validate_cliresult(result) validate_cliresult(result)
# isntall
result = clirunner.invoke(cmd_lib, ["install", "54", "ArduinoJson"])
validate_cliresult(result)
items1 = [
d.basename
for d in tmpdir.join(basename(util.get_projectlibdeps_dir()))
.listdir()
]
items2 = ["DallasTemperature_ID54", "OneWire_ID1", "ArduinoJson_ID64"]
assert set(items1) == set(items2)
# list def test_lib_stats(clirunner, validate_cliresult, isolated_pio_home):
result = clirunner.invoke(cmd_lib, ["list", "--json-output"]) result = clirunner.invoke(cmd_lib, ["stats"])
validate_cliresult(result) validate_cliresult(result)
items1 = [i['name'] for i in json.loads(result.output)] assert all([
items2 = ["DallasTemperature", "OneWire", "ArduinoJson"] s in result.output
assert set(items1) == set(items2) for s in ("UPDATED", "ago", "http://platformio.org/lib/show")
])
# update result = clirunner.invoke(cmd_lib, ["stats", "--json-output"])
result = clirunner.invoke(cmd_lib, ["update"]) validate_cliresult(result)
validate_cliresult(result) assert set([
assert "[Up-to-date]" in result.output "dlweek", "added", "updated", "topkeywords", "dlmonth", "dlday",
"lastkeywords"
]) == set(json.loads(result.output).keys())

View File

@ -156,7 +156,7 @@ def test_check_platform_updates(clirunner, validate_cliresult,
manifest['version'] = "0.0.0" manifest['version'] = "0.0.0"
manifest_path.write(json.dumps(manifest)) manifest_path.write(json.dumps(manifest))
# reset cached manifests # reset cached manifests
PlatformManager().reset_cache() PlatformManager().cache_reset()
# reset check time # reset check time
interval = int(app.get_setting("check_platforms_interval")) * 3600 * 24 interval = int(app.get_setting("check_platforms_interval")) * 3600 * 24

View File

@ -12,70 +12,118 @@
# 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 json
from os.path import join
from platformio import util from platformio import util
from platformio.managers.package import BasePkgManager from platformio.managers.package import PackageManager
def test_pkg_name_parser(): def test_pkg_input_parser():
items = [ items = [
["PkgName", ("PkgName", None, None)], ["PkgName", ("PkgName", None, None)],
[("PkgName", "!=1.2.3,<2.0"), ("PkgName", "!=1.2.3,<2.0", None)], [("PkgName", "!=1.2.3,<2.0"), ("PkgName", "!=1.2.3,<2.0", None)],
["PkgName@1.2.3", ("PkgName", "1.2.3", None)], ["PkgName@1.2.3", ("PkgName", "1.2.3", None)],
[("PkgName@1.2.3", "1.2.5"), ("PkgName@1.2.3", "1.2.5", None)], [("PkgName@1.2.3", "1.2.5"), ("PkgName@1.2.3", "1.2.5", None)],
["id:13", ("id:13", None, None)], ["id:13", ("id:13", None, None)],
["id:13@~1.2.3", ("id:13", "~1.2.3", None)], [ ["id:13@~1.2.3", ("id:13", "~1.2.3", None)],
[
util.get_home_dir(), util.get_home_dir(),
(".platformio", None, "file://" + util.get_home_dir()) (".platformio", None, "file://" + util.get_home_dir())
], [ ],
[
"LocalName=" + util.get_home_dir(), "LocalName=" + util.get_home_dir(),
("LocalName", None, "file://" + util.get_home_dir()) ("LocalName", None, "file://" + util.get_home_dir())
], [ ],
[
"LocalName=%s@>2.3.0" % util.get_home_dir(),
("LocalName", ">2.3.0", "file://" + util.get_home_dir())
],
[
"https://github.com/user/package.git", "https://github.com/user/package.git",
("package", None, "git+https://github.com/user/package.git") ("package", None, "git+https://github.com/user/package.git")
], [ ],
"https://gitlab.com/user/package.git", [
("package", None, "git+https://gitlab.com/user/package.git") "MyPackage=https://gitlab.com/user/package.git",
], [ ("MyPackage", None, "git+https://gitlab.com/user/package.git")
],
[
"MyPackage=https://gitlab.com/user/package.git@3.2.1,!=2",
("MyPackage", "3.2.1,!=2",
"git+https://gitlab.com/user/package.git")
],
[
"https://somedomain.com/path/LibraryName-1.2.3.zip",
("LibraryName-1.2.3", None,
"https://somedomain.com/path/LibraryName-1.2.3.zip")
],
[
"https://github.com/user/package/archive/branch.zip", "https://github.com/user/package/archive/branch.zip",
("branch", None, ("branch", None,
"https://github.com/user/package/archive/branch.zip") "https://github.com/user/package/archive/branch.zip")
], [ ],
[
"https://github.com/user/package/archive/branch.zip@~1.2.3",
("branch", "~1.2.3",
"https://github.com/user/package/archive/branch.zip")
],
[
"https://github.com/user/package/archive/branch.tar.gz", "https://github.com/user/package/archive/branch.tar.gz",
("branch", None, ("branch.tar", None,
"https://github.com/user/package/archive/branch.tar.gz") "https://github.com/user/package/archive/branch.tar.gz")
], [ ],
[
"https://github.com/user/package/archive/branch.tar.gz@!=5",
("branch.tar", "!=5",
"https://github.com/user/package/archive/branch.tar.gz")
],
[
"https://developer.mbed.org/users/user/code/package/", "https://developer.mbed.org/users/user/code/package/",
("package", None, ("package", None,
"hg+https://developer.mbed.org/users/user/code/package/") "hg+https://developer.mbed.org/users/user/code/package/")
], [ ],
[
"https://github.com/user/package#v1.2.3", "https://github.com/user/package#v1.2.3",
("package", None, "git+https://github.com/user/package#v1.2.3") ("package", None, "git+https://github.com/user/package#v1.2.3")
], [ ],
[
"https://github.com/user/package.git#branch", "https://github.com/user/package.git#branch",
("package", None, "git+https://github.com/user/package.git#branch") ("package", None, "git+https://github.com/user/package.git#branch")
], [ ],
[
"PkgName=https://github.com/user/package.git#a13d344fg56", "PkgName=https://github.com/user/package.git#a13d344fg56",
("PkgName", None, ("PkgName", None,
"git+https://github.com/user/package.git#a13d344fg56") "git+https://github.com/user/package.git#a13d344fg56")
], [ ],
[
"user/package",
("package", None, "git+https://github.com/user/package")
],
[
"PkgName=user/package", "PkgName=user/package",
("PkgName", None, "git+https://github.com/user/package") ("PkgName", None, "git+https://github.com/user/package")
], [ ],
[
"PkgName=user/package#master", "PkgName=user/package#master",
("PkgName", None, "git+https://github.com/user/package#master") ("PkgName", None, "git+https://github.com/user/package#master")
], [ ],
[
"git+https://github.com/user/package", "git+https://github.com/user/package",
("package", None, "git+https://github.com/user/package") ("package", None, "git+https://github.com/user/package")
], [ ],
[
"hg+https://example.com/user/package", "hg+https://example.com/user/package",
("package", None, "hg+https://example.com/user/package") ("package", None, "hg+https://example.com/user/package")
], [ ],
[
"git@github.com:user/package.git", "git@github.com:user/package.git",
("package", None, "git@github.com:user/package.git") ("package", None, "git@github.com:user/package.git")
], [ ],
[
"git@github.com:user/package.git#v1.2.0", "git@github.com:user/package.git#v1.2.0",
("package", None, "git@github.com:user/package.git#v1.2.0") ("package", None, "git@github.com:user/package.git#v1.2.0")
], [ ],
[
"git+ssh://git@gitlab.private-server.com/user/package#1.2.0", "git+ssh://git@gitlab.private-server.com/user/package#1.2.0",
("package", None, ("package", None,
"git+ssh://git@gitlab.private-server.com/user/package#1.2.0") "git+ssh://git@gitlab.private-server.com/user/package#1.2.0")
@ -83,6 +131,72 @@ def test_pkg_name_parser():
] ]
for params, result in items: for params, result in items:
if isinstance(params, tuple): if isinstance(params, tuple):
assert BasePkgManager.parse_pkg_name(*params) == result assert PackageManager.parse_pkg_input(*params) == result
else: else:
assert BasePkgManager.parse_pkg_name(params) == result assert PackageManager.parse_pkg_input(params) == result
def test_install_packages(isolated_pio_home, tmpdir):
packages = [
dict(id=1, name="name_1", version="shasum"),
dict(id=1, name="name_1", version="2.0.0"),
dict(id=1, name="name_1", version="2.1.0"),
dict(id=1, name="name_1", version="1.2.0"),
dict(id=1, name="name_1", version="1.0.0"),
dict(name="name_2", version="1.0.0"),
dict(name="name_2", version="2.0.0",
__src_url="git+https://github.com"),
dict(name="name_2", version="3.0.0",
__src_url="git+https://github2.com"),
dict(name="name_2", version="4.0.0",
__src_url="git+https://github2.com")
]
pm = PackageManager(join(util.get_home_dir(), "packages"))
for package in packages:
tmp_dir = tmpdir.mkdir("tmp-package")
tmp_dir.join("package.json").write(json.dumps(package))
pm._install_from_url(package['name'], "file://%s" % str(tmp_dir))
tmp_dir.remove(rec=1)
assert len(pm.get_installed()) == len(packages) - 1
pkg_dirnames = [
'name_1_ID1', 'name_1_ID1@1.0.0', 'name_1_ID1@1.2.0',
'name_1_ID1@2.0.0', 'name_1_ID1@shasum', 'name_2',
'name_2@src-177cbce1f0705580d17790fda1cc2ef5',
'name_2@src-f863b537ab00f4c7b5011fc44b120e1f'
]
assert set([p.basename for p in isolated_pio_home.join(
"packages").listdir()]) == set(pkg_dirnames)
def test_get_package(isolated_pio_home):
tests = [
[("unknown", ), None],
[("1", ), None],
[("id=1", "shasum"), dict(id=1, name="name_1", version="shasum")],
[("id=1", "*"), dict(id=1, name="name_1", version="2.1.0")],
[("id=1", "^1"), dict(id=1, name="name_1", version="1.2.0")],
[("id=1", "^1"), dict(id=1, name="name_1", version="1.2.0")],
[("name_1", "<2"), dict(id=1, name="name_1", version="1.2.0")],
[("name_1", ">2"), None],
[("name_1", "2-0-0"), dict(id=1, name="name_1", version="2.1.0")],
[("name_1", "2-0-0"), dict(id=1, name="name_1", version="2.1.0")],
[("name_2", ), dict(name="name_2", version="4.0.0")],
[("url_has_higher_priority", None, "git+https://github.com"),
dict(name="name_2", version="2.0.0",
__src_url="git+https://github.com")],
[("name_2", None, "git+https://github.com"),
dict(name="name_2", version="2.0.0",
__src_url="git+https://github.com")],
]
pm = PackageManager(join(util.get_home_dir(), "packages"))
for test in tests:
manifest = pm.get_package(*test[0])
if test[1] is None:
assert manifest is None, test
continue
for key, value in test[1].items():
assert manifest[key] == value, test

View File

@ -16,19 +16,6 @@ import pytest
import requests import requests
def pytest_generate_tests(metafunc):
if "package_data" not in metafunc.fixturenames:
return
pkgs_manifest = requests.get(
"https://dl.bintray.com/platformio/dl-packages/manifest.json").json()
assert isinstance(pkgs_manifest, dict)
packages = []
for _, variants in pkgs_manifest.iteritems():
for item in variants:
packages.append(item)
metafunc.parametrize("package_data", packages)
def validate_response(req): def validate_response(req):
assert req.status_code == 200 assert req.status_code == 200
assert int(req.headers['Content-Length']) > 0 assert int(req.headers['Content-Length']) > 0
@ -36,13 +23,22 @@ def validate_response(req):
"application/octet-stream") "application/octet-stream")
def test_package(package_data): def test_packages():
assert package_data['url'].endswith(".tar.gz") pkgs_manifest = requests.get(
"https://dl.bintray.com/platformio/dl-packages/manifest.json").json()
assert isinstance(pkgs_manifest, dict)
items = []
for _, variants in pkgs_manifest.iteritems():
for item in variants:
items.append(item)
r = requests.head(package_data['url'], allow_redirects=True) for item in items:
validate_response(r) assert item['url'].endswith(".tar.gz"), item
if "X-Checksum-Sha1" not in r.headers: r = requests.head(item['url'], allow_redirects=True)
return pytest.skip("X-Checksum-Sha1 is not provided") validate_response(r)
assert package_data['sha1'] == r.headers.get("X-Checksum-Sha1") if "X-Checksum-Sha1" not in r.headers:
return pytest.skip("X-Checksum-Sha1 is not provided")
assert item['sha1'] == r.headers.get("X-Checksum-Sha1"), item

View File

@ -24,6 +24,7 @@ deps =
yapf yapf
pylint pylint
pytest pytest
show
commands = python --version commands = python --version
[testenv:docs] [testenv:docs]