Skip to content

Commit 815d15e

Browse files
committed
mip: Support optional per-entry native code compatibility tags.
Related to #1140 / micropython#19478 / micropython#19479: those add URL-templating for single-file natmod installs, but don't help when a single manifest needs to serve different files per architecture. Adds an optional third element to hashes/urls entries: a raw sys.implementation._mpy-shaped integer. Entries without it install unconditionally (backward compatible); entries with it only install if _mpy_tag_ok() matches - exact match on version/sub-version/arch, subset match on arch-flags (a variant that doesn't require an extension must still install on hardware that supports it). Signed-off-by: o-murphy <thehelixpg@gmail.com>
1 parent 6a9f163 commit 815d15e

4 files changed

Lines changed: 68 additions & 2 deletions

File tree

micropython/mip/mip/__init__.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,29 @@
2424

2525
_ALLOWED_MIP_URL_PREFIXES = const(("http://", "https://", "codeberg:", "github:", "gitlab:"))
2626

27+
# Bits 0-15 of sys.implementation._mpy: version/sub-version/arch (exact
28+
# match required). Bits 16+: optional arch-flags (e.g. RV32 extensions) -
29+
# a tag's flags must be a subset of what this device supports, not equal.
30+
_MPY_VERSION_MASK = const(0xFF)
31+
_MPY_BASE_MASK = const(0xFFFF)
32+
33+
34+
def _mpy_tag_ok(tag, device_mpy=None):
35+
if device_mpy is None:
36+
device_mpy = getattr(sys.implementation, "_mpy", None)
37+
if device_mpy is None:
38+
return False
39+
# A bytecode-only tag (arch nibble 0) only needs the major version to
40+
# match - mirrors mp_raw_code_load()'s own arch == MP_NATIVE_ARCH_NONE
41+
# fast path in py/persistentcode.c, which skips the sub-version check
42+
# entirely for non-native code (sub-version only encodes native-code ABI
43+
# changes and is meaningless for portable bytecode).
44+
if (tag >> 10) & 0xF == 0:
45+
return (tag & _MPY_VERSION_MASK) == (device_mpy & _MPY_VERSION_MASK)
46+
if (tag & _MPY_BASE_MASK) != (device_mpy & _MPY_BASE_MASK):
47+
return False
48+
return (tag >> 16) & (device_mpy >> 16) == (tag >> 16)
49+
2750

2851
# This implements os.makedirs(os.dirname(path))
2952
def _ensure_path_exists(path):
@@ -112,7 +135,10 @@ def _install_json(package_json_url, index, target, version, mpy):
112135
package_json = response.json()
113136
finally:
114137
response.close()
115-
for target_path, short_hash in package_json.get("hashes", ()):
138+
for entry in package_json.get("hashes", ()):
139+
target_path, short_hash = entry[0], entry[1]
140+
if len(entry) > 2 and not _mpy_tag_ok(entry[2]):
141+
continue
116142
fs_target_path = target + "/" + target_path
117143
if _check_exists(fs_target_path, short_hash):
118144
print("Exists:", fs_target_path)
@@ -122,7 +148,10 @@ def _install_json(package_json_url, index, target, version, mpy):
122148
print("File not found: {} {}".format(target_path, short_hash))
123149
return False
124150
base_url = package_json_url.rpartition("/")[0]
125-
for target_path, url in package_json.get("urls", ()):
151+
for entry in package_json.get("urls", ()):
152+
target_path, url = entry[0], entry[1]
153+
if len(entry) > 2 and not _mpy_tag_ok(entry[2]):
154+
continue
126155
fs_target_path = target + "/" + target_path
127156
is_full_url = any(url.startswith(p) for p in _ALLOWED_MIP_URL_PREFIXES)
128157
if base_url and not is_full_url:

micropython/mip/test_mip_tag.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from mip import _mpy_tag_ok
2+
3+
# Exact match on version/sub-version/arch (bits 0-15), no arch-flags.
4+
assert _mpy_tag_ok(0x0A06, 0x0A06) is True
5+
6+
# Different version/sub-version/arch -> reject.
7+
assert _mpy_tag_ok(0x0A06, 0x0B06) is False
8+
assert _mpy_tag_ok(0x0A06, 0x0A07) is False
9+
10+
# Arch-flags (bits 16+): tag's required flags must be a subset of the
11+
# device's, not an exact match.
12+
assert _mpy_tag_ok(0x0A06 | (0b001 << 16), 0x0A06 | (0b011 << 16)) is True
13+
assert _mpy_tag_ok(0x0A06 | (0b011 << 16), 0x0A06 | (0b001 << 16)) is False
14+
assert _mpy_tag_ok(0x0A06, 0x0A06 | (0b111 << 16)) is True # tag needs nothing
15+
assert _mpy_tag_ok(0x0A06 | (0b111 << 16), 0x0A06) is False # device supports nothing
16+
17+
# No _mpy support on the device (e.g. bytecode-only build).
18+
assert _mpy_tag_ok(0x0A06, None) is False
19+
20+
# Bytecode-only tag (arch nibble 0): only the major version must match --
21+
# sub-version and arch are ignored, mirroring mp_raw_code_load()'s own
22+
# fast path for non-native code in py/persistentcode.c.
23+
_BYTECODE_TAG = 0x0206 # version=6, sub-version=2, arch=NONE(0)
24+
assert _mpy_tag_ok(_BYTECODE_TAG, 0x0A06) is True # same major, different sub/arch on device
25+
assert _mpy_tag_ok(_BYTECODE_TAG, 0x0107) is False # different major version
26+
27+
# Omitting device_mpy falls back to sys.implementation._mpy; just check it
28+
# runs without raising, since that value depends on the build running the test.
29+
_mpy_tag_ok(0x0A06)
30+
31+
print("PASS")

tools/build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@
9999
# "v": 1, <-- file format version
100100
# "hashes": [
101101
# ["aioble/server.mpy", "e39dbf64"],
102+
# ["aioble/server.mpy", "e39dbf64", 2822], # optional 3rd element: only
103+
# # installed if it matches the
104+
# # device's sys.implementation._mpy
105+
# # (not emitted by this script)
102106
# ...
103107
# ],
104108
# "urls": [ <-- not used by micropython-lib packages

tools/ci.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,15 @@ function ci_package_tests_setup_lib {
5050
$CP -r python-stdlib/unittest/unittest "${VIRTUAL_ENV}/lib/"
5151
$CP -r python-stdlib/unittest-discover/unittest "${VIRTUAL_ENV}/lib/"
5252
$CP unix-ffi/ffilib/ffilib.py "${VIRTUAL_ENV}/lib/"
53+
$CP -r python-ecosys/requests/requests "${VIRTUAL_ENV}/lib/"
5354
tree "${VIRTUAL_ENV}"
5455
}
5556

5657
function ci_package_tests_run {
5758
export MICROPYPATH
5859
for test in \
5960
micropython/drivers/storage/sdcard/sdtest.py \
61+
micropython/mip/test_mip_tag.py \
6062
micropython/umqtt.simple/test_umqtt_simple.py \
6163
micropython/xmltok/test_xmltok.py \
6264
python-ecosys/requests/test_requests.py \

0 commit comments

Comments
 (0)