Home

Awesome

freezefs: Create self-extracting compressed or self-mounting archives for MicroPython

Purpose

freezefs saves a file structure with subfolders and builds an self-extractable or self-mountable archive in a .py file, optionally with compression, to be frozen as bytecode or extracted.

There are several ways to use freezefs:

These two options require RAM to import the archive:

Overall, it simplifies deploying text and binary files, such as MicroPython code, html pages, json data files, etc.

Description

freezefs is a utility program that runs on a PC and converts an arbitrary folder, subfolder and file structure into a Python source file. The files can be compressed. The generated Python file can then be frozen as bytecode into a MicroPython image, installed with mip on a microcontroller.

The archive file can be either mounted as a Virtual File System or extracted.

The files can be compressed or be left uncompressed.

The drivers for mounting or extracting are included in the same generated Python file, making the output file a self-mounting or self-extracting archive file.

Feedback

Please report any problem or ask for support in the issues section. If it works for you, please star the repository.

Installation

Install the software with pip install freezefs

This installs the freezefs utility to be run on PC or MAC. The necessary MicroPython code is also installed on the PC or MAC and then automatically included in the output file.

An example: freeze a files to a MicroPython image and mount file system

Suppose you have the following folders, files and subfolders on your PC and want to freeze that together with your MicroPython programs in a MicroPython image:

myfolder
    |
    +---index.html
    +---tunes.html
    +---favicon.ico
    +---css
    |    |
    |    +---mystyles.css
    |    +---normalize.css
    |
    +---images
         |
         +---myimage.jpg

The following command will archive the complete structure to the myfolder.py file:

python -m freezefs  myfolder frozen_myfolder.py 

The frozen_myfolder.py will now contain all the files and folders, together with the code to mount this as a read only file system. To mount on the microcontroller, add this line to boot.py or main.py:

import frozen_myfolder

When booting up the microcontroller, and once import frozen_myfolder has been executed, the above file structure is mounted (using os.mount() internally) at /myfolder, and the files and folders will appear under /myfolder on the microcontroller as read only files. The files are not copied to /myfolder, but remain in the MicroPython image on flash. They now can be accessed with MicroPython statements such as open( "/myfolder/index.html", "r"), read(), readline(), open in "rb" or "r" mode, os.listdir("/myfolder") etc. lsIf the import is in boot.py, the files are also visible with mpremote ls. The RAM overhead is low, and access speed is similar to regular flash files.

Another example: create a self-extractable file archive

Use:

python -m freezefs  myfolder frozen_myfolder.py --on-import=extract --compress

The frozen_myfolder.py will now contain all the files and folders compressed with zlib, together with the code to extract the files to the flash file system at /. Optionally compile with mpy-cross frozen_myfolder.py to reduce file size. Have your code import frozen_myfolder. This will decompress and extract (copy) the complete folder and subfolders to flash memory. On the next import, the files won't be overwritten (see --overwrite option).

Importing or running the file, for example mpremote run frozen_myfolder will also extract all files. Since this is quite fast, this is aids deploying software.

freezefs utility

You run this program on your PC to freeze a folder and its content (files and subfolders) to a .py file.

python -m freezefs --help will show the command format and options.

usage: python -m freezefs [-h] [--on-import {mount,extract}] [--target TARGET] [--overwrite {never,always}]
                          [--compress | --no-compress | -c] [--wbits WBITS] [--level LEVEL] [--silent]
                          infolder outfile

freezefs.py
freezefs saves a file structure with subfolders and builds an self-extractable or self-mountable archive in a .py file, optionally with compression, to be frozen as bytecode or extracted.

Examples:
freezefs.py input_folder frozenfiles.py --target=/myfiles --on_import mount
freezefs.py input_folder frozenfiles.py --target=/myfiles --on_import=extract --compress

positional arguments:
  infolder              Input folder path
  outfile               Path and name of output module. Must have .py extension.

