8000 micropython/upip: Add a new upip library. · micropython/micropython-lib@b086eaf · GitHub
[go: up one dir, main page]

Skip to content

Commit b086eaf

Browse files
committed
micropython/upip: Add a new upip library.
This is a replacement for the previous `upip` tool for on-device installation of packages. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent a294aeb commit b086eaf

File tree

2 files changed

+167
-0
lines changed

2 files changed

+167
-0
lines changed

micropython/upip/manifest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
metadata(version="0.1.0")
2+
3+
require("urequests")
4+
5+
module("upip.py")

micropython/upip/upip.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Micropython package installer
2+
# MIT license; Copyright (c) 2022 Jim Mussared
3+
4+
import urequests as requests
5+
import sys
6+
7+
8+
_PACKAGE_INDEX = const("https://micropython.org/pi/v2")
9+
_CHUNK_SIZE = 128
10+
11+
12+
# This implements os.makedirs(os.dirname(path))
13+
def _ensure_path_exists(path):
14+
import os
15+
16+
split = path.split("/")
17+
18+
# Handle paths starting with "/".
19+
if not split[0]:
20+
split.pop(0)
21+
split[0] = "/" + split[0]
22+
23+
prefix = ""
24+
for i in range(len(split) - 1):
25+
prefix += split[i]
26+
try:
27+
os.stat(prefix)
28+
except:
29+
os.mkdir(prefix)
30+
prefix += "/"
31+
32+
33+
# Copy from src (stream) to dest (function-taking-bytes)
34+
def _chunk(src, dest):
35+
buf = memoryview(bytearray(_CHUNK_SIZE))
36+
while True:
37+
n = src.readinto(buf)
38+
if n == 0:
39+
break
40+
dest(buf if n == _CHUNK_SIZE else buf[:n])
41+
42+
43+
# Check if the specified path exists and matches the hash.
44+
def _check_exists(path, short_hash):
45+
import os
46+
47+
try:
48+
import binascii
49+
import hashlib
50+
51+
with open(path, "rb") as f:
52+
hs256 = hashlib.sha256()
53+
_chunk(f, hs256.update)
54+
existing_hash = str(binascii.hexlify(hs256.digest())[: len(short_hash)], "utf-8")
55+
return existing_hash == short_hash
56+
except:
57+
return False
58+
59+
60+
def download_file(url, dest):
61+
if url.startswith("github:"):
62+
url = url[7:].split("/")
63+
url = (
64+
"https://raw.githubusercontent.com/"
65+
+ url[0]
66+
+ "/"
67+
+ url[1]
68+
+ "/HEAD/"
69+
+ "/".join(url[2:])
70+
)
71+
72+
response = requests.get(url)
73+
try:
74+
if response.status_code != 200:
75+
print("Error", response.status_code, "requesting", url)
76+
return False
77+
78+
print("Copying:", dest)
79+
_ensure_path_exists(dest)
80+
with open(dest, "wb") as f:
81+
_chunk(response.raw, f.write)
82+
83+
return True
84+
finally:
85+
response.close()
86+
87+
88+
def install_json(package_json_url, index, target):
89+
response = requests.get(package_json_url)
90+
try:
91+
if response.status_code != 200:
92+
print("Package not found")
93+
return False
94+
95+
package_json = response.json()
96+
finally:
97+
response.close()
98+
for target_path, short_hash in package_json.get("hashes", ()):
99+
fs_target_path = target + "/" + target_path
100+
if _check_exists(fs_target_path, short_hash):
101+
print("Exists:", fs_target_path)
102+
else:
103+
file_url = "{}/file/{}/{}".format(index, short_hash[:2], short_hash[2:])
104+
if not download_file(file_url, fs_target_path):
105+
print("File not found: {} {}".format(target_path, short_hash))
106+
return False
107+
for target_path, url in package_json.get("urls", ()):
108+
fs_target_path = target + "/" + target_path
109+
if not download_file(url, fs_target_path):
110+
print("File not found: {} {}".format(target_path, url))
111+
return False
112+
for dep, version in package_json.get("deps", ()):
113+
if not install_package(dep, index, target, version):
114+
return False
115+
return True
116+
117+
118+
def install_lib(package, index, target, version):
119+
print("Installing {} from {} to {}".format(package, index, target))
120+
121+
mpy_version = sys.implementation._mpy & 0xFF
122+
123+
package_json_url = "{}/package/{}/{}/{}.json".format(
124+
index.rstrip("/"), mpy_version, package, version
125+
)
126+
return install_package(package_json_url, index, target, version)
127+
128+
129+
def install_package(package, index, target, version):
130+
if (
131+
package.startswith("http://")
132+
or package.startswith("https://")
133+
or package.startswith("github:")
134+
):
135+
if package.endswith(".py") or package.endswith(".mpy"):
136+
print("Downloading {} to {}".format(package, target))
137+
return download_file(package, target + "/" + package.rsplit("/")[-1])
138+
else:
139+
if not package.endswith(".json"):
140+
if not package.endswith("/"):
141+
package += "/"
142+
package += "package.json"
143+
print("Installing {} to {}".format(package, target))
144+
return install_json(package, index, target)
145+
else:
146+
return install_lib(package, index, target, A56D version)
147+
148+
149+
def install(package, index=_PACKAGE_INDEX, target=None, version="latest"):
150+
if not target:
151+
for p in sys.path:
152+
if p == "lib" or p.endswith("/lib"):
153+
target = p
154+
break
155+
else:
156+
print("Unable to find lib dir in sys.path.")
157+
return
158+
159+
if install_package(package, index, target, version):
160+
print("Done")
161+
else:
162+
print("Package may be partially installed.")

0 commit comments

Comments
 (0)
0