8000 tools/mpremote: Add `mpremote install` to install packages. · micropython/micropython@f5b5cfc · GitHub
[go: up one dir, main page]

Skip to content

Commit f5b5cfc

Browse files
committed
tools/mpremote: Add mpremote install to install packages.
This supports the same package sources as the new `mip` tool. - micropython-lib (by name) - http(s) & github packages with json description - directly downloading a .py/.mpy file The version is specified with an optional `@version` on the end of the package name. The target dir, index, and mpy/no-mpy can be set through command line args. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent 291828a commit f5b5cfc

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

docs/reference/mpremote.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,14 @@ The full list of supported commands are:
146146
variable ``$EDITOR``). If the editor exits successfully, the updated file will
147147
be copied back to the device.
148148

149+
- install packages from :term:`micropython-lib` (or GitHub):
150+
151+
.. code-block:: bash
152+
153+
$ mpremote install <packages...>
154+
155+
See :ref:`packages` for more information.
156+
149157
- mount the local directory on the remote device:
150158

151159
.. code-block:: bash
@@ -269,3 +277,9 @@ Examples
269277
mpremote cp -r dir/ :
270278
271279
mpremote cp a.py b.py : + repl
280+
281+
mpremote install aioble
282+
283+
mpremote install github:org/repo@branch
284+
285+
mpremote install --target /flash/third-party functools

tools/mpremote/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ The full list of supported commands are:
2727
--capture <file>
2828
--inject-code <string>
2929
--inject-file <file>
30+
mpremote install <package...> -- Install packages (from micropython-lib or third-party sources)
31+
options:
32+
--target <path>
33+
--index <url>
34+
--no-mpy
3035
mpremote help -- print list of commands and exit
3136

3237
Multiple commands can be specified and they will be run sequentially. Connection
@@ -73,3 +78,5 @@ Examples:
7378
mpremote cp :main.py .
7479
mpremote cp main.py :
7580
mpremote cp -r dir/ :
81+
mpremote install aioble
82+
mpremote install github:org/repo@branch

tools/mpremote/mpremote/commands.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,27 @@ def do_resume(state, _args=None):
237237

238238
def do_soft_reset(state, _args=None):
239239
state.ensure_raw_repl(soft_reset=True)
240+
241+
242+
def do_install(state, args):
243+
state.ensure_raw_repl()
244+
state.did_action()
245+
246+
for package in args.packages:
247+
version = None
248+
if "@" in package:
249+
package, version = package.split("@")
250+
251+
print("install", package)
252+
253+
from . import mip
254+
255+
kwargs = {}
256+
if args.index is not None:
257+
kwargs["index"] = args.index
258+
if args.target is not None:
259+
kwargs["target"] = args.target
260+
if args.mpy is not None:
261+
kwargs["mpy"] = args.mpy
262+
263+
mip.install(state.pyb, package, version=version, **kwargs)

tools/mpremote/mpremote/main.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
do_run,
3636
do_resume,
3737
do_soft_reset,
38+
do_install,
3839
)
3940
from .repl import do_repl
4041

@@ -144,6 +145,14 @@ def argparse_filesystem():
144145
return cmd_parser
145146

146147

148+
def argparse_install():
149+
cmd_parser = argparse.ArgumentParser(description="install packages from micropython-lib or third-party sources")
150+
_bool_flag(cmd_parser, "mpy", "m", True, "download as compiled .mpy files")
151+
cmd_parser.add_argument("--target", type=str, required=False, help="destination direction on the device")
152+
cmd_parser.add_argument("--index", type=str, required=False, help="package index to use (defaults to micropython-lib)")
153+
cmd_parser.add_argument("packages", nargs="+", help="list package specifications, e.g. name, name@version, github:org/repo, github:org/repo@branch")
154+
return cmd_parser
155+
147156
def argparse_none(description):
148157
return lambda: argparse.ArgumentParser(description=description)
149158

