Repository navigation
Description
Summary
There are two mutually incompatible .lnk formats in the FastLED ecosystem today. The one file actually committed to FastLED cannot be read by the tool named lnk. This issue is to research and decide a single normalized format plus a migration path.
The split
Format A — plain text (FastLED#2284). Committed today at FastLED/FastLED:examples/AudioUrl/data/track.mp3.lnk on origin/master:
# .lnk asset link file — see FastLED issue #2284.
# The first non-comment line is the URL this asset resolves to.
# Lines starting with '#' and blank lines are ignored.
# Additional lines (metadata like sha256=, fallback=) are reserved for
# future use and are ignored by the v1 parser.
https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3
Format B — JSON v1 (fbuild crates/fbuild-toolchain/src/lnk/format.rs). What fbuild lnk add emits and fbuild lnk pull consumes:
{
"sha256": "164804d94802dd96cd8cd37f7528ca6d30f8fd9396e812fe67ec6164bbdd2282",
"size": 6051840,
"url": "https://raw.githubusercontent.com/FastLED/assets/main/examples/FxLedmapper32x32/video1.rgb",
"v": 1
}format.rs::from_raw parses JSON only and hard-rejects anything else, so Format A fails outright under fbuild lnk pull / check.
Why this matters now
examples/AudioUrlis onorigin/masterin Format A. Anyfbuild lnk pullover that tree errors on it.- New work (
examples/Fx/FxLedmapper32x32) uses Format B. - A third variant was independently drafted during design work — key=value with repeated
url=for fallbacks — which converged on Format A's reservedsha256=/fallback=keys rather than Format B. That three-way convergence is evidence the format was never actually settled. FastLED/fbuild#1356(storage targets and diagnostics) depends on knowing which format it is annotating. This should probably be decided before that design lands.
Research needed
Prior art is genuinely split, which is why this needs research rather than a coin flip:
- git-LFS uses a text key-value pointer — and it is the closest analogue to this use case:
Deliberately line-oriented so it diffs cleanly and merges predictably in git.
version https://git-lfs.github.com/spec/v1 oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 size 12345 - DVC uses YAML (
.dvcfiles). - Bazel uses Starlark declarations, not sidecar files.
- Nix uses expressions, not sidecars.
The git-LFS precedent is a real argument for text, and it is the system whose problem most resembles ours (pointer files checked into git, standing in for large binaries).
Questions to answer:
- Text or JSON? JSON is trivially machine-parseable and already implemented in fbuild. Text diffs and merges better in git — the property git-LFS optimized for and the reason it rejected structured formats. Which matters more here, given these files live in git and are edited rarely but reviewed often?
- Should the parser accept both? A tolerant reader (sniff
{→ JSON, else text) is cheap and eliminates the migration cliff. Does permanent dual support cost more in confusion than it saves in transition? - Migration path for
examples/AudioUrl. Convert to the winner, or leave and make the parser tolerant? Note it also lacks asha256entirely, so converting it requires fetching and hashing the upstream MP3 — which is a third-party URL that could change. - What is the versioning story? Format B has
v: 1andformat.rs:110-116hard-rejectsv != 1, so a v2 bump breaks every older fbuild on sight. Adding fields to v1 is safe (unknown keys are ignored by bothLnkFileRawand FastLED'sci/asset_resolver.py). Any normalization must not require a version bump, or must accept breaking older clients. - Is
sha256required or optional? Format A has none. Format B treats it as present-in-practice. A.lnkwithout a digest gets fetch but no integrity — should that be an error, a warning, or allowed? - Multiple URLs / fallbacks? Format A reserves
fallback=. Format B has a singleurl. FastLED'sci/asset_resolver.pyaccepts a list forurl. CDN-primary-with-raw-fallback is a real need (see feat: structured manifest.json index + CDN serving for FastLED assets assets#1), so the normalized format should have an answer.
Acceptance criteria
- A single normalized format is specified and documented
-
examples/AudioUrl/data/track.mp3.lnkparses successfully underfbuild lnk check -
examples/Fx/FxLedmapper32x32/data/video1.rgb.lnkcontinues to parse - A stated position on dual-format support (permanent, transitional with an end date, or never)
- FastLED's
ci/asset_resolver.pyand its tests updated to match - Migration documented for anyone with existing
.lnkfiles
Related
- investigate: storage-target descriptors and misconfiguration diagnostics for .lnk / assets.json #1356 — storage targets and diagnostics (depends on this)
- feat: resolve remote example assets at build time (sha256-verified, globally cached) #1354 — build-time resolution
- FastLED#2284 — origin of Format A
ci/asset_resolver.pyin FastLED/FastLED — reference parser, 19 tests
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
zackees commentedon Aug 22, 2026
Tracked in the meta issue: FastLED/FastLED#3986 — remote assets: hosting, build-time resolution, and on-device filesystem deployment.
zackees commentedon Aug 22, 2026
Research: the split is wider than this issue describes — and JSON was already deliberated
Investigated against current
origin/masterof both repos (FastLED3de8d4f8d, fbuild49ec69ce). Three corrections to the issue body.1. The text format has THREE implementations, not one committed file
I originally found only
examples/AudioUrl/data/track.mp3.lnkand assumed it was a stray. It is not — it is one artifact of a complete, shipped v1 asset pipeline (FastLED#2284):src/fl/stl/url.h:274fl::parse_lnk(),:325parse_lnk_with_metadata().lnkat runtimesrc/fl/asset/asset.h,asset.cpp.hpp:109FL_ASSET("data/track.mp3")handlesci/compiler/asset_scanner.py:85_parse_lnk_content()fl::parse_lnk_with_metadata()logic"ci/wasm_build.py:1712-1720callsscan_sketch_assets()/write_manifest_json()on every WASM build. So the text format is live in the shipping product, not legacy cruft.Its grammar (
url.h:312-325, mirrored inasset_scanner.py:85-126): first non-comment line is the primary URL; subsequentkey=valuelines carry metadata; recognized keys aresha256andfallback; unknown keys ignored for forward-compat.LnkMetadata(url.h:252-259) is explicit that v1 "parsed but not enforced... The runtime ignores everything exceptprimary."2. JSON-vs-text was already deliberated in fbuild, and git-LFS was explicitly rejected
crates/fbuild-toolchain/src/lnk/format.rs:27-31documents the rationale:So my framing of "git-LFS precedent argues for text" is already-considered ground, not a new input. That weakens research question 1 considerably — the fbuild side has a written position.
3. Two schema facts I got wrong
sha256is REQUIRED in fbuild's schema, not optional (format.rs:22— "Required fields:v,url,sha256"). The text format treats it as optional and unenforced. This is the sharpest incompatibility:examples/AudioUrl/data/track.mp3.lnkhas no digest at all, so it cannot become a valid fbuild.lnkwithout someone fetching and hashing a third-party MP3 whose bytes may change.extractfield (file|zip|tar.gz,format.rs:41-50) with no text-format equivalent. Any normalization has to account for it.What this means for the decision
The two formats are not competing drafts — they serve different consumers with different guarantees:
fallback=.That reframes the question from "which format wins" to "should these stay two formats with a documented boundary, or does one absorb the other — and at what cost?"
Three real options:
.lnkextension guarantees continued confusion, andasset.h:13already misdescribes reality ("At build time, fbuild scans<sketch>/data/for*.lnkfiles" — fbuild's parser would reject the text files it is describing).{-> JSON, else text. ~30 lines each in Rust and Python; C++ runtime would need a JSON path it currently does not have and arguably should not carry.fl::parse_lnkis deliberately allocation-light), a digest for the AudioUrl asset, and touching a shipped v1 API.My recommendation is option 1 plus a rename — distinct extensions for the two roles (e.g. keep
.lnkfor the runtime asset link, use something distinct for build-time blob pointers), which removes the ambiguity without forcing a JSON parser into the MCU runtime. Recording it as a recommendation, not a decision: this touches a shipped API in both repos and deserves a maintainer call.Related discovery
src/fl/asset/asset.h:21-22states: "v1 scope: WASM and host/stub only. ESP32 LittleFS / SD-card resolution is future work and intentionally omitted." That is independent confirmation of the gap tracked in FastLED/FastLED#3984 and #1358 — the asset pipeline was deliberately built without a device-filesystem path.feat(ci): fold all asset declaration parsing into one scanner (#3990)
feat(lnk): accept the text-format .lnk alongside JSON (#1362)
zackees commentedon Aug 23, 2026
Parser side landed — #1362 merged
fbuild now reads both formats.
from_str_any()sniffs the first non-blank, non-comment character ({-> JSON, else text) andfrom_path()routes through it, solnk pull,lnk check, and the ESP embed stage all gained the text format with no call-site changes. JSON stays canonical —fbuild lnk addis unchanged.Paired with FastLED/FastLED#3990, which folded FastLED's three Python/C++ parsers into one scanner that accepts both
.lnkforms plusassets.json. Both sides of the split now read both formats.One deliberate asymmetry
sha256remains mandatory in fbuild even for the text form, where the C++ runtime treats it as optional. fbuild's resolver caches by content digest, so a.lnkwithout one cannot be content-addressed or verified — relaxing that would give up the guarantee the cache is built on. The error is actionable instead:Verified
.lnkfetched and sha256-verified into the blob cache.partialnever promoted — the integrity path, not just the happy pathCaveat on how it landed: merged with
--adminat the user's explicit instruction. The 98-job CI matrix was still running and got cancelled by the merge (92 cancelled, 2 passed), so CI did not validate this — the evidence above is all local.Still open on this issue
The parser accepts both, but the underlying question this issue asks — should there be two formats — is unanswered:
examples/AudioUrl/data/track.mp3.lnkstill has nosha256, so it now gets a clear error instead of a confusing one. Adding a digest means hashing a third-party MP3 whose bytes can change..lnkfor the runtime asset link, something else for build-time blob pointers — rather than forcing a JSON parser into the MCU runtime. That is a maintainer call, not a parser change.asset.h:13still misdescribes reality ("At build time, fbuild scans<sketch>/data/for*.lnkfiles") — truer now than before, but worth a pass.Leaving open for those.
feat(examples): mirror the AudioUrl track
fix(examples): give AudioUrl's .lnk a verifiable digest (#4012)
docs(asset): correct what actually emits the asset manifest (#4013)
zackees commentedon Aug 23, 2026
Resolved — both parsers read both formats, and the one file in the wild verifies
The research this issue asked for is done, and every actionable item from it has landed.
What was actually wrong
Wider than the issue first described. Three implementations of the text format, all live in the shipping product:
src/fl/stl/url.h—fl::parse_lnk(),parse_lnk_with_metadata()src/fl/asset/asset.h—FL_ASSET("data/track.mp3")ci/compiler/asset_scanner.py— emits the WASM manifest…against one implementation of JSON in fbuild. The single
.lnkcommitted to FastLED was text, so the tool namedlnkcould not read it.Also corrected along the way:
sha256is required in fbuild's schema, not optional; there is anextractfield (file/zip/tar.gz) with no text equivalent; andformat.rsalready documented a deliberate choice of JSON over the git-LFS pointer style, so "git-LFS argues for text" was settled ground rather than new input.Landed
.lnk(sniffs{-> JSON, else text)assets.jsonexamples/AudioUrlgains a verifiable digest, mirrored off a third-party hostasset.hstops attributing the manifest to fbuildsha256stays mandatory in fbuild even for the text form. The resolver caches by content digest, so a.lnkwithout one cannot be content-addressed or verified — relaxing that to accommodate the text format would surrender the guarantee the cache is built on. The error names the file and the fix instead.A concrete demonstration of why this mattered
While adding the digest to
examples/AudioUrl, I converted its.lnkto fbuild's JSON schema — the obvious "tidy it up" move.fl_asset_assetfailed:fl::parse_lnkreads only the text form, so it took{as the URL. Someone was always going to do that; now the assertion carries a comment explaining why the format cannot change there.Not resolved here — spun out
Whether the two
.lnkroles should keep sharing an extension long-term is a design decision rather than a defect, and it needs a maintainer. My recommendation, recorded above: give them distinct extensions —.lnkfor the runtime asset link, something else for build-time blob pointers — rather than forcing a JSON parser into the MCU runtime, wherefl::parse_lnkis deliberately allocation-light.Filed as #1369 so it is not blocked behind an issue whose work is finished.
Closing.