Skip to content

Commit 01a0c1d

Browse files
pi-anlClaude
andcommitted
tools/mpremote: Add automatic .mpy compilation on mount.
Implement on-the-fly compilation of Python files to .mpy bytecode when MicroPython imports them via mounted filesystem. This improves import performance while maintaining backward compatibility. Features: - Auto-detect device architecture during mount - Compile .py to .mpy on-demand when device requests it - Intelligent caching with mtime-based invalidation (100MB limit) - Resource limits: cache size management with LRU eviction, 1MB file limit - Graceful fallbacks when mpy-cross unavailable or compilation fails - Command-line options: --auto-mpy (default) / --no-auto-mpy - Verbose logging support for debugging Implementation: - New module: mpremote/mpy_compiler.py (MpyCrossCompiler class) - Modified: transport_serial.py (architecture detection, stat interception) - Modified: main.py (CLI options, fixed _bool_flag for hyphenated flags) - Modified: commands.py (pass auto_mpy parameter to mount) - Modified: pyproject.toml (add mpy-cross optional dependency) - Modified: README.md (user documentation) Testing: - 25 unit tests with 100% coverage (tests/test_mpy_compiler.py) - Integration test script (tests/test_auto_mpy.sh) - All tests passing Requires mpy-cross Python package for compilation feature. Install with: pip install mpremote[mpy] Co-authored-by: Claude <[email protected]> Signed-off-by: Andrew Leech <[email protected]>
1 parent a686410 commit 01a0c1d

File tree

9 files changed

+1328
-11
lines changed

9 files changed

+1328
-11
lines changed

tools/mpremote/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ The full list of supported commands are:
1616
or any valid device name/path
1717
mpremote disconnect -- disconnect current device
1818
mpremote mount <local-dir> -- mount local directory on device
19+
options:
20+
--unsafe-links / -l: follow symlinks outside mount
21+
--auto-mpy / -m: auto-compile .py to .mpy (default)
22+
--no-auto-mpy: disable auto-compilation
1923
mpremote eval <string> -- evaluate and print the string
2024
mpremote exec <string> -- execute the string
2125
mpremote run <file> -- run the given local script
@@ -82,3 +86,88 @@ Examples:
8286
mpremote mip install aioble
8387
mpremote mip install github:org/repo@branch
8488
mpremote mip install gitlab:org/repo@branch
89+
90+
## Auto-MPY Compilation
91+
92+
When a local directory is mounted using `mpremote mount`, the tool can automatically
93+
compile Python files to bytecode (.mpy) format on-demand. This feature:
94+
95+
- **Improves import performance**: .mpy files are pre-compiled and load faster
96+
- **Transparent operation**: Automatically compiles when device requests .mpy
97+
- **Caching**: Compiled files are cached to avoid recompilation
98+
- **Graceful fallback**: Uses .py files if compilation fails
99+
100+
### Installation
101+
102+
Auto-compilation requires the `mpy-cross` Python package:
103+
104+
pip install mpremote[mpy]
105+
106+
Or install `mpy-cross` separately:
107+
108+
pip install mpy-cross
109+
110+
### Usage
111+
112+
mpremote mount /path/to/code # Auto-compile enabled (default)
113+
mpremote mount --no-auto-mpy /path/to/code # Disable auto-compilation
114+
115+
### How it Works
116+
117+
1. When MicroPython tries to import a module, it checks for .py files first
118+
2. mpremote intercepts the .py stat request and compiles to .mpy if needed
119+
3. mpremote returns "file not found" for .py to force MicroPython to use .mpy
120+
4. The compiled .mpy is saved both locally and in a cache directory
121+
5. Subsequent imports use the cached .mpy file
122+
6. Cache is automatically managed (100MB limit, LRU eviction)
123+
124+
### Important Limitations
125+
126+
When auto-mpy is enabled with `mount`, `.py` files on mounted filesystems become invisible to the device for ALL operations, not just imports.
127+
128+
This means:
129+
-**`open("script.py")`** will fail (file appears not to exist)
130+
-**`os.stat("script.py")`** will return ENOENT
131+
-**`import script`** works (uses compiled .mpy)
132+
133+
**Workaround:** Disable auto-mpy if you need direct .py file access:
134+
135+
```bash
136+
mpremote mount --no-auto-mpy /path/to/code
137+
```
138+
139+
This limitation exists because mpremote cannot distinguish between import-related file access and regular file operations at the stat interception level.
140+
141+
### Requirements
142+
143+
- The `mpy-cross` Python package must be installed
144+
- Device must support .mpy files (MicroPython v1.12+)
145+
- Architecture is auto-detected from the connected device
146+
147+
### Cache Structure
148+
149+
Compiled `.mpy` files are stored in a cache directory:
150+
151+
**Location:** `/tmp/mpremote_mpy_cache/`
152+
153+
**Cache key format:** `_<path>_<mtime>_<arch>.mpy`
154+
- Example: `_tmp_mpy_test_script_py_1732456789_0_xtensawin.mpy`
155+
- `<path>`: Source .py file path (sanitized, slashes/dots → underscores)
156+
- `<mtime>`: Modification timestamp of .py file
157+
- `<arch>`: Target architecture (e.g., xtensawin, armv7m)
158+
159+
**Cache behavior:**
160+
- Automatic LRU eviction when cache exceeds 100MB
161+
- Cached .mpy is reused if source .py file hasn't changed (mtime check)
162+
- Different architectures maintain separate cache entries
163+
- Compiled .mpy files are ALSO copied to the mounted directory alongside .py files
164+
- Example: `/path/to/code/module.py``/path/to/code/module.mpy` + cache entry
165+
166+
### Troubleshooting
167+
168+
If auto-compilation isn't working:
169+
- Check if `mpy-cross` is installed: `python -c "import mpy_cross"`
170+
- Use verbose mode to see compilation messages: `mpremote -v mount /path/to/code`
171+
- Manually disable if needed: `mpremote mount --no-auto-mpy /path/to/code`
172+
- Check cache directory: `ls -lh /tmp/mpremote_mpy_cache/`
173+
- Clear cache if needed: `rm -rf /tmp/mpremote_mpy_cache/`

