Skip to content

feat: add buildfs/uploadfs — build and flash LittleFS/SPIFFS images from data/ #1358

Open

Description

@zackees

Summary

fbuild has no way to build or flash a filesystem image. PlatformIO's buildfs / uploadfs targets have no fbuild equivalent, so there is no path from a sketch's data/ directory to an on-device filesystem.

Split out of #1354, whose original premise turned out to be partly stale: build-time .lnk resolution already exists. This is the actual remaining gap.

Verified current state

  • No buildfs, uploadfs, --filesystem or equivalent anywhere in the fbuild CLI surface.
  • .lnk resolution and materialization already exist: crates/fbuild-toolchain/src/lnk/mod.rs:1-22 documents scanner -> resolver -> materializer -> downstream build steps, and materialize.rs projects <src>/<rel>/foo.ext.lnk into <build_resources_dir>/<rel>/foo.ext.
  • One consumer already exists: crates/fbuild-build-esp/src/esp32/orchestrator/embed_stage.rs:38-46 resolves .lnk entries named in board_build.embed_files and objcopy-embeds them into firmware.
  • So the pipeline terminates at "materialized into the build tree" with no filesystem-image consumer.

Proposed

fbuild buildfs — assemble a filesystem image from the build-tree projection of data/:

  • inputs = committed files in data/ union materialized .lnk blobs
  • exclude *.lnk sidecars from the image (they must never ship to the device)
  • invoke mklittlefs / mkspiffs per the environment's board_build.filesystem
  • pre-check total size plus filesystem overhead against the partition size from the partition CSV, and fail at build time rather than at runtime mount

fbuild uploadfs — flash that image to the filesystem partition via esptool (already a dependency for firmware flashing).

Partition configuration — ci/boards.py in FastLED already has board_build_filesystem_size (unused) and board_partitions (used by two boards, both huge_app.csv). A default partition CSV reserving a filesystem region is needed for boards that want one.

Platform capability

buildfs is only meaningful where a flash filesystem exists:

Platform Flash FS PIO buildfs/uploadfs
ESP32 (Arduino-ESP32) LittleFS, SPIFFS (legacy), FFat Yes
ESP8266 LittleFS (SPIFFS deprecated in core docs) Yes (plus uploadfsota)
RP2040/RP2350 (arduino-pico) LittleFS only Yes
Teensy 4.x LittleFS_Program — runtime only, re-upload wipes files No
AVR none (1 KB EEPROM) No
SD card, all platforms runtime library, removable media n/a — not deployable by a build tool

Selecting a filesystem target on AVR or Teensy should be a clear error, not a silent no-op.

Blocker

FastLED has no LittleFS backend. FsImpl is implemented only for Arduino SD over SPI, the WASM browser VFS, and a host stub. A littlefs image would be unreachable from FastLED's FileSystem API. buildfs should not ship pointing at an API gap — see the paired FastLED issue linked below.

Related

