Docs
Application home & migrations
Where an application keeps its files, what the folder looks like, and how it is carried from one version to the next.
Identity
Every application has a name, an id and a version. They come, in order of precedence, from the adm.toml manifest in the build, from @manifest on the application block, or from defaults: the declared application name (the source file name for an anonymous application { }) and version 0.1.
@manifest(name = "myapp", version = "1.2.0")
application MyApp {
def new(args string[]) int {
let home = try Application.home()
...
}
}
Inside the program they are the constants __app_name__, __app_id__ and __app_version__, and Application.name() and version() return the same. Two builds that must coexist on one machine get different names (myapp, myapp-beta); versions never get their own folders.
Where the folder lives
Application.home() (also std.os.appHome()) returns the application's folder and creates it on first use. It sits where the platform keeps per-user application data, named after the app, never as a dotfolder in the home directory:
| OS | Folder |
|---|---|
| Linux | $XDG_DATA_HOME/<name>, default ~/.local/share/<name> |
| macOS | ~/Library/Application Support/<name> |
| Windows | %APPDATA%\<name> |
The environment variable ADM_APP_HOME names the folder directly, for portable installs and tests; adm test sets it to a scratch directory so a check suite never touches the real folder.
A folder that already exists at that path with contents but no version marker belongs to some other program. home() refuses it with a message naming the path and ADM_APP_HOME; it never adopts, overwrites or deletes anything there. An empty folder is adopted.
Layout
One folder shared by every version of the app:
version the last version that ran this folder, e.g. "3.54"
vault/ std.services.Vault, secrets and logins; shared across versions
plugins/ extracted plugin objects, keyed by content hash
data/ the app's own files: configuration, caches, documents it owns
backup/<ver>/ snapshot of data/ taken right before migrating from <ver>
Secrets, plugins, configuration and caches all live under it, so an application never writes anywhere else; in particular never under ~/.adm, which belongs to the toolchain. Sharing one folder rather than one per version keeps logins across patch releases and avoids copying large local data; rollback comes from the snapshot instead.
What the first call does
The folder is brought up to date on the first home() call of a run, not at start-up, so a program that never asks for its folder never gets one. The call reads the version marker and compares it with __app_version__:
- Missing: first run. The marker is written and that is all.
- Equal: nothing to do.
- Older: an upgrade.
data/is copied tobackup/<old>/, the migration steps run, and the marker is set to the new version. - Newer: a downgrade. The call fails naming both versions, unless the application block carries
@allowDowngrade(). Data written by a newer schema is how apps corrupt themselves, so opting in is deliberate.
Versions compare as semantic versions (1.0.0-rc.1 is older than 1.0.0); a version that is not a valid semver falls back to a numeric part-by-part comparison.
Writing migrations
A migration is an ordered list of steps, each knowing both ends, the way database migrations work. Annotate functions in the application block with @migration; each takes the home folder path and returns !none:
application MyApp {
@migration(from = "3.54", to = "4.0")
def settingsToJson(home string) !none {
let ini = try os.readText("{home}/data/settings.ini")
try os.writeText("{home}/data/settings.json", toJson(ini))
try os.remove("{home}/data/settings.ini")
}
@migration(from = "4.0", to = "5.63")
def splitCache(home string) !none { ... }
}
- Only the steps newer than the version that last ran the folder fire, in ascending order. Upgrading 3.54 to 5.64 runs both steps above; 5.63 to 5.64 runs none.
tomay be omitted: it then means the next declared step'sfrom, so gaps between steps are fine.- A step only ever sees the folder as the previous step left it. Code never has to guess where the data came from.
Rollback
Before the first step runs, data/ has been snapshotted to backup/<old>/. If a step fails, data/ is restored from that snapshot, the version marker keeps the old value, and home() returns the step's error; the user can go back to the previous build and everything is as it was.
Backups are kept until the application deletes them, so a manual rollback is always one folder away. vault/ and plugins/ are not part of the snapshot: they are version-agnostic by design.
Testing an upgrade
Point ADM_APP_HOME at a scratch folder holding a copy of an older layout and its version marker, then run the new build: the first home() call performs the upgrade against that copy. adm test does the same automatically with a fresh scratch folder per app, so a check suite can exercise the whole chain with fixture data.