Distributable Obtainium configurations for Meshtastic for Android, written to the format Obtainium and the crowdsourced config site expect. Setup instructions and channel reference live in docs/en/developer/test-builds.md; this directory holds the machine-readable artifacts.
| File | What it is | Generated? |
|---|---|---|
generate-links.py |
Source of truth for every channel/flavor config | — |
meshtastic-obtainium-export-google.json |
Import/Export → Obtainium import file: google release + both snapshots |
✅ |
meshtastic-obtainium-export-fdroid.json |
Same, with the fdroid release |
✅ |
| the deep-link table in test-builds.md | obtainium://app/… one-tap links, every channel × both flavors |
✅ |
| the deep-link table in README.md | same links, narrowed to latest release + open beta × both flavors | ✅ |
com.geeksville.mesh.json |
Submission file for apps.obtainium.imranr.dev | hand-maintained |
generate-links.py writes the three generated targets from one CHANNELS ×
FLAVORS definition, so the links and the import files cannot disagree:
python3 obtainium/generate-links.py # regenerate
python3 obtainium/generate-links.py --check # verify, exits 1 on driftRun --check after editing anything in this directory. Editing a generated file
by hand is always wrong — change generate-links.py and regenerate.
Obtainium keys apps by application ID, and that governs the whole layout:
| Application ID | Coexist? | |
|---|---|---|
| Release builds — stable, open, closed, bleeding edge, either flavor | com.geeksville.mesh |
No, one only |
Snapshot google |
com.geeksville.mesh.google.debug |
Yes |
Snapshot fdroid |
com.geeksville.mesh.fdroid.debug |
Yes |
So each import file carries exactly one release entry — hence one file per
flavor — plus both snapshots. Import is saveApps() keyed by id, so two
release entries in one file would silently overwrite rather than error. The beta
channels are deliberately left to the deep links for the same reason.
Only the snapshot labels carry a flavor suffix (Meshtastic Snapshot (fdroid)),
because only those can coexist and need telling apart. Labels are set via
appName, not name: App.finalName is additionalSettings['appName'] ?? name,
and without appName Obtainium falls back to the installed app's own label — a
debug build shows up as "Google Debug".
com.geeksville.mesh.jsonis a byte-identical mirror of what we submitted upstream as apps.obtainium.imranr.dev#1566 (public/data/apps/complex/com.geeksville.mesh.json). Change it here and upstream together, or the config site and this repo will disagree. Their formatting conventions are load-bearing for review: 4-space indent, trailing newline, top-level key orderconfigs, icon, categories, description, config key orderid, url, author, name, additionalSettings, altLabel(altLabellast), and short lowercasealtLabels. Verify withdiffagainst their tree rather than by eye. No formatter in this repo touches it — spotless only targets*.ktand*.gradle.kts.
An Obtainium app configuration is a flat JSON object. Four keys are required:
| Key | Value |
|---|---|
id |
Android package name — com.geeksville.mesh (debug snapshots: com.geeksville.mesh.<flavor>.debug) |
url |
https://github.com/meshtastic/Meshtastic-Android — this is what selects the GitHub source |
author |
meshtastic |
name |
Display name |
Everything else goes in additionalSettings, which is a JSON string nested
inside the JSON — its quotes and backslashes are escaped one extra level. Any
key you omit takes its default, so only set what you need. The GitHub source
accepts includePrereleases, fallbackToOlderReleases,
filterReleaseTitlesByRegEx, filterReleaseNotesByRegEx, verifyLatestTag,
sortMethodChoice, useLatestAssetDateAsReleaseDate and
releaseTitleAsVersion, on top of the source-agnostic keys (apkFilterRegEx,
versionExtractionRegEx, versionDetection, autoApkFilterByArch, trackOnly,
and so on).
Three distribution shapes, all carrying the same object:
- Config site submission —
{ "configs": [ …, … ], "icon", "categories", "description" }. Aconfigsarray (with per-entryaltLabel) is the "complex" form used when an app needs more than one variant; a singleconfigobject is the "simple" form.categoriesmust come frompublic/data/categories.json—messagingfor us. - Import/export file —
{ "apps": [ … ], "settings": null }, one entry per application ID (see above). The legacy shape withoutschemaVersionis deliberate: Obtainium reads it through theschemaVersion-absent branch, which avoids inventingexportedAt/appVersionprovenance for a file no Obtainium install actually produced. - Deep link —
obtainium://app/<percent-encoded config JSON>adds the app with settings baked in.obtainium://add/<url>only prefills the Add-App page. Wrapping it ashttps://apps.obtainium.imranr.dev/redirect.html?r=obtainium://app/…keeps the link clickable in places that won't linkify a custom scheme.
apkFilterRegEx— each release attaches the google-flavor APK, several fdroid-flavor APKs, an.aab, and the desktop installers. Obtainium ignores non-APK assets on its own, but the flavors still need disambiguating: both share thecom.geeksville.meshpackage name, so the filter must select exactly one flavor. The fdroid filter deliberately matches every ABI split and letsautoApkFilterByArchchoose, so a new ABI doesn't need a config change.autoApkFilterByArch: trueis set explicitly even though it is the form default.appJSONCompatibilityModifierscoerces the key tofalsewhen it arrives unset, and an unsetpreferredApkIndexbecomes0— together those would silently pick whichever split happens to be first.- No
versionExtractionRegEx— see Version detection. It only rewrites the version parsed from the release, not the installedversionNamethat actually fails to parse, so it cannot fix reconciliation — and it converts a quiet degradation into a hardNoVersionErrorwhenever it fails to match. - Snapshot uses date-based pseudo-versioning — the snapshot tag never moves
off the literal string
snapshot, so there is no version to compare;releaseDateAsVersionplususeLatestAssetDateAsReleaseDatetracks it by asset date instead. These keys are already in Obtainium's post-migration form, so_migrateVersionDetectionFormatpasses them through untouched. fallbackToOlderReleasesis left at its default (true) on the channel-pinned configs, deliberately. It reads like a strictness knob but isn't:_selectGitHubTargetReleaserunsif (!fallbackToOlderReleases && i > prereleaseSkipped) break;, andprereleaseSkippedstays0whenincludePrereleasesis on — so with fallback off, only the release at index 0 is ever considered and a title filter can never skip past it. Setting it tofalsemade the snapshot and beta configs fail outright withNoReleasesError. The title filter alone pins the channel.
apps.obtainium.imranr.dev is a crowdsourced repo that takes pull requests. Meshtastic is not listed there yet.
- Read
APP_CRITERIA.md. We qualify: official upstream source, no fork, no reupload mirror. - Copy
com.geeksville.mesh.jsontopublic/data/apps/complex/com.geeksville.mesh.jsonin a fork of that repo. npm i && npm run dev(Node 22) and confirm the entry renders and its install link works.- Open the PR.
Only the stable configs belong there — the criteria ask for the minimum set of variants and for defaults to be left alone, so the beta and snapshot channels stay in this repo's own docs.