options:
  -h, --help            show this help message and exit
  --on-import {mount,extract}, -oi {mount,extract}
                        Action when importing output module. Default is mount.
  --target TARGET, -t TARGET
                        For --on-import=mount: mount point. For --on-import=extract: destination folder. 
                        Default: the infolder.
                        Example: --target /myfiles. Must start with /
  --overwrite {never,always}, -ov {never,always}
                        always: on extract, all files are overwritten. never: on extract, no file is
                        overwritten, only new files are extracted. Default: never.
  --compress, --no-compress, -c
                        Compress files before writing to output .py. See python zlib compression. (default:
                        False)
  --wbits WBITS, -w WBITS
                        Compression window of 2**WBITS bytes. Between 9 and 14. Default is 10 (1024 bytes)
  --level LEVEL, -l LEVEL
                        Compression level. Between 0 (no compression) and 9 (best compression). Default is 9
  --silent, -s          Supress messages printed when mounting/copying files and while running this program.

The infolder

The input folder and subfolders contain the files to be archived in the output .py file.

The output .py file

The outfile is overwritten with the MicroPython code with file contents (possibly compressed), file and folder names and the code to mount the archive as file system or extract the files.

freezefs with --on-import mount

With this option, the output .py module mounts its file system on import at the mount point (virtual folder) specified by --target as read-only files.

The purpose --on-import=mount option to enable mounting a file system frozen in bytecode in a MicroPython image. So the best use for this option is to put the .py output file or files into a manifest.py, generate the MicroPython image and load the image to a microcontroller. Add a import of the output .py file in the main.py or boot.py and the files get visible read only at the specified target.

See section on RAM usage below for --on-import with --compress.

freezefs with --on-import=extract

This option is intended for use with --compress to makme a self extractable .py file.

To obtain maximum gain from compression, compile the .py with mpy-cross to a .mpy file.

When importing or running this .py file on a MicroPython system, the file system gets decompressed and extracted.

Also see --overwrite option.

--on-import=extract with --overwrite=never

When extracting, each file that exists will be skipped. Only non-existing files will be extracted. Existing files will be never overwritten.

--on-import=extract with --overwrite=always

When extracting, existing files will be always overwritten.

freezefs --target

For --on-import=mount this is the mount point on the file system of the microcontroller.

For --on-import=export, this is the destination folder on the file system of the microcontroller.

Must start with /

For --on-import=extract, --target=/ is used to deploy files to the root folder, such as main.py.

If omitted, the last subfolder of the infolder is set as target, for example if the infolder is /myfolder/subfolder, target will be set to /subfolder.

freezefs --compress

This option compresses the files when packing them into the output .py files and decompresses them using MicroPytnon deflate on the microcontroller.

This option is best for use with --on-import=extract. It works with --on-import=mount, but the RAM usage is high when opening text files with "r" mode. See section on RAM usage below.

freezefs --compress, --wbits and --level options

--wbits indicates the number of bytes used at any time for compressing (called the window size). The size of the window is 2**WBITS, so --wbits=9 means windows size of 2**9=512 bytes and --wbits=14 means 2**14=16384 bytes. The higher the value, the better the compression. However, to decompress, up to 2**WBITS bytes may needed on the microcontroller.

--level goes from 0 (no compression) to 9 (highest compression). Level 9 is a bit slower to decompress.

See Python docs for zlib and MicroPython docs for deflate for more details.

freezefs --silent

By default, mount and extract print the progress. If you want to suppress those messages, freeze the files with --silent.

If an exception occurs during mount or extract, the exception will be raised independently of the --silent option.

The frozen .py output file

The output file of the freezefs utility is a module with the frozen file system. This generated module contains consts with all the file data. MicroPython will access the file data directly in flash, if the .py file is frozen as bytecode in a MicroPython image.

The code for extract or mount is included in the file. When compiled to .mpy files, the additional code amounts to about 1800 bytes for mount and 1300 bytes for extract.

The mounted virtual file system

