← All posts

Cure v0.26.0 :: Applications and Releases

by Aleksei Matiushkin

release applications otp releases systools

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:

  1. A new app container (app MyApp) that declares the project's OTP application directly in Cure source.
  2. Two new Cure.toml sections, [application] and [release], that describe the application-level and release-level metadata -- exactly the information that .app files and release boot scripts need.
  3. A new cure release subcommand (also mix 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          = %&lbrace;port: 4000&rbrace;
  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, and env are top-level assignments in the container body. They override the corresponding entries in [application] (applications is merged rather than replaced).
  • on_start and on_stop reuse the actor / FSM callback-clause grammar. Their bodies become the generated module's start/2 and stop/1 callbacks.
  • Each on_phase :name block introduces a single three-argument clause (phase_args, start_kind, start_args) feeding the generated start_phase/3 callback.

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].name is the source of truth for the emitted <name>.app resource.
  • [application].start_phases is authoritative. Every entry must have a matching on_phase :name clause in the container, and vice versa. Mismatches surface as E053 Start Phase Mismatch.
  • [application].applications is merged with the container's own applications = [...] 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 app container in a project, or a name mismatch with [application].name.
  • E052 Missing Application -- cure release invoked with no app declared.
  • 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/2 or 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          = %&lbrace;idle_timeout_ms: 5000, greeting: "forge ready"&rbrace;
  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 %&lbrace;requests: 0, errors: 0&rbrace;
  on_message
    (:inc_requests, state) -> %&lbrace;state | requests: state.requests + 1&rbrace;
    (:inc_errors,   state) -> %&lbrace;state | errors:   state.errors   + 1&rbrace;
    (: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.