mip: Add PyPI support

pull/632/head
Jonas Scharpf 2023-03-21 14:22:07 +01:00
rodzic ea21cb3fdc
commit d8e058dab3
2 zmienionych plików z 170 dodań i 13 usunięć

Wyświetl plik

@ -115,6 +115,30 @@ mip.install(PACKAGE_NAME, index="https://USERNAME.github.io/micropython-lib/mip/
(Where `USERNAME`, `BRANCH_NAME` and `PACKAGE_NAME` are replaced with the owner (Where `USERNAME`, `BRANCH_NAME` and `PACKAGE_NAME` are replaced with the owner
of the fork, the branch the packages were built from, and the package name.) of the fork, the branch the packages were built from, and the package name.)
## Installing packages from Python Package Index
It is possible to use the `mpremote mip install` or `mip.install()` methods to
install packages built from the official
[PyPI](https://pypi.org/), [Test PyPI](https://test.pypi.org/) or a selfhosted
Python Package Index.
To install a package and its dependencies from a Python Package Index, use
commands such as:
```bash
$ mpremote connect /dev/ttyUSB0 mip install --index PACKAGE_INDEX --pypi PACKAGE_NAME
```
Or from a networked device:
```py
import mip
mip.install(PACKAGE_NAME, index=PACKAGE_INDEX, pypi=True)
```
(Where `PACKAGE_NAME` and `PACKAGE_INDEX` are replaced with the package name
and the package index URL, e.g. `https://test.pypi.org/pypi` for Test PyPI)
## Contributing ## Contributing
We use [GitHub Discussions](https://github.com/micropython/micropython/discussions) We use [GitHub Discussions](https://github.com/micropython/micropython/discussions)

Wyświetl plik

@ -1,5 +1,7 @@
# MicroPython package installer # MicroPython package installer
# MIT license; Copyright (c) 2022 Jim Mussared # MIT license
# Copyright (c) 2022 Jim Mussared
# Extended with PyPI support by brainelectronics 2023
import urequests as requests import urequests as requests
import sys import sys
@ -42,8 +44,6 @@ def _chunk(src, dest):
# Check if the specified path exists and matches the hash. # Check if the specified path exists and matches the hash.
def _check_exists(path, short_hash): def _check_exists(path, short_hash):
import os
try: try:
import binascii import binascii
import hashlib import hashlib
@ -92,16 +92,24 @@ def _download_file(url, dest):
response.close() response.close()
def _install_json(package_json_url, index, target, version, mpy): def _get_package_json(package_json_url, version):
package_json = {}
response = requests.get(_rewrite_url(package_json_url, version)) response = requests.get(_rewrite_url(package_json_url, version))
try: try:
if response.status_code != 200: if response.status_code != 200:
print("Package not found:", package_json_url) print("Package not found:", package_json_url)
return False return package_json
package_json = response.json() package_json = response.json()
finally: finally:
response.close() response.close()
return package_json
def _install_json(package_json_url, index, target, version, mpy):
package_json = _get_package_json(package_json_url, version)
for target_path, short_hash in package_json.get("hashes", ()): for target_path, short_hash in package_json.get("hashes", ()):
fs_target_path = target + "/" + target_path fs_target_path = target + "/" + target_path
if _check_exists(fs_target_path, short_hash): if _check_exists(fs_target_path, short_hash):
@ -122,11 +130,124 @@ def _install_json(package_json_url, index, target, version, mpy):
return True return True
def _install_package(package, index, target, version, mpy): def _install_tar(package_json_url, index, target, version):
import gc
package_json = _get_package_json(package_json_url, version)
meta = {}
if not version:
version = package_json.get("info", {}).get("version", "")
if version not in package_json.get("releases", ()):
print("Version {} not found".format(version))
return False
package_url = package_json["releases"][version][0]["url"]
# save some memory, the large dict is no longer required
del package_json
gc.collect()
fs_target_path = target + "/" + package_url.rsplit("/", 1)[1]
if not _download_file(package_url, fs_target_path):
print("Failed to download {} to {}".format(package_url, fs_target_path))
return False
try:
from uzlib import DecompIO
from utarfile import TarFile
gzdict_sz = 16 + 15
sz = gc.mem_free() + gc.mem_alloc()
if sz <= 65536:
gzdict_sz = 16 + 12
zipped_file = open(fs_target_path, "rb")
decompressed_file = DecompIO(zipped_file, gzdict_sz)
tar_file = TarFile(fileobj=decompressed_file)
meta = _install_tar_file(tar_file, target)
zipped_file.close()
del zipped_file
del decompressed_file
del tar_file
except Exception as e:
print("Failed to decompress downloaded file due to {}".format(e))
return False
# cleanup downloaded file
try:
from os import unlink
unlink(fs_target_path)
except Exception as e:
print("Error during cleanup of {}".format(fs_target_path), e)
gc.collect()
deps = meta.get("deps", "").rstrip()
if deps:
deps = deps.decode("utf-8").split("\n")
print("Install additional deps: {}".format(deps))
results = []
for ele in deps:
res = _install_package(
package=ele, index=index, target=target, version=None, mpy=False, pypi=True
)
if not res:
print("Package may be partially installed")
results.append(res)
return all(results)
return True
def _install_tar_file(f, target):
from utarfile import DIRTYPE
from shutil import copyfileobj
meta = {}
for info in f:
if "PaxHeader" in info.name:
continue
print("Processing: {}".format(info))
fname = info.name
try:
fname = fname[fname.index("/") + 1 :]
except ValueError:
fname = ""
save = True
for p in ("setup.", "PKG-INFO", "README"):
if fname.startswith(p) or ".egg-info" in fname:
if fname.endswith("/requires.txt"):
meta["deps"] = f.extractfile(info).read()
save = False
break
if save:
outfname = target + "/" + fname
_ensure_path_exists(outfname)
if info.type != DIRTYPE:
this_file = f.extractfile(info)
copyfileobj(this_file, open(outfname, "wb"))
return meta
def _install_package(package, index, target, version, mpy, pypi):
if ( if (
package.startswith("http://") package.startswith("http://")
or package.startswith("https://") or package.startswith("https://")
or package.startswith("github:") or package.startswith("github:")
or pypi
): ):
if package.endswith(".py") or package.endswith(".mpy"): if package.endswith(".py") or package.endswith(".mpy"):
print("Downloading {} to {}".format(package, target)) print("Downloading {} to {}".format(package, target))
@ -134,11 +255,23 @@ def _install_package(package, index, target, version, mpy):
_rewrite_url(package, version), target + "/" + package.rsplit("/")[-1] _rewrite_url(package, version), target + "/" + package.rsplit("/")[-1]
) )
else: else:
if not package.endswith(".json"): if pypi:
if not package.endswith("/"): this_version = version
package += "/" if not version:
package += "package.json" this_version = "latest"
print("Installing {} to {}".format(package, target)) print(
"Installing {} ({}) from {} to {}".format(package, this_version, index, target)
)
package = "{}/{}/json".format(index, package)
install("utarfile")
install("shutil")
return _install_tar(package, index, target, version)
else:
if not package.endswith(".json"):
if not package.endswith("/"):
package += "/"
package += "package.json"
print("Installing {} to {}".format(package, target))
else: else:
if not version: if not version:
version = "latest" version = "latest"
@ -153,7 +286,7 @@ def _install_package(package, index, target, version, mpy):
return _install_json(package, index, target, version, mpy) return _install_json(package, index, target, version, mpy)
def install(package, index=None, target=None, version=None, mpy=True): def install(package, index=None, target=None, version=None, mpy=True, pypi=False):
if not target: if not target:
for p in sys.path: for p in sys.path:
if p.endswith("/lib"): if p.endswith("/lib"):
@ -166,7 +299,7 @@ def install(package, index=None, target=None, version=None, mpy=True):
if not index: if not index:
index = _PACKAGE_INDEX index = _PACKAGE_INDEX
if _install_package(package, index.rstrip("/"), target, version, mpy): if _install_package(package, index.rstrip("/"), target, version, mpy, pypi):
print("Done") print("Done")
else: else:
print("Package may be partially installed") print("Package may be partially installed")