Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 5ae5fdd

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 76277c3 commit 5ae5fdd

File tree

2 files changed

+175
-0
lines changed

2 files changed

+175
-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", description="On-device package installer for network-capable boards")
2+
3+
require("urequests")
4+
5+
module("upip.py")

‎micropython/upip/upip.py‎

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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 _rewrite_url(url, branch=None):
61+
if not branch:
62+
branch = "HEAD"
63+
if url.startswith("github:"):
64+
url = url[7:].split("/")
65+
url = (
66+
"https://raw.githubusercontent.com/"
67+
+ url[0]
68+
+ "/"
69+
+ url[1]
70+
+ "/"
71+
+ branch
72+
+ "/"
73+
+ "/".join(url[2:])
74+
)
75+
return url
76+
77+
78+
def _download_file(url, dest):
79+
response = requests.get(url)
80+
try:
81+
if response.status_code != 200:
82+
print("Error", response.status_code, "requesting", url)
83+
return False
84+
85+
print("Copying:", dest)
86+
_ensure_path_exists(dest)
87+
with open(dest, "wb") as f:
88+
_chunk(response.raw, f.write)
89+
90+
return True
91+
finally:
92+
response.close()
93+
94+
95+
def _install_json(package_json_url, index, target, version, mpy):
96+
response = requests.get(_rewrite_url(package_json_url, version))
97+
try:
98+
if response.status_code != 200:
99+
print("Package not found:", package_json_url)
100+
return False
101+
102+
package_json = response.json()
103+
finally:
104+
response.close()
105+
for target_path, short_hash in package_json.get("hashes", ()):
106+
fs_target_path = target + "/" + target_path
107+
if _check_exists(fs_target_path, short_hash):
108+
print("Exists:", fs_target_path)
109+
else:
110+
file_url = "{}/file/{}/{}".format(index, short_hash[:2], short_hash)
111+
if not _download_file(file_url, fs_target_path):
112+
print("File not found: {} {}".format(target_path, short_hash))
113+
return False
114+
for target_path, url in package_json.get("urls", ()):
115+
fs_target_path = target + "/" + target_path
116+
if not _download_file(_rewrite_url(url, version), fs_target_path):
117+
print("File not found: {} {}".format(target_path, url))
118+
return False
119+
for dep, dep_version in package_json.get("deps", ()):
120+
if not _install_package(dep, index, target, dep_version, mpy):
121+
return False
122+
return True
123+
124+
125+
def _install_package(package, index, target, version, mpy):
126+
if (
127+
package.startswith("http://")
128+
or package.startswith("https://")
129+
or package.startswith("github:")
130+
):
131+
if package.endswith(".py") or package.endswith(".mpy"):
132+
print("Downloading {} to {}".format(package, target))
133+
return _download_file(
134+
_rewrite_url(package, version), target + "/" + package.rsplit("/")[-1]
135+
)
136+
else:
137+
if not package.endswith(".json"):
138+
if not package.endswith("/"):
139+
package += "/"
140+
package += "package.json"
141+
print("Installing {} to {}".format(package, target))
142+
else:
143+
if not version:
144+
version = "latest"
145+
print("Installing {} ({}) from {} to {}".format(package, version, index, target))
146+
147+
mpy_version = (
148+
sys.implementation._mpy & 0xFF if mpy and hasattr(sys.implementation, "_mpy") else "py"
149+
)
150+
151+
package = "{}/package/{}/{}/{}.json".format(index, mpy_version, package, version)
152+
153+
return _install_json(package, index, target, version, mpy)
154+
155+
156+
157+
def install(package, index=_PACKAGE_INDEX, target=None, version=None, mpy=True):
158+
if not target:
159+
for p in sys.path:
160+
if p.endswith("/lib"):
161+
target = p
162+
break
163+
else:
164+
print("Unable to find lib dir in sys.path")
165+
return
166+
167+
if _install_package(package, index.rstrip("/"), target, version, mpy):
168+
print("Done")
169+
else:
170+
print("Package may be partially installed")

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /