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

Skip to content

Commit 08df7bf

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. Also adds support for optional --args to multi-arg commands like "edit" and the filesystem commands (and now "install"). This allows `--mpy`/`--no-mpy` and `--target` to be specified to the `install` command. This work was funded through GitHub Sponsors. Signed-off-by: Jim Mussared <jim.mussared@gmail.com>
1 parent 65ab0ec commit 08df7bf

File tree

5 files changed

+248
-8
lines changed

5 files changed

+248
-8
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ 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)
31+
options:
32+
--target <path>
33+
--no-mpy
3034
mpremote help -- print list of commands and exit
3135

3236
Multiple commands can be specified and they will be run sequentially. Connection
@@ -73,3 +77,4 @@ Examples:
7377
mpremote cp :main.py .
7478
mpremote cp main.py :
7579
mpremote cp -r dir/ :
80+
mpremote install aioble

tools/mpremote/mpremote/main.py

Lines changed: 74 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"exec": (True, True, 1, "execute the string"),
7171
"run": (True, True, 1, "run the given local script"),
7272
"fs": (True, True, 1, "execute filesystem commands on the device"),
73+
"install": (True, True, 1, "install package"),
7374
"help": (False, False, 0, "print help and exit"),
7475
"version": (False, False, 0, "print version and exit"),
7576
}
@@ -306,15 +307,63 @@ def show_progress_bar(size, total_size):
306307

307308
# Get all args up to the terminator ("+").
308309
# The passed args will be updated with these ones removed.
309-
def get_fs_args(args):
310+
# Optional kwargs can be specified and any leading --args will be parsed.
311+
def get_args_to_terminator(args, **kwargs):
312+
# Process any leading --args.
310313
n = 0
311-
for src in args:
312-
if src == "+":
314+
while n < len(args):
315+
arg = args[n]
316+
if not arg.startswith("--"):
317+
# Stop at the first arg that doesn't start with --
313318
break
319+
arg = arg[2:]
320+
321+
# Handle --no-foo --> --foo=0
322+
if arg.startswith("no-"):
323+
arg = arg[3:] + "=0"
324+
325+
# Handle both `--foo bar` and `--foo=bar`.
326+
arg = arg.split("=")
327+
if arg[0] not in kwargs:
328+
print("Invalid argument '--{}'".format(arg[0]))
329+
sys.exit(1)
330+
default = kwargs[arg[0]]
331+
if len(arg) == 1:
332+
if isinstance(default, bool):
333+
# --foo --> --foo=1 (only if this is a bool arg).
334+
arg.append("1")
335+
elif n + 1 < len(args):
336+
# `--foo bar` --> `--foo=bar`.
337+
n += 1
338+
arg.append(args[n])
339+
else:
340+
# Final argument was `--foo`.
341+
print("Missing value for argument '--{}'".format(arg[0]))
342+
sys.exit(1)
343+
344+
# Parse bool and integer arguments.
345+
if isinstance(default, bool):
346+
arg[1] = arg[1].lower() in ("1", "true", "y",)
347+
if isinstance(default, int):
348+
arg[1] = int(arg[1])
349+
350+
# Assign the argument value to the result dict.
351+
kwargs[arg[0]] = arg[1]
352+
314353
n += 1
315-
fs_args = args[:n]
316-
args[:] = args[n + 1 :]
317-
return fs_args
354+
355+
# Process remaining arguments.
356+
args[:] = args[n:]
357+
358+
# Find a "+" (terminator).
359+
try:
360+
n = args.index("+")
361+
cmd_args = args[:n]
362+
args[:] = args[n + 1 :]
363+
return cmd_args, kwargs
364+
except ValueError:
365+
# Use all remaining args.
366+
return args, kwargs
318367

319368

320369
def do_filesystem(pyb, args):
@@ -325,7 +374,7 @@ def _list_recursive(files, path):
325374
else:
326375
files.append(os.path.split(path))
327376

328-
fs_args = get_fs_args(args)
377+
fs_args, _ = get_args_to_terminator(args)
329378

330379
# Don't be verbose when using cat, so output can be redirected to something.
331380
verbose = fs_args[0] != "cat"
@@ -371,7 +420,8 @@ def _list_recursive(files, path):
371420
def do_edit(pyb, args):
372421
if not os.getenv("EDITOR"):
373422
raise pyboard.PyboardError("edit: $EDITOR not set")
374-
for src in get_fs_args(args):
423+
fs_args, _ = get_args_to_terminator(args)
424+
for src in fs_args:
375425
src = src.lstrip(":")
376426
dest_fd, dest = tempfile.mkstemp(suffix=os.path.basename(src))
377427
try:
@@ -480,6 +530,20 @@ def console_out_write(b):
480530
capture_file.close()
481531

482532

533+
def do_install(pyb, args):
534+
packages, opts = get_args_to_terminator(args, target=None, mpy=True)
535+
536+
for package in packages:
537+
version = None
538+
if "@" in package:
539+
package, version = package.split("@")
540+
541+
print("install", package)
542+
543+
from . import mip
544+
mip.install(pyb, package, version=version, target=opts["target"], mpy=opts["mpy"])
545+
546+
483547
def execbuffer(pyb, buf, follow):
484548
ret_val = 0
485549
try:
@@ -621,6 +685,8 @@ def main():
621685
do_edit(pyb, args)
622686
elif cmd == "repl":
623687
do_repl(pyb, args)
688+
elif cmd == "install":
689+
do_install(pyb, args)
624690

625691
if not did_action:
626692
if pyb is None:

tools/mpremote/mpremote/mip.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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+
< 1241 span class=pl-k>def _rewrite_url(url, branch=None):
35+
if not branch:
36+
branch = "HEAD"
37+
if url.startswith("githu F438 b:"):
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))
10000
122+
123+
mpy_version = "py"
124+
if mpy:
125+
pyb.exec("import sys")
126+
mpy_version = eval(pyb.eval("getattr(sys.implementation, '_mpy', 0) & 0xFF").decode()) or "py"
127+
128+
package = "{}/package/{}/{}/{}.json".format(index, mpy_version, package, version)
129+
130+
return _install_json(pyb, package, index, target, version, mpy)
131+
132+
133+
def install(pyb, package, index=_PACKAGE_INDEX, target=None, version=None, mpy=True):
134+
if not target:
135+
pyb.exec("import sys")
136+
sys_path = eval(pyb.eval("sys.path").decode())
137+
for p in sys_path:
138+
if p.endswith("/lib"):
139+
target = p
140+
break
141+
else:
142+
print("Unable to find lib dir in sys.path")
143+
return
144+
145+
if _install_package(pyb, package, index.rstrip("/"), target, version, mpy):
146+
print("Done")
147+
else:
148+
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