@@ -198,6 +207,10 @@ def argparse_none(description):
198207
do_filesystem,
199208
argparse_filesystem,
200209
),
210+
"install": (
211+
do_install,
212+
argparse_install,
213+
),
201214
"help": (
202215
do_help,
203216
argparse_none("print help and exit"),

tools/mpremote/mpremote/mip.py

Lines changed: 150 additions & 0 deletions
72A8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Micropython package installer
2+
# Ported from micropython-lib/micropython/mip/mip.py.
3+
# MIT license; Copyright (c) 2022 Jim Mussared
4+
5+
import urllib.error
6+
import urllib.request
7+
import json
8+
import tempfile
9+
import os
10+
11+
12+
_PACKAGE_INDEX = "https://micropython.org/pi/v2"
13+
14+
15+
# This implements os.makedirs(os.dirname(path))
16+
def _ensure_path_exists(pyb, path):
17+
import os
18+
19+
split = path.split("/")
20+
21+
# Handle paths starting with "/".
22+
if not split[0]:
23+
split.pop(0)
24+
split[0] = "/" + split[0]
25+
26+
prefix = ""
27+
for i in range(len(split) - 1):
28+
prefix += split[i]
29+
if not pyb.fs_exists(prefix):
30+
pyb.fs_mkdir(prefix)
31+
prefix += "/"
32+
33+
34+
def _rewrite_url(url, branch=None):
35+
if not branch:
36+
branch = "HEAD"
37+
if url.startswith("github:"):
38+
url = url[7:].split("/")
39+
url = (
40+
"https://raw.githubusercontent.com/"
41+
+ url[0]
42+
+ "/"
43+
+ url[1]
44+
+ "/"
45+
+ branch
46+
+ "/"
47+
+ "/".join(url[2:])
48+
)
49+
return url
50+
51+
52+
def _download_file(pyb, url, dest):
53+
try:
54+
with urllib.request.urlopen(url) as src:
55+
fd, path = tempfile.mkstemp()
56+
try:
57+
with os.fdopen(fd, "wb") as f:
58+
f.write(src.read())
59+
print("Copying:", dest)
60+
_ensure_path_exists(pyb, dest)
61+
pyb.fs_put(path, dest)
62+
finally:
63+
os.unlink(path)
64+
except urllib.error.HTTPError as e:
65+
print("Error", e.status, "requesting", url)
66+
return False
67+
68+
return True
69+
70+
71+
def _install_json(pyb, package_json_url, index, target, version, mpy):
72+
try:
73+
with urllib.request.urlopen(_rewrite_url(package_json_url, version)) as response:
74+
package_json = json.load(response)
75+
except urllib.error.HTTPError as e:
76+
if e.status == 404:
77+
print("Package not found:", package_json_url)
78+
else:
79+
print("Error", e.status, "requesting", package_json_url)
80+
return False
81+
except urllib.error.URLError as e:
82+
print(e.reason, "requesting", package_json_url)
83+
return False
84+
for target_path, short_hash in package_json.get("hashes", ()):
85+
fs_target_path = target + "/" + target_path
86+
file_url = "{}/file/{}/{}".format(index, short_hash[:2], short_hash)
87+
if not _download_file(pyb, file_url, fs_target_path):
88+
print("File not found: {} {}".format(target_path, short_hash))
89+
return False
90+
for target_path, url in package_json.get("urls", ()):
91+
fs_target_path = target + "/" + target_path
92+
if not _download_file(pyb, _rewrite_url(url, version), fs_target_path):
93+
print("File not found: {} {}".format(target_path, url))
94+
return False
95+
for dep, dep_version in package_json.get("deps", ()):
96+
if not _install_package(pyb, dep, index, target, dep_version, mpy):
97+
return False
98+
return True
99+
100+
101+
def _install_package(pyb, package, index, target, version, mpy):
102+
if (
103+
package.startswith("http://")
104+
or package.startswith("https://")
105+
or package.startswith("github:")
106+
):
107+
if package.endswith(".py") or package.endswith(".mpy"):
108+
print("Downloading {} to {}".format(package, target))
109+
return _download_file(
110+
pyb, _rewrite_url(package, version), target + "/" + package.rsplit("/")[-1]
111+
)
112+
else:
113+
if not package.endswith(".json"):
114+
if not package.endswith("/"):
115+
package += "/"
116+
package += "package.json"
117+
print("Installing {} to {}".format(package, target))
118+
else:
119+
if not version:
120+
version = "latest"
121+
print("Installing {} ({}) from {} to {}".format(package, version, index, target))
122+
123+
mpy_version = "py"
124+
if mpy:
125+
pyb.exec("import sys")
126+
mpy_version = (
127+
eval(pyb.eval("getattr(sys.implementation, '_mpy', 0) & 0xFF").decode()) or "py"
128+
)
129+
130+
package = "{}/package/{}/{}/{}.json".format(index, mpy_version, package, version)
131+
132+
return _install_json(pyb, package, index, target, version, mpy)
133+
134+
135+
def install(pyb, package, index=_PACKAGE_INDEX, target=None, version=None, mpy=True):
136+
if not target:
137+
pyb.exec("import sys")
138+
sys_path = eval(pyb.eval("sys.path").decode())
139+
for p in sys_path:
140+
if p.endswith("/lib"):
141+
target = p
142+
break
143+
else:
144+
print("Unable to find lib dir in sys.path")
145+
return
146+
147+
if _install_package(pyb, package, index.rstrip("/"), target, version, mpy):
148+
print("Done")
149+
else:
150+
print("Package may be partially installed")

tools/pyboard.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,13 @@ def get_time(self):
476476
t = str(self.eval("pyb.RTC().datetime()"), encoding="utf8")[1:-1].split(", ")
477477
return int(t[4]) * 3600 + int(t[5]) * 60 + int(t[6])
478478

479+
def fs_exists(self, src):
480+
try:
481+
self.exec_("import uos\nuos.stat(%s)" % (("'%s'" % src) if src else ""))
482+
return True
483+
except PyboardError:
484+
return False
485+
479486
def fs_ls(self, src):
480487
cmd = (
481488
"import uos\nfor f in uos.ilistdir(%s):\n"

0 commit comments

Comments
 (0)
0