tools/mpremote/mpremote/commands.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import sys
66
import tempfile
77
import zlib
8-
98
import serial.tools.list_ports
109

1110
from .transport import TransportError, TransportExecError, stdout_write_bytes
@@ -506,19 +505,18 @@ def do_eval(state, args):
506505

507506

508507
def do_run(state, args):
509-
filename = args.path[0]
510508
try:
511-
with open(filename, "rb") as f:
509+
with open(args.path[0], "rb") as f:
512510
buf = f.read()
513511
except OSError:
514-
raise CommandError(f"could not read file '{filename}'")
512+
raise CommandError(f"could not read file '{args.path[0]}'")
515513
_do_execbuffer(state, buf, args.follow)
516514

517515

518516
def do_mount(state, args):
519517
state.ensure_raw_repl()
520518
path = args.path[0]
521-
state.transport.mount_local(path, unsafe_links=args.unsafe_links)
519+
state.transport.mount_local(path, unsafe_links=args.unsafe_links, auto_mpy=args.auto_mpy)
522520
print(f"Local directory {path} is mounted at /remote")
523521

524522

tools/mpremote/mpremote/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,18 +83,20 @@ def do_version(state, _args=None):
8383

8484
def _bool_flag(cmd_parser, name, short_name, default, description):
8585
# In Python 3.9+ this can be replaced with argparse.BooleanOptionalAction.
86+
dest = name.replace("-", "_")
8687
group = cmd_parser.add_mutually_exclusive_group()
8788
group.add_argument(
8889
"--" + name,
8990
"-" + short_name,
9091
action="store_true",
9192
default=default,
93+
dest=dest,
9294
help=description,
9395
)
9496
group.add_argument(
9597
"--no-" + name,
9698
action="store_false",
97-
dest=name,
99+
dest=dest,
98100
)
99101

100102

@@ -127,6 +129,13 @@ def argparse_mount():
127129
False,
128130
"follow symbolic links pointing outside of local directory",
129131
)
132+
_bool_flag(
133+
cmd_parser,
134+
"auto-mpy",
135+
"m",
136+
True,
137+
"automatically compile .py to .mpy on import (default)",
138+
)
130139
cmd_parser.add_argument("path", nargs=1, help="local path to mount")
131140
return cmd_parser
132141

0 commit comments

Comments
 (0)