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.

main.adm
@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:

OSFolder
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:

~/.local/share/myapp
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 to backup/<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:

main.adm
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.
  • to may be omitted: it then means the next declared step's from, 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.