#!/usr/bin/env python3 # -*- coding: utf-8 -*- # This is a port of the MicroPython upip utility to CPython # Port Copyright (c) Peter Hinch # Licensed under the MIT license. # upip licensing/attribution # upip - Package manager for MicroPython # # Copyright (c) 2015-2018 Paul Sokolovsky # # Licensed under the MIT license. # # Please note that the author of upip, Paul Sokolovsky, advocates its use # rather than this port. This is true if using his MicroPython firmware, as # upip looks in his repo for library modules. # For users of mainline MicroPython this port ensures that compatible library # modules are installed. # Now searches the official library first before looking on PyPi for user # contributed packages. import sys import os import errno import json import zlib import tarfile import tempfile import ssl as ussl import socket as usocket debug = False g_install_path = os.getcwd() # Default install path gzdict_sz = 16 + 15 def version(): print('Python version 3.2 or above is required.') sys.exit(1) if sys.version_info.major < 3: version() elif sys.version_info.major == 3 and sys.version_info.minor < 2: version() class NotFoundError(Exception): pass # Read a line from a socket def read_line(sock): ret = b'' while True: c = sock.recv(1) if c == b'': break elif c == b'\n': ret += c break else: ret += c return ret # Read multiple lines from a socket def read_lines(sock): s = b'' while True: s1 = read_line(sock) s += s1 if s1 == b'\r\n' or s1 == b'': break return s # Expects absolute path and *file* name def _makedirs(name): dirname = os.path.dirname(name) def split_path(lst, path): q = os.path.split(path) if q[1] != '': lst.append(q[1]) split_path(lst, q[0]) lst = [] split_path(lst, dirname) lst.reverse() mypath = os.path.abspath('/') for elem in lst: mypath = os.path.join(mypath, elem) if not os.path.exists(mypath): try: os.mkdir(mypath) except OSError as e: if e.args[0] != errno.EEXIST and e.args[0] != errno.EISDIR: raise def install_tar(f, prefix): meta = {} for info in f: #print(info) fname = info.name try: fname = fname[fname.index("/") + 1:] except ValueError: fname = "" save = True for p in ("setup.", "PKG-INFO", "README"): #print(fname, p) if fname.startswith(p) or ".egg-info" in fname: if fname.endswith("/requires.txt"): meta["deps"] = f.extractfile(info).read() save = False if debug: print("Skipping", fname) break if save: outfname = prefix + fname if info.type != tarfile.DIRTYPE: if debug: print("Extracting " + outfname) _makedirs(outfname) subf = f.extractfile(info) with open(outfname, "wb") as outf: outf.write(subf.read()) return meta warn_ussl = True def url_open(url): global warn_ussl if debug: print(url) proto, _, host, urlpath = url.split('/', 3) try: ai = usocket.getaddrinfo(host, 443) except OSError as e: print("Unable to resolve %s (no Internet?)" % host) raise addr = ai[0][4] s = usocket.socket(ai[0][0]) try: if proto == "https:": s = ussl.wrap_socket(s) if warn_ussl: print("Warning: %s SSL certificate is not validated" % host) warn_ussl = False s.connect(addr) s.setblocking(True) s.send(("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (urlpath, host)).encode('UTF8')) l = read_line(s) protover, status, msg = l.split(None, 2) if status != b"200": if status == b"404" or status == b"301": raise NotFoundError("Package not found") raise ValueError(status) while 1: l = read_line(s) if not l: raise ValueError("Unexpected EOF in HTTP headers") if l == b'\r\n': break except Exception as e: s.close() raise e return s # Now searches official library first before looking on PyPi for user packages def get_pkg_metadata(name): try: f = url_open("https://micropython.org/pi/%s/json" % name) except: f = url_open("https://pypi.org/pypi/%s/json" % name) s = read_lines(f) try: return json.loads(s.decode('UTF8')) finally: f.close() def fatal(msg): print("Error:", msg) sys.exit(1) def install_pkg(pkg_spec, install_path): data = get_pkg_metadata(pkg_spec) latest_ver = data["info"]["version"] packages = data["releases"][latest_ver] assert len(packages) == 1 package_url = packages[0]["url"] print("Installing %s %s from %s" % (pkg_spec, latest_ver, package_url)) f1 = url_open(package_url) s = read_lines(f1) try: str1 = zlib.decompress(s, gzdict_sz) with tempfile.TemporaryFile() as temp_file: temp_file.write(str1) temp_file.seek(0) with tarfile.TarFile(fileobj=temp_file) as tar_file: # Expects a file object meta = install_tar(tar_file, install_path) finally: f1.close() return meta def install(to_install): install_path = g_install_path install_path = os.path.join(install_path, '') # Append final / if not isinstance(to_install, list): to_install = [to_install] print("Installing to: " + install_path) # sets would be perfect here, but don't depend on them installed = [] try: while to_install: if debug: print("Queue:", to_install) pkg_spec = to_install.pop(0) if pkg_spec in installed: continue meta = install_pkg(pkg_spec, install_path) installed.append(pkg_spec) if debug: print(meta) deps = meta.get("deps", "").rstrip() if deps: deps = deps.decode("utf-8").split("\n") to_install.extend(deps) except Exception as e: print("Error installing '{}': {}, packages may be partially installed".format( pkg_spec, e), file=sys.stderr) def help_msg(): print("""\ micropip - Simple PyPI package manager for MicroPython Runs on a PC under Python 3.2 or above, and installs to a PC directory for subsequent transfer to target hardware. Usage: micropip.py install [-p ] ... | -r The above requires micropip.py to have executable permission. Alternatively: python3 -m micropip install [-p ] ... | -r If is not given, packages will be installed into the current directory. Note: only MicroPython packages (usually, named micropython-*) are supported for installation, upip does not support arbitrary code in setup.py. """) def main(): global debug global g_install_path if len(sys.argv) < 2 or sys.argv[1] == "-h" or sys.argv[1] == "--help": help_msg() return if sys.argv[1] != "install": fatal("Only 'install' command supported") to_install = [] i = 2 while i < len(sys.argv) and sys.argv[i][0] == "-": opt = sys.argv[i] i += 1 if opt == "-h" or opt == "--help": help_msg() return elif opt == "-p": g_install_path = sys.argv[i] i += 1 elif opt == "-r": list_file = sys.argv[i] i += 1 with open(list_file) as f: while True: l = f.readline() if not l: break if l[0] == "#": continue to_install.append(l.rstrip()) elif opt == "--debug": debug = True else: fatal("Unknown/unsupported option: " + opt) to_install.extend(sys.argv[i:]) if not to_install: help_msg() return g_install_path = os.path.expanduser(g_install_path) g_install_path = os.path.abspath(g_install_path) install(to_install) if __name__ == "__main__": main()