Activity

  1. zackees commented on Aug 22, 2026

    @zackees
    MemberAuthor

    Tracked in the meta issue: FastLED/FastLED#3986 — remote assets: hosting, build-time resolution, and on-device filesystem deployment.

  2. zackees commented on Aug 23, 2026

    @zackees
    MemberAuthor

    Unblocked: FastLED/FastLED#3984 is merged, so fl::getEmbeddedFs() now exists and a LittleFS image would be readable from fl::FileSystem.

    That was the stated blocker here — shipping buildfs without a backend would have produced images nothing could read. The remaining work in this issue (buildfs/uploadfs, partition config, mklittlefs/esptool wiring) is independent of it now.

  3. zackees commented on Oct 29, 2026

    @zackees
    MemberAuthor

    Progress: the image-assembly half is up as #1788 — fbuild buildfs.

    What it does today:

    • packs data/ (or data_dir) into a LittleFS/SPIFFS image using
      mklittlefs/mkspiffs, sized from the board's partition CSV;
    • resolves fbuild blob pointers (.fetch, JSON .lnk) into the image under
      their projected name and never ships the pointer; a pointer that fails to
      parse is now a hard error (it was silently copied into the image during
      development, which would have handed the device a pointer instead of
      content);
    • ships plain-text .lnk (FastLED runtime asset links) as ordinary data;
    • selects the filesystem partition by label or by spiffs/littlefs subtype
      (0x82/0x83 included), and fails at build time with the byte shortfall
      when data/ cannot fit, instead of failing to mount at runtime;
    • --check-only prints the size plan without running the tool.

    Proven end-to-end on this host with the real mklittlefs (ver. 05d49dc):
    addresses 0x310000/1 MiB partition → 1048576-byte image, mklittlefs -l
    lists the packed tree, 18 new unit tests, full workspace suite 3974 passed.

    Still open here (deliberately not in #1788 — it belongs to the daemon deploy
    path, not the image builder):

    • uploadfs — flash the image to the filesystem partition via esptool;
    • default partition CSVs that reserve a filesystem region for boards that
      currently have none.
  4. zackees commented on Oct 29, 2026

    @zackees
    MemberAuthor

    Update: the image-assembly half of this issue is merged — #1788 (fbuild buildfs) landed on main (squash/merge commit f4bb0f18).

    Shipped: fbuild buildfs packs data/ (or the env's data_dir) into a LittleFS/SPIFFS image using the real mklittlefs/mkspiffs, sized from the board's partition CSV (row picked by label or spiffs/littlefs/0x82/0x83 subtype), with --check-only / --json, blob-pointer resolution (.fetch / JSON .lnk packed under the projected name, never shipped), and a capacity pre-check that fails with the byte shortfall before the tool runs. Docs in docs/reference/cli.md.

    Still open in this issue (intentionally not done here): uploadfs, which is a daemon deploy operation needing region selection and progress plumbing, plus default filesystem partition CSVs for boards that do not declare one.

  5. zackees commented on Oct 29, 2026

    @zackees
    MemberAuthor

    uploadfs is up as #1789 (the deploy half; buildfs shipped in #1788).

    What it does: resolves the target partition through the same buildfs::plan_image the build half uses, so the image is written at exactly the offset the firmware mounts — no offset flag; builds a missing image first; writes the region through esptool with the board's flash params and the build's resolved esptool path; preempts the daemon's serial lease first so a monitor session is not "port busy"; --verify reads the region back with verify-flash.

    Verified on an ESP32-S3 (/dev/ttyACM1): 1,441,792-byte LittleFS image written at 0x290000, verify matched on device, mklittlefs -l lists the packed tree.

    Still open against this issue after #1789 merges: RP2040 littlefs and ESP8266 image building (both need their own image pipeline — the current plan reader is an ESP32 partition CSV), and the FastLED LittleFS FsImpl backend that the issue's "Blocker" section calls out.

  6. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    fbuild uploadfs (ESP32 half of this issue) is merged: #1789 (70aec880).

    It flashes one filesystem image at the offset the project's partition CSV
    declares — no user-supplied offset, one file/one region through the same
    esptool argv + --verify readback fbuild deploy uses. buildfs and
    uploadfs now share buildfs::plan_image, so image size and flash offset
    can't drift apart.

    Verified end-to-end on an ESP32-S3 (/dev/ttyACM1): 1,441,792-byte LittleFS
    image at 0x290000, uploadfs --verify -> matched.

    Still open here: RP2040 / ESP8266 filesystem support (ESP32 only today).

  7. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    RP2040 filesystem image building has landed: #1792 (e1cf487f).

    What it does: fbuild buildfs now reads the filesystem region from arduino-pico's own boards.txt (build.fs_start / build.fs_end on the flash menu option selected by board_build.filesystem_size, else board_build.flash) through the same loader the RP2040 build uses — so the image size and the firmware's FS_START/FS_END defines cannot drift, the same invariant plan_image gives ESP32's partition CSV.

    Verified on a fixture rpipico project with board_build.filesystem_size = 0.5m: 524,288-byte littlefs image at 0x1017f000 (arduino-pico's own fs_start for that option), mklittlefs -l lists all three packed files, and unpacking round-trips a 70,000-byte blob byte-identically. A board whose selected menu reserves no volume (the stock 2 MB Pico default, fs_start == fs_end) fails with both addresses plus the knob to set; --filesystem=spiffs is refused because arduino-pico has a single littlefs driver.

    Still open on this issue:

    • ESP8266 image building (different tool/geometry: mkspiffs/mklittlefs with the eagle.flash.*.ld linker-script layout, not a testable target on this host today).
    • RP2040 flashing — an RP2040 filesystem is written over PICOBOOT/UF2, not an esptool region write, so fbuild uploadfs remains ESP32-only.
  8. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    ESP8266 filesystem image building is up as #1800.

    What it does: an ESP8266 module has no partition table, so fbuild buildfs now
    resolves the eesz "Flash Size" menu option in arduino-esp8266's boards.txt
    (build.flash_ld=eagle.flash.4m1m.ld), reads that linker script, and takes both
    the region and the filesystem geometry from its _FS_start / _FS_end /
    _FS_block / _FS_page symbols — the same symbols the firmware links against,
    so image size and mounted volume cannot drift. The script is parsed directly
    rather than re-derived from the duplicated build.spiffs_start/spiffs_end
    copies in boards.txt.

    The block/page size moved into FsGeometry (buildfs_geometry.rs) because an
    image formatted 4096/256 without being mounted 4096/256 does not mount: ESP32
    stays 4096/256, an ESP8266 volume takes its script's own geometry (8192/256 for
    a 4 MB layout), and the size estimate and the mklittlefs argv come from that
    one value.

    Verified end-to-end with arduino-esp8266 3.30102.0 and PlatformIO's mklittlefs
    on a nodemcuv2 / board_build.eesz = 4M2M fixture: 2,072,576-byte littlefs
    image at 0x40400000 (the script's _FS_start), mklittlefs -l -b 8192 -p 256
    lists the packed tree, and unpacking round-trips a 70,000-byte blob
    byte-identically. 14 new unit tests; workspace suite 4020 passed / 0 failed.

    Still open on this issue:

    • RP2040 / ESP8266 flashing — an ESP8266 filesystem write is a serial
      esptool region write at _FS_start - 0x40200000, and RP2040 goes over
      PICOBOOT/UF2; fbuild uploadfs is ESP32-only until those transports exist.
    • the FastLED LittleFS FsImpl backend the issue's "Blocker" section calls out.
  9. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    ESP8266 image building landed: #1800 merged (merge commit f65fdf6, 2026-10-30T13:37:33Z).

    fbuild buildfs now covers three platforms:

    • ESP32 — partition CSV (partitions.csv), 4096/256 geometry.
    • RP2040 — build.fs_start/build.fs_end from arduino-pico's flash menu option.
    • ESP8266 — the eesz menu's eagle.flash.*.ld linker script: region and block/page geometry from _FS_start/_FS_end/_FS_page/_FS_block (256/8192 there, not the ESP32 4096/256).

    fbuild uploadfs remains ESP32-only. Still open here: serial flashing of the ESP8266 image (_FS_start - 0x40200000 write) and an RP2040 transport (PICOBOOT/UF2).

  10. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    fbuild uploadfs now flashes ESP8266 images too — PR #1802 (unmerged).

    The ESP8266 half needed one address conversion plus a deployer: the linker
    script's _FS_start is a CPU-mapped address (0x40400000 for 4M2M), while
    esptool wants an offset from the start of flash, so 0x40200000 comes back off
    before the write (0x200000) and an address below the mapping is a hard error
    rather than a wrapped huge offset. The write goes through the same esptool
    region writer as ESP32 with --chip esp8266 and the board's own flash
    mode/frequency/size.

    Verified on hardware: nodemcuv2 + board_build.eesz = 4M2M on /dev/ttyUSB0
    (CH340) — the 2,072,576-byte littlefs image the buildfs PR produces flashed at
    0x200000, then --verify (device-side MD5 comparison) matched.

    Remaining on this issue: an RP2040 transport (PICOBOOT/UF2). All three platforms
    now build images; ESP32 and ESP8266 flash them.

  11. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    Merged #1802 (merge commit 3e901c21), which closes the ESP8266 half of the flashing story.

    Status matrix now on main

    Platform fbuild buildfs fbuild uploadfs
    ESP32 (all variants) ✅ #1788 ✅ #1789 (partition-table offset, --verify readback)
    ESP8266 (arduino-esp8266) ✅ #1800 (region + 256/8192 geometry from eagle.flash.*.ld) ✅ #1802 (--chip esp8266, mapped→flash offset)
    RP2040 (arduino-pico) ✅ #1792 (build.fs_start/_end from the flash menu) ❌ still open

    What #1802 added for ESP8266 uploads

    • uploadfs accepts espressif8266; the linker script's _FS_start is a mapped
      address, so 0x40200000 is subtracted before esptool sees the offset
      (underflow → hard error), while ESP32 partition offsets pass through unchanged.
    • Deploy writes one region with --chip esp8266 using the board's flash
      mode/freq/size (native espflash is off — no ESP8266 support).
    • esptool resolution: FBUILD_ESPTOOL_PATH → managed v5 binary → PATH
      (arduino-esp8266 pins the v4 source package, which the v5 CLI deployer can't drive).

    Real hardware E2E on a nodemcuv2 / 4M2M (CH340, /dev/ttyUSB0):
    2,072,576-byte littlefs flashed at 0x200000, --verify matched on-device.

    Remaining on this issue

    RP2040 flashing (PICOBOOT/UF2 transport) — buildfs for RP2040 landed in #1792,
    but there is no deploy transport yet. Deploy transports for ESP32 and ESP8266 are
    now complete.

  12. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    buildfs / uploadfs matrix after #1803

    platform buildfs uploadfs
    ESP32 ✅ partition CSV (#1792 line) ✅ esptool region write + --verify
    ESP8266 ✅ eesz linker-script region and geometry ✅ esptool --chip esp8266 + --verify
    RP2040 (arduino-pico) ✅ build.fs_start/fs_end per flash menu ✅ (PR #1803) UF2 into the mounted BOOTSEL volume

    RP2040 is a different transport by nature: the region is only programmable by the
    ROM's BOOTSEL mass-storage loader, at the absolute flash address the firmware
    mounts (build.fs_start, no mapping subtraction). #1803 encodes the image as UF2
    blocks at that address, writes them to the single mounted RPI-RP2 volume as
    NEW.UF2, and reports the volume + block count. --verify is refused for RP2040
    because the region can only be read back over PICOBOOT — the volume ejecting is
    the confirmation.

    With that, every platform fbuild can build an image for also has an upload path, so
    this issue's scope is complete pending #1803's merge and a hardware run on a Pico.

  13. zackees commented on Oct 30, 2026

    @zackees
    MemberAuthor

    #1803 merged (merge commit 503410b8) — the matrix is code-complete:

    platform buildfs uploadfs
    ESP32 ✅ partition CSV ✅ esptool region write + --verify
    ESP8266 ✅ eesz linker-script region + geometry ✅ esptool --chip esp8266 + --verify
    RP2040 (arduino-pico) ✅ build.fs_start/fs_end ✅ UF2 into the mounted BOOTSEL volume (#1803)

    Usage on a Pico: set board_build.filesystem_size (the stock 2 MB Pico default
    reserves no volume), fbuild buildfs -e rpipico, hold BOOTSEL while plugging in,
    then fbuild uploadfs -e rpipico. The image is encoded as UF2 blocks at the
    absolute fs_start and written to the single mounted RPI-RP2 volume as
    NEW.UF2; the volume ejecting is the confirmation. --verify is refused (a
    region is readable only over PICOBOOT) and --port/--baud are ignored with a
    warning (BOOTSEL is mass storage).

    One caveat to close this out honestly: the RP2040 upload path is not proven on
    RP2040 hardware yet
    — no Pico was attached to the machine that wrote it. The
    encoding, address arithmetic and write/eject handling are unit-tested (8 new
    tests incl. a full 512 KiB / 2048-block image) and reuse fbuild deploy's
    existing transport, but a first real fbuild uploadfs -e rpipico on a board is
    still outstanding before I would call the cell green end-to-end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions