diff options
| author | Glenn Moloney <glenn.moloney@gmail.com> | 2023-09-19 13:04:46 +1000 |
|---|---|---|
| committer | Damien George <damien@micropython.org> | 2025-02-24 13:29:40 +1100 |
| commit | 2992e3495630a8b83683db28241d638dbc53c911 (patch) | |
| tree | 0c4228a8651ed3d5f5c5af21c1ab2c13704f8932 | |
| parent | 4364d9411ad4c71bbf6de9bcc82683d6ee671cf5 (diff) | |
tools/mpremote: Add support for relative urls in package.json files.
URLs in `package.json` may now be specified relative to the base URL of the
`package.json` file.
Relative URLs wil work for `package.json` files installed from the web as
well as local file paths.
Docs: update `docs/reference/packages.rst` to add documentation for:
- Installing packages from local filesystems (PR #12476); and
- Using relative URLs in the `package.json` file (PR #12477);
- Update the packaging example to encourage relative URLs as the default
in `package.json`.
Add `tools/mpremote/tests/test_mip_local_install.sh` to test the
installation of a package from local files using relative URLs in the
`package.json`.
Signed-off-by: Glenn Moloney <glenn.moloney@gmail.com>
| -rw-r--r-- | docs/reference/packages.rst | 48 | ||||
| -rw-r--r-- | tools/mpremote/mpremote/mip.py | 42 | ||||
| -rwxr-xr-x | tools/mpremote/tests/test_mip_local_install.sh | 67 | ||||
| -rw-r--r-- | tools/mpremote/tests/test_mip_local_install.sh.exp | 11 |
4 files changed, 149 insertions, 19 deletions
diff --git a/docs/reference/packages.rst b/docs/reference/packages.rst index 86b8efaa1..5b5f626d4 100644 --- a/docs/reference/packages.rst +++ b/docs/reference/packages.rst @@ -96,6 +96,18 @@ The ``--target=path``, ``--no-mpy``, and ``--index`` arguments can be set:: $ mpremote mip install --no-mpy pkgname $ mpremote mip install --index https://host/pi pkgname +:term:`mpremote` can also install packages from files stored on the host's local +filesystem:: + + $ mpremote mip install path/to/pkg.py + $ mpremote mip install path/to/app/package.json + $ mpremote mip install \\path\\to\\pkg.py + +This is especially useful for testing packages during development and for +installing packages from local clones of GitHub repositories. Note that URLs in +``package.json`` files must use forward slashes ("/") as directory separators, +even on Windows, so that they are compatible with installing from the web. + Installing packages manually ---------------------------- @@ -116,12 +128,25 @@ To write a "self-hosted" package that can be downloaded by ``mip`` or ``mpremote``, you need a static webserver (or GitHub) to host either a single .py file, or a ``package.json`` file alongside your .py files. -A typical ``package.json`` for an example ``mlx90640`` library looks like:: +An example ``mlx90640`` library hosted on GitHub could be installed with:: + + $ mpremote mip install github:org/micropython-mlx90640 + +The layout for the package on GitHub might look like:: + + https://github.com/org/micropython-mlx90640/ + package.json + mlx90640/ + __init__.py + utils.py + +The ``package.json`` specifies the location of files to be installed and other +dependencies:: { "urls": [ - ["mlx90640/__init__.py", "github:org/micropython-mlx90640/mlx90640/__init__.py"], - ["mlx90640/utils.py", "github:org/micropython-mlx90640/mlx90640/utils.py"] + ["mlx90640/__init__.py", "mlx90640/__init__.py"], + ["mlx90640/utils.py", "mlx90640/utils.py"] ], "deps": [ ["collections-defaultdict", "latest"], @@ -132,9 +157,20 @@ A typical ``package.json`` for an example ``mlx90640`` library looks like:: "version": "0.2" } -This includes two files, hosted at a GitHub repo named -``org/micropython-mlx90640``, which install into the ``mlx90640`` directory on -the device. It depends on ``collections-defaultdict`` and ``os-path`` which will +The ``urls`` list specifies the files to be installed according to:: + + "urls": [ + [destination_path, source_url] + ... + +where ``destination_path`` is the location and name of the file to be installed +on the device and ``source_url`` is the URL of the file to be installed. The +source URL would usually be specified relative to the directory containing the +``package.json`` file, but can also be an absolute URL, eg:: + + ["mlx90640/utils.py", "github:org/micropython-mlx90640/mlx90640/utils.py"] + +The package depends on ``collections-defaultdict`` and ``os-path`` which will be installed automatically from the :term:`micropython-lib`. The third dependency installs the content as defined by the ``package.json`` file of the ``main`` branch of the GitHub repo ``org/micropython-additions``. diff --git a/tools/mpremote/mpremote/mip.py b/tools/mpremote/mpremote/mip.py index 4ce62b7f7..858b7933d 100644 --- a/tools/mpremote/mpremote/mip.py +++ b/tools/mpremote/mpremote/mip.py @@ -7,6 +7,7 @@ import urllib.request import json import tempfile import os +import os.path from .commands import CommandError, show_progress_bar @@ -64,22 +65,33 @@ def _rewrite_url(url, branch=None): def _download_file(transport, url, dest): - try: - with urllib.request.urlopen(url) as src: - data = src.read() - print("Installing:", dest) - _ensure_path_exists(transport, dest) - transport.fs_writefile(dest, data, progress_callback=show_progress_bar) - except urllib.error.HTTPError as e: - if e.status == 404: - raise CommandError(f"File not found: {url}") - else: - raise CommandError(f"Error {e.status} requesting {url}") - except urllib.error.URLError as e: - raise CommandError(f"{e.reason} requesting {url}") + if url.startswith(allowed_mip_url_prefixes): + try: + with urllib.request.urlopen(url) as src: + data = src.read() + except urllib.error.HTTPError as e: + if e.status == 404: + raise CommandError(f"File not found: {url}") + else: + raise CommandError(f"Error {e.status} requesting {url}") + except urllib.error.URLError as e: + raise CommandError(f"{e.reason} requesting {url}") + else: + if "\\" in url: + raise CommandError(f'Use "/" instead of "\\" in file URLs: {url!r}\n') + try: + with open(url, "rb") as f: + data = f.read() + except OSError as e: + raise CommandError(f"{e.strerror} opening {url}") + + print("Installing:", dest) + _ensure_path_exists(transport, dest) + transport.fs_writefile(dest, data, progress_callback=show_progress_bar) def _install_json(transport, package_json_url, index, target, version, mpy): + base_url = "" if package_json_url.startswith(allowed_mip_url_prefixes): try: with urllib.request.urlopen(_rewrite_url(package_json_url, version)) as response: @@ -91,12 +103,14 @@ def _install_json(transport, package_json_url, index, target, version, mpy): raise CommandError(f"Error {e.status} requesting {package_json_url}") except urllib.error.URLError as e: raise CommandError(f"{e.reason} requesting {package_json_url}") + base_url = package_json_url.rpartition("/")[0] elif package_json_url.endswith(".json"): try: with open(package_json_url, "r") as f: package_json = json.load(f) except OSError: raise CommandError(f"Error opening {package_json_url}") + base_url = os.path.dirname(package_json_url) else: raise CommandError(f"Invalid url for package: {package_json_url}") for target_path, short_hash in package_json.get("hashes", ()): @@ -105,6 +119,8 @@ def _install_json(transport, package_json_url, index, target, version, mpy): _download_file(transport, file_url, fs_target_path) for target_path, url in package_json.get("urls", ()): fs_target_path = target + "/" + target_path + if base_url and not url.startswith(allowed_mip_url_prefixes): + url = f"{base_url}/{url}" # Relative URLs _download_file(transport, _rewrite_url(url, version), fs_target_path) for dep, dep_version in package_json.get("deps", ()): _install_package(transport, dep, index, target, dep_version, mpy) diff --git a/tools/mpremote/tests/test_mip_local_install.sh b/tools/mpremote/tests/test_mip_local_install.sh new file mode 100755 index 000000000..fb8c597bd --- /dev/null +++ b/tools/mpremote/tests/test_mip_local_install.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# This test the "mpremote mip install" from local files. It creates a package +# and "mip installs" it into a ramdisk. The package is then imported and +# executed. The package is a simple "Hello, world!" example. + +set -e + +PACKAGE=mip_example +PACKAGE_DIR=${TMP}/example +MODULE_DIR=${PACKAGE_DIR}/${PACKAGE} + +target=/__ramdisk +block_size=512 +num_blocks=50 + +# Create the smallest permissible ramdisk. +cat << EOF > "${TMP}/ramdisk.py" +class RAMBlockDev: + def __init__(self, block_size, num_blocks): + self.block_size = block_size + self.data = bytearray(block_size * num_blocks) + + def readblocks(self, block_num, buf): + for i in range(len(buf)): + buf[i] = self.data[block_num * self.block_size + i] + + def writeblocks(self, block_num, buf): + for i in range(len(buf)): + self.data[block_num * self.block_size + i] = buf[i] + + def ioctl(self, op, arg): + if op == 4: # get number of blocks + return len(self.data) // self.block_size + if op == 5: # get block size + return self.block_size + +import os + +bdev = RAMBlockDev(${block_size}, ${num_blocks}) +os.VfsFat.mkfs(bdev) +os.mount(bdev, '${target}') +EOF + +echo ----- Setup +mkdir -p ${MODULE_DIR} +echo "def hello(): print('Hello, world!')" > ${MODULE_DIR}/hello.py +echo "from .hello import hello" > ${MODULE_DIR}/__init__.py +cat > ${PACKAGE_DIR}/package.json <<EOF +{ + "urls": [ + ["${PACKAGE}/__init__.py", "${PACKAGE}/__init__.py"], + ["${PACKAGE}/hello.py", "${PACKAGE}/hello.py"] + ], + "version": "0.2" +} +EOF + +$MPREMOTE run "${TMP}/ramdisk.py" +$MPREMOTE resume mkdir ${target}/lib +echo +echo ---- Install package +$MPREMOTE resume mip install --target=${target}/lib ${PACKAGE_DIR}/package.json +echo +echo ---- Test package +$MPREMOTE resume exec "import sys; sys.path.append(\"${target}/lib\")" +$MPREMOTE resume exec "import ${PACKAGE}; ${PACKAGE}.hello()" diff --git a/tools/mpremote/tests/test_mip_local_install.sh.exp b/tools/mpremote/tests/test_mip_local_install.sh.exp new file mode 100644 index 000000000..81cd06c60 --- /dev/null +++ b/tools/mpremote/tests/test_mip_local_install.sh.exp @@ -0,0 +1,11 @@ +----- Setup +mkdir :/__ramdisk/lib + +---- Install package +Install ${TMP}/example/package.json +Installing: /__ramdisk/lib/mip_example/__init__.py +Installing: /__ramdisk/lib/mip_example/hello.py +Done + +---- Test package +Hello, world! |
