upip.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. #
  2. # upip - Package manager for MicroPython
  3. #
  4. # Copyright (c) 2015-2018 Paul Sokolovsky
  5. #
  6. # Licensed under the MIT license.
  7. #
  8. import sys
  9. import gc
  10. import uos as os
  11. import uerrno as errno
  12. import ujson as json
  13. import uzlib
  14. import upip_utarfile as tarfile
  15. gc.collect()
  16. debug = False
  17. install_path = None
  18. cleanup_files = []
  19. gzdict_sz = 16 + 15
  20. file_buf = bytearray(512)
  21. class NotFoundError(Exception):
  22. pass
  23. def op_split(path):
  24. if path == "":
  25. return ("", "")
  26. r = path.rsplit("/", 1)
  27. if len(r) == 1:
  28. return ("", path)
  29. head = r[0]
  30. if not head:
  31. head = "/"
  32. return (head, r[1])
  33. def op_basename(path):
  34. return op_split(path)[1]
  35. # Expects *file* name
  36. def _makedirs(name, mode=0o777):
  37. ret = False
  38. s = ""
  39. comps = name.rstrip("/").split("/")[:-1]
  40. if comps[0] == "":
  41. s = "/"
  42. for c in comps:
  43. if s and s[-1] != "/":
  44. s += "/"
  45. s += c
  46. try:
  47. os.mkdir(s)
  48. ret = True
  49. except OSError as e:
  50. if e.args[0] != errno.EEXIST and e.args[0] != errno.EISDIR:
  51. raise
  52. ret = False
  53. return ret
  54. def save_file(fname, subf):
  55. global file_buf
  56. with open(fname, "wb") as outf:
  57. while True:
  58. sz = subf.readinto(file_buf)
  59. if not sz:
  60. break
  61. outf.write(file_buf, sz)
  62. def install_tar(f, prefix):
  63. meta = {}
  64. for info in f:
  65. #print(info)
  66. fname = info.name
  67. try:
  68. fname = fname[fname.index("/") + 1:]
  69. except ValueError:
  70. fname = ""
  71. save = True
  72. for p in ("setup.", "PKG-INFO", "README"):
  73. #print(fname, p)
  74. if fname.startswith(p) or ".egg-info" in fname:
  75. if fname.endswith("/requires.txt"):
  76. meta["deps"] = f.extractfile(info).read()
  77. save = False
  78. if debug:
  79. print("Skipping", fname)
  80. break
  81. if save:
  82. outfname = prefix + fname
  83. if info.type != tarfile.DIRTYPE:
  84. if debug:
  85. print("Extracting " + outfname)
  86. _makedirs(outfname)
  87. subf = f.extractfile(info)
  88. save_file(outfname, subf)
  89. return meta
  90. def expandhome(s):
  91. if "~/" in s:
  92. h = os.getenv("HOME")
  93. s = s.replace("~/", h + "/")
  94. return s
  95. import ussl
  96. import usocket
  97. warn_ussl = True
  98. def url_open(url):
  99. global warn_ussl
  100. if debug:
  101. print(url)
  102. proto, _, host, urlpath = url.split('/', 3)
  103. try:
  104. ai = usocket.getaddrinfo(host, 443, 0, usocket.SOCK_STREAM)
  105. except OSError as e:
  106. fatal("Unable to resolve %s (no Internet?)" % host, e)
  107. #print("Address infos:", ai)
  108. ai = ai[0]
  109. s = usocket.socket(ai[0], ai[1], ai[2])
  110. try:
  111. #print("Connect address:", addr)
  112. s.connect(ai[-1])
  113. if proto == "https:":
  114. s = ussl.wrap_socket(s, server_hostname=host)
  115. if warn_ussl:
  116. print("Warning: %s SSL certificate is not validated" % host)
  117. warn_ussl = False
  118. # MicroPython rawsocket module supports file interface directly
  119. s.write("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (urlpath, host))
  120. l = s.readline()
  121. protover, status, msg = l.split(None, 2)
  122. if status != b"200":
  123. if status == b"404" or status == b"301":
  124. raise NotFoundError("Package not found")
  125. raise ValueError(status)
  126. while 1:
  127. l = s.readline()
  128. if not l:
  129. raise ValueError("Unexpected EOF in HTTP headers")
  130. if l == b'\r\n':
  131. break
  132. except Exception as e:
  133. s.close()
  134. raise e
  135. return s
  136. def get_pkg_metadata(name):
  137. f = url_open("https://pypi.org/pypi/%s/json" % name)
  138. try:
  139. return json.load(f)
  140. finally:
  141. f.close()
  142. def fatal(msg, exc=None):
  143. print("Error:", msg)
  144. if exc and debug:
  145. raise exc
  146. sys.exit(1)
  147. def install_pkg(pkg_spec, install_path):
  148. data = get_pkg_metadata(pkg_spec)
  149. latest_ver = data["info"]["version"]
  150. packages = data["releases"][latest_ver]
  151. del data
  152. gc.collect()
  153. assert len(packages) == 1
  154. package_url = packages[0]["url"]
  155. print("Installing %s %s from %s" % (pkg_spec, latest_ver, package_url))
  156. package_fname = op_basename(package_url)
  157. f1 = url_open(package_url)
  158. try:
  159. f2 = uzlib.DecompIO(f1, gzdict_sz)
  160. f3 = tarfile.TarFile(fileobj=f2)
  161. meta = install_tar(f3, install_path)
  162. finally:
  163. f1.close()
  164. del f3
  165. del f2
  166. gc.collect()
  167. return meta
  168. def install(to_install, install_path=None):
  169. # Calculate gzip dictionary size to use
  170. global gzdict_sz
  171. sz = gc.mem_free() + gc.mem_alloc()
  172. if sz <= 65536:
  173. gzdict_sz = 16 + 12
  174. if install_path is None:
  175. install_path = get_install_path()
  176. if install_path[-1] != "/":
  177. install_path += "/"
  178. if not isinstance(to_install, list):
  179. to_install = [to_install]
  180. print("Installing to: " + install_path)
  181. # sets would be perfect here, but don't depend on them
  182. installed = []
  183. try:
  184. while to_install:
  185. if debug:
  186. print("Queue:", to_install)
  187. pkg_spec = to_install.pop(0)
  188. if pkg_spec in installed:
  189. continue
  190. meta = install_pkg(pkg_spec, install_path)
  191. installed.append(pkg_spec)
  192. if debug:
  193. print(meta)
  194. deps = meta.get("deps", "").rstrip()
  195. if deps:
  196. deps = deps.decode("utf-8").split("\n")
  197. to_install.extend(deps)
  198. except Exception as e:
  199. print("Error installing '{}': {}, packages may be partially installed".format(
  200. pkg_spec, e),
  201. file=sys.stderr)
  202. def get_install_path():
  203. global install_path
  204. if install_path is None:
  205. # sys.path[0] is current module's path
  206. install_path = sys.path[1]
  207. install_path = expandhome(install_path)
  208. return install_path
  209. def cleanup():
  210. for fname in cleanup_files:
  211. try:
  212. os.unlink(fname)
  213. except OSError:
  214. print("Warning: Cannot delete " + fname)
  215. def help():
  216. print("""\
  217. upip - Simple PyPI package manager for MicroPython
  218. Usage: micropython -m upip install [-p <path>] <package>... | -r <requirements.txt>
  219. import upip; upip.install(package_or_list, [<path>])
  220. If <path> is not given, packages will be installed into sys.path[1]
  221. (can be set from MICROPYPATH environment variable, if current system
  222. supports that).""")
  223. print("Current value of sys.path[1]:", sys.path[1])
  224. print("""\
  225. Note: only MicroPython packages (usually, named micropython-*) are supported
  226. for installation, upip does not support arbitrary code in setup.py.
  227. """)
  228. def main():
  229. global debug
  230. global install_path
  231. install_path = None
  232. if len(sys.argv) < 2 or sys.argv[1] == "-h" or sys.argv[1] == "--help":
  233. help()
  234. return
  235. if sys.argv[1] != "install":
  236. fatal("Only 'install' command supported")
  237. to_install = []
  238. i = 2
  239. while i < len(sys.argv) and sys.argv[i][0] == "-":
  240. opt = sys.argv[i]
  241. i += 1
  242. if opt == "-h" or opt == "--help":
  243. help()
  244. return
  245. elif opt == "-p":
  246. install_path = sys.argv[i]
  247. i += 1
  248. elif opt == "-r":
  249. list_file = sys.argv[i]
  250. i += 1
  251. with open(list_file) as f:
  252. while True:
  253. l = f.readline()
  254. if not l:
  255. break
  256. if l[0] == "#":
  257. continue
  258. to_install.append(l.rstrip())
  259. elif opt == "--debug":
  260. debug = True
  261. else:
  262. fatal("Unknown/unsupported option: " + opt)
  263. to_install.extend(sys.argv[i:])
  264. if not to_install:
  265. help()
  266. return
  267. install(to_install)
  268. if not debug:
  269. cleanup()
  270. if __name__ == "__main__":
  271. main()