freezefs implements a Virtual File System (VFS), with the driver included in the output file when using --on-import=mount

The VFS implements os.mount, os.umount, os.chdir, os.getcwd, os.ilistdir, os.listdir, os.stat, open, close, read, readinto, readline, the iterator for reading lines and the decoding of UTF-8 format files to MicroPython strings.

statvfs returns block size of 1. The file system size is the sum of all file sizes, without overhead. Mode flag is 1 (read only). The maximum file length is set to 255.

open will only accept modes "r", "rt" and "rb". As usual, "r" will decode UTF-8 to strings, and "rb" will return bytes.

open with modes "w", "wb", "a", etc. raises an OSError, since the file system frozen into the MicroPython image is read only.

remove, mkdir and rename will raise an OSError( errno.EPERM ).

ilistdir will show file type (0x4000 for folders, 0x8000 for files as usual) and file size. Unused fields returned by ilistdir are set to zero.

If --compress was used, the files are decompressed on open while reading the stream. read(), readinto(), readline(), readlines() are available. However, seek() and tell() are not available for compress. See also RAM usage below.

RAM usage for --on-import=mount

When frozen as bytecode in a MicroPython image, the RAM usage is low, about 1 kbyte.

---on-import=mount with --compress is RAM intensive on the microcontroller. Files opened with "r" (text mode) have to be decompressed in RAM, and the complete file gets loaded to RAM. This does not affect files opened with "rb" mode (binary mode), RAM usage is similar to opening a regular file system file.

When the .py file resides in the flash file system (as opposed to being frozen in the MicroPython image) the complete file is read to RAM, and it's now essentially a read only RAM disk.

RAM usage for --on-import=extract

When frozen as bytecode in a MicroPython image, the RAM usage is very low while extracting. The buffer size to read/write is set at 256 bytes.

Compressed .py files will use up to 2**WBITS bytes of RAM while decompressing. The --wbits option can be used to set this value if RAM is low. The higher the WBITS value, the better the compression.

When extracting a .py archive residing in the flash file system (or on SD card), the .py file is best compiled with mpy-cross to a .mpy to have the best gain in size. The complete .py (or .mpy) file will be loaded to RAM. To get that memory back once the extract is done, use __import__("module-name"), without assigning the result of __import__ to a variable. The extract driver will delete the itself from the sys.modules[] list, so the next garbage collection will free the memory.

Unit tests

The /tests folder on github has unit tests.

Dependencies

These standard MicroPython libraries are needed: sys, os, io.BytesIO, io.StringIO, collections.OrderedDict and errno. If --compress is used, deflate is needed. Deflate is present in MicroPython 1.20 or later.

Python 3.10 or later must be installed on the PC. Probably earlier versions of Python will work too. pahtlib is used to be platform independent.

The code is MicroPython/Python only, no C/C++ code. There are no processor or board specific dependencies.

Restrictions

While a freezefs filesystem is mounted, os.sync() may crash the microcontroller, raise a TypeError, or it may even appear to work. os.sync() cannot be used with freezefs.

If you are using os.sync() and want to use freezefs, you may consider dropping os.sync() altogether. See MicroPython issue #11449 (https://github.com/micropython/micropython/issues/11449). On most architectures and filesystems (including ESP32, RP2040 and LittleFS2), os.sync() is a no-op (at lest for MicroPython up to version 1.23). Verify that for you case os.sync() really does something.

Changes since version 1

Version number 2. If you are using version 1, please regenerate the output .py files with the new version of freezefs as they are incompatible.

Added --compress and --overwrite switches. Drivers for extracting and mounting are now included in the compressed file, no need to install drivers. freezefs is now pip installable.

Compatibility with MicroPython/Python versions

Tested with MicroPython 1.20 to 1.22 and Python 3.10.7 and 3.11.4.

Copyright and license

Source code and documentation Copyright (c) 2023 Hermann Paul von Borries.

This software and documentation is licensed according to the MIT license:

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.