Cure v0.26.0 :: Applications and Releases
by Aleksei Matiushkin
v0.25.0 gave Cure everything it needed to write supervision trees: typed actor containers, verified sup containers, the Melquiades Operator for typed sends, and a stdlib surface exposing the runtime (Std.Actor, Std.Process, Std.Supervisor). What it did not give you was a way to describe the application that owns the tree. You still had to write an Elixir Application callback module and mix.exs, then wire up the compiled Cure.Sup.Foo as one of its children.
v0.26.0 closes that loop.
The shape of the release
Three pieces land together:
- A new
appcontainer (app MyApp) that declares the project's OTP application directly in Cure source. - Two new
Cure.tomlsections,[application]and[release], that describe the application-level and release-level metadata -- exactly the information that.appfiles and release boot scripts need. - A new
cure releasesubcommand (alsomix cure.release) that packages the compiler output as a bootable BEAM release under_build/cure/rel/<name>/.
As always, Std.App brings the runtime half into Cure source: a thin wrapper over :application that returns plain atoms and values instead of OTP's tagged tuples.
The app container
Here is the full grammar:
app MyApp
vsn = "0.1.0"
description = "My humble application"
root = sup MyApp.Root
applications = [:logger, :crypto]
env = %{port: 4000}
on_start
(start_kind, args) -> do_start(start_kind, args)
on_stop
(state) -> cleanup(state)
on_phase :init
(args, _kind, _start_args) -> init_phase(args)
on_phase :warm_cache
(_args, _kind, _start_args) -> Std.Cache.warm()
vsn,description,root,applications,included_applications,registered, andenvare top-level assignments in the container body. They override the corresponding entries in[application](applicationsis merged rather than replaced).on_startandon_stopreuse the actor / FSM callback-clause grammar. Their bodies become the generated module'sstart/2andstop/1callbacks.- Each
on_phase :nameblock introduces a single three-argument clause(phase_args, start_kind, start_args)feeding the generatedstart_phase/3callback.
The container compiles to a loaded BEAM module named :"Cure.App.<Name>" that uses Application. With --output-dir, the bytecode and the matching <name>.app resource file are persisted alongside every other Cure module. The module is never started automatically by the compiler; it is started by the OTP boot script when the release boots, or manually via Std.App.ensure_all_started/1.
Root resolution
root = ... accepts the same four forms as sup child specs:
root = sup Name->:"Cure.Sup.Name"(soft-keyword form).root = Name->:"Cure.Sup.Name"(PascalCase identifier).root = App.Sub.Root->:"App.Sub.Root"(dotted path used verbatim).root = :my_app_sup->:my_app_sup(atom literal; escape hatch).
A phase-only app (one that only runs its on_phase :init and returns) can omit root entirely; start/2 then simply returns {:ok, self()}.
Single-app enforcement
Cure.Project.compile_project/2 scans every .cure file under lib/ and refuses to compile when there is more than one app container:
error: duplicate application (E051)
--> Cure.toml
| more than one `app` container in the project:
| lib/foo_app.cure -> app Foo
| lib/bar_app.cure -> app Bar
The same diagnostic fires when the container's name does not match [application].name in Cure.toml. Both names are normalised through Macro.underscore/1, so app MyApp matches name = "my_app".
Cure.toml: [application] and [release]
[application]
name = "my_app"
vsn = "0.1.0"
description = ""
applications = ["logger", "crypto"]
included_applications = []
start_phases = ["init", "warm_cache"]
[application.env]
port = 4000
[release]
name = "my_app"
vsn = "0.1.0"
include_erts = false
applications = ["logger"]
vm_args = "rel/vm.args"
sys_config = "rel/sys.config"
Notable rules:
[application].nameis the source of truth for the emitted<name>.appresource.[application].start_phasesis authoritative. Every entry must have a matchingon_phase :nameclause in the container, and vice versa. Mismatches surface asE053 Start Phase Mismatch.[application].applicationsis merged with the container's ownapplications = [...]list.- The TOML parser accepts a minimal subset: scalar string / integer / bool / array-of-strings values, plus nested tables for
[application.env].
cure release
Once the project compiles cleanly:
cure release
# or: mix cure.release
produces:
_build/cure/rel/my_app/
lib/<app>-<vsn>/ebin/*.{beam,app} # every included app
releases/<vsn>/<name>.rel
releases/<vsn>/start.boot
releases/<vsn>/start.script
releases/<vsn>/sys.config
releases/<vsn>/vm.args
bin/<name> # POSIX runner script
The runner script uses ${ERL:-erl} so the release can be tested against any Erlang VM on PATH. Pass --include-erts (or set [release].include_erts = true) to bundle ERTS into the release directory itself; the resulting tree is then fully self-contained.
Application closure
Cure.Release seeds the boot script's application closure with :kernel, :stdlib, :compiler, :elixir, the project's own application atom, and every entry in [release].applications. Out-of-tree dependencies must be loaded by the calling VM (typically by Mix when cure release runs through mix cure.release); the closure is read from the live code path.
Std.App
From Cure source, the application lifecycle is reached through Std.App:
use Std.App
fn boot() -> Atom = Std.App.ensure_all_started(:my_app)
fn port() -> Int = Std.App.get_env(:my_app, :port, 4000)
The full surface is nine functions: ensure_all_started/1, start/1, stop/1, get_env/2, get_env/3, put_env/3, which_applications/0, loaded_applications/0, start_phase/3. Each is a thin wrapper over :application that returns plain atoms and values rather than OTP's {ok, _} tuples.
Error catalog additions
Five new codes for the application surface. Run cure explain <code> for the full text with examples:
- E051 Duplicate Application -- more than one
appcontainer in a project, or a name mismatch with[application].name. - E052 Missing Application --
cure releaseinvoked with noappdeclared. - E053 Start Phase Mismatch -- TOML and container disagree on phase names.
- E054 Unresolved Root Supervisor --
root = ...does not resolve to a known module reference. - E055 Release Build Failed -- wraps
:systools.make_script/2or release-write I/O errors.
examples/cure_forge/
The canonical end-to-end showcase that ships with the release. A small Mix project that wires an OTP application on top of four cooperating actors:
app CureForge
vsn = "0.1.0"
description = "Cure forge showcase: a typed OTP application"
root = sup Forge.Root
applications = [:logger]
env = %{idle_timeout_ms: 5000, greeting: "forge ready"}
on_start
(_kind, _args) -> :ok
on_stop
(_state) -> :ok
on_phase :warm_cache
(_args, _kind, _start_args) -> :ok
sup Forge.Root
strategy = :one_for_one
intensity = 5
period = 10
children
Metrics as metrics
Logger as logger (restart: :permanent, shutdown: 2000)
Queue as queue (restart: :transient)
Pool as pool (restart: :permanent)
actor Metrics with %{requests: 0, errors: 0}
on_message
(:inc_requests, state) -> %{state | requests: state.requests + 1}
(:inc_errors, state) -> %{state | errors: state.errors + 1}
(:get, state) ->
notify(%[:metrics, state])
state
The root supervisor is started as the root of the application. Each actor owns a narrow responsibility:
- Metrics counts requests and errors and reports snapshots.
- Logger buffers log lines under a soft cap and exposes a drain handler.
- Queue enqueues work requests for the pool and fans out messages.
- Pool is a small worker pool that receives tasks from the queue and reports their completion to the metrics actor via
pid <-| :inc_requests.
The accompanying CureForge Elixir facade exposes the running tree to iex -S mix and to the ExUnit suite, so you can observe the application booting, exercise every actor, watch the supervisor restart a killed worker, and confirm that the :warm_cache start phase executed before the first request.
The project's mix.exs uses mod: {CureColony.Application, []}-style bootstrap in tests, and cure release can produce the standalone release under _build/cure/rel/cure_forge/. Read the Applications reference for the tour, or docs/APP.md for the on-disk reference.
Getting started
git clone https://github.com/am-kantox/cure-lang.git
cd cure
mix deps.get && mix test
mix escript.build
./cure version # Cure 0.26.0
./cure new myapp --app # scaffold an application project
cd myapp
./cure release # build a bootable release
Then jump into examples/cure_forge/ or read the new Applications page for the tour, docs/APP.md for the on-disk reference, and the CHANGELOG for the full changeset.
What's next
The long-term radar from v0.24.0 / v0.25.0 is unchanged: monomorphisation, profile-guided optimisation, broader IDE reach, REPL-level hot reload. Each remains on the roadmap without a fixed slot. The repository is at github.com/am-kantox/cure-lang.