Elixir
Once can read an existing mix.exs, derive a typed build graph, and cache the
compiled project and each locked dependency separately. You can build, lint,
test, run Mix tasks, and assemble a release without first translating the
project into once.toml.
Start With an Existing Mix Project#
Check the Toolchain#
Once invokes elixir, erl, and mix from the selected toolchain. Confirm
that the versions expected by the project are available:
elixir --version
mix --version
If the repository uses mise to pin them, install and activate that configuration first:
mise install
mise exec -- elixir --version
mise exec -- mix --version
Fetching packages from Hex requires a local Hex installation. A dependency whose build manager is Rebar also requires Rebar 3:
mix local.hex --force
mix local.rebar --force
Materialize Locked Dependencies#
Once does not download dependencies or change mix.lock. If the project has a
current lockfile, materialize its exact sources before asking Once to load the
graph:
mix deps.get --check-locked
Run mix deps.get without --check-locked once when the lockfile needs to be
created or deliberately updated. Review that change before continuing. A
dependency-free project does not need mix.lock.
Once currently requires a concrete lockfile when a project declares external
dependencies. Library maintainers who do not commit mix.lock can generate
one locally before using Once, but repeatable builds on another machine require
the same lockfile.
Preview the Derived Graph#
From the directory that contains mix.exs, inspect the match and the graph:
once query native-projects
once query native-project mix
once query targets
No once.toml is required, and these commands do not write one. If the
workspace contains several Mix projects, select one explicitly:
once query native-project mix --package apps/accounts
The identifiers printed by once query targets are the source of truth. For a
Mix project at the workspace root, discovery normally derives:
mix_application_dev,mix_application_test, andmix_application_prodcompile the first-party project in each environment;- one dependency target per environment when
mix.lockis present; mix_lintchecks unused dependencies and formatting;mix_testsruns the project tests when the project has files undertest/;mix_releaseassembles a production release;mixbuilds the default development application.
Nested projects use package-qualified identifiers such as
apps/accounts/mix_tests.
Build, Lint, Test, Run, and Release#
Use the derived targets through the same commands as every other Once graph:
once build mix
once run mix_lint
once test mix_tests
once run mix_application_dev -- phx.server
once build mix_release
The argument after -- is a Mix task selected at invocation time.
phx.server is only an example; native discovery does not assume that the
project uses Phoenix or another framework.
mix_lint runs mix deps.unlock --check-unused and
mix format --check-formatted. It does not infer project-specific tools such
as Credo. The run target is intentionally uncached for servers and interactive
tasks. Builds, the derived lint target, tests, dependency compilation, and
release assembly are cacheable.
mix_release is useful for applications that support mix release. A library
can still build, lint, and test even when producing a release is not meaningful.
Framework-specific preparation such as asset compilation is not inferred. Add
an explicit release target with pre_tasks when the release requires it.
Confirm Caching#
Run the same build twice without changing an input:
once build mix_application_dev
once build mix_application_dev
The second invocation should restore unchanged actions from the configured
cache. Outputs are materialized under .once/out/. Changing one dependency
invalidates that package and its consumers, changing application source
invalidates the application, and changing only a test file reruns the tests
without recompiling the application.
Dependency fetching is outside the Once graph. Once caches compilation after
deps/ contains the sources selected by mix.lock. Continue with
Caching to configure a shared remote cache.
Record the Project Selection#
Initialization is optional. Use it when the repository should make its native project selection explicit:
once edit init-native-project mix
This writes only the mix_workspace seed. The detailed targets remain derived
from mix.exs and mix.lock, so they do not become duplicated configuration.
Commit the seed if reviewers and continuous integration should see the
selection. Pin the same Once version in continuous integration, materialize
locked dependencies, and run the same target commands used locally.
Umbrellas, Nested Projects, and Path Dependencies#
Discovery preserves workspace-relative source roots for path dependencies and
recognizes nested Mix projects. An umbrella root builds its detected child
workspaces. Use once query targets to find each package-qualified target
rather than assuming that every target lives at the root.
External Hex and Git dependencies must be present under deps/. Mix and Rebar
3 packages are supported. Packages that require Make or a custom dependency
compile command fail with an explicit diagnostic instead of silently falling
back to an opaque build.
Troubleshooting#
- If graph loading reports a missing lockfile or dependency source, run
mix deps.get, then retry withmix deps.get --check-locked. - If
mix_testsis absent, check whether the project has files undertest/. Themix_application_testtarget is still available for a test-environment compile. - If release assembly needs
assets.deployor another preparation task, declare amix_releasetarget withpre_tasks. Once does not infer framework conventions. - Native project evaluation requires Erlang 27 or newer.
- Use
once query native-project mix --package <path>when more than onemix.exsmatches the workspace.
Author a Graph When You Need More Control#
Direct mix.exs discovery is the default for an existing Mix project.
Hand-authored targets are useful for a standalone library without Mix, custom
lint or release tasks, database setup, or a boundary that should be cached
independently.
Declare a Standalone Library and Test#
Create apps/greeting/once.toml:
[[target]]
name = "greeting"
kind = "elixir_library"
srcs = ["lib/**/*.ex"]
[target.attrs]
app_name = "greeting"
mix_env = "test"
[[target]]
name = "greeting_test"
kind = "elixir_test"
srcs = ["test/**/*.exs"]
deps = ["./greeting"]
[target.attrs]
labels = ["unit"]
Use this source layout:
apps/greeting/
├── once.toml
├── lib/
│ └── greeting.ex
└── test/
├── test_helper.exs
└── greeting_test.exs
The library uses mix_env = "test" because elixir_test requires exactly one
dependency that provides a complete elixir_app compiled for the test
environment. The test target consumes the existing bytecode instead of
compiling the application a second time.
Query Before Building#
Inspect the declared targets and their contracts:
once query targets --kind elixir_library
once query capabilities apps/greeting/greeting
once query capabilities apps/greeting/greeting_test
once query schema elixir_library
once query schema elixir_test
The library exposes build. The test target exposes test. Neither target
exposes run.
Build and Test#
Build the application bytecode:
once build apps/greeting/greeting
For this target, the staged bytecode and application metadata appear under
.once/out/apps/greeting/greeting/ebin. Private application files appear
under .once/out/apps/greeting/greeting/priv. Compile metadata and persisted
warnings live beside them as compile-metadata.tsv and
compile-warnings.log.
Run the tests against that compiled library:
once test apps/greeting/greeting_test
Test results and logs appear under
.once/out/apps/greeting/greeting_test/test/, including
test_results.json, elixir-test.log, and native_results.txt.
Changing only a test file reruns the test without recompiling the library. Changing a library source invalidates the library build and the dependent test. Configuration files, private files, data, and supported dynamic compile inputs can also participate in cache invalidation when declared by the target.
Add Dependencies Deliberately#
Compile-time dependencies such as macros or structs used from another target
should be separate elixir_library targets connected through deps. This
keeps each compiled application queryable and lets Once rebuild the affected
part of the graph.
The library target does not require mix.exs. Set mix_config only when the
project file should affect the build or when tests must run through mix test.
In that mode, Once still uses bytecode compiled by the library target and runs
Mix with dependency checks and compilation disabled.
For a first-party project that needs its registered Mix compiler pipeline,
declare mix_project instead of elixir_library:
[[target]]
name = "dependencies_test"
kind = "mix_dependencies"
srcs = ["mix.exs", "mix.lock"]
[target.attrs]
resolver_inputs = [
"mix.exs",
"mix.lock",
"deps/*/mix.exs",
"deps/*/rebar.config",
]
mix_env = "test"
target_prefix = "mix-test"
[[target]]
name = "application_test"
kind = "mix_project"
srcs = ["lib/**/*", "test/support/**/*"]
deps = ["./dependencies_test"]
[target.attrs]
app_name = "greeting"
mix_env = "test"
manifest = "mix.exs"
compile_args = ["--warnings-as-errors"]
[[target]]
name = "tests"
kind = "elixir_test"
srcs = ["test/**/*.exs"]
deps = ["./application_test"]
[target.attrs]
cacheable = false
setup_tasks = [
["ecto.create", "--quiet", "--no-compile", "--no-deps-check"],
["ecto.migrate", "--quiet", "--no-compile", "--no-deps-check"],
]
data = ["priv/repo/migrations/**/*"]
Fetch the exact dependency sources before loading the graph:
mix deps.get --check-locked
once build application_test
once test tests
mix_project runs the first-party compiler pipeline with dependency checking
and dependency compilation disabled. Each dependency is already represented by
an elixir_app provider. A manifest can set run_task, or the caller can
supply a task without coupling the discovered graph to a framework convention:
once run mix_application_dev -- phx.server
For third-party packages managed by Mix and Hex, declare one
mix_dependencies target. Mix evaluates the active dependency graph from
mix.exs, while the committed mix.lock supplies the exact version, source,
and checksums. Once emits a synthetic mix_package target for each active
locked package and preserves Mix's dependency edges.
[[target]]
name = "mix_dependencies"
kind = "mix_dependencies"
srcs = ["mix.exs", "mix.lock", "mix-dependencies.json"]
[target.attrs]
graph_file = "mix-dependencies.json"
resolver_inputs = [
"mix.exs",
"mix.lock",
"mix-dependencies.json",
"local_helper/mix.exs",
]
vendor_dir = "deps"
[target.attrs.path_dependencies]
local_helper = "./local_helper"
[[target]]
name = "local_helper"
kind = "elixir_library"
srcs = ["local_helper/lib/**/*.ex"]
[target.attrs]
app_name = "local_helper"
[[target]]
name = "greeting"
kind = "elixir_library"
srcs = ["lib/**/*.ex"]
deps = ["./mix_dependencies"]
Fetch dependency sources before the Once build and keep them under the
configured vendor directory. Graph loading never runs dependency fetching or
updates the lockfile. With graph_file, ordinary graph loading also avoids
executing Mix. Without it, Once evaluates the project with isolated Mix and Hex
homes and Hex offline mode. Vendored locked packages do not require a Hex
archive to be installed in that isolated home. The live graph path requires
Erlang 27 or newer for its built-in JavaScript Object Notation encoder. The
checked-in snapshot binds the exact mix.exs and mix.lock text together with
the selected mix_env, so a manifest, lockfile, or environment change makes
graph loading fail until the snapshot is regenerated. Its once_inputs map
also binds every exact resolver input except the snapshot itself. Include path
dependency manifests so their edges cannot become stale. Package compilation
sets Hex offline mode. Mix packages use their registered compiler pipeline,
while Rebar packages use Rebar 3 directly. Each dependency uses the compilation
environment reported by Mix, with dependency_mix_env = "prod" as the
fallback. The root configuration is read with the dependency set's mix_env,
so use separate dependency targets and distinct target_prefix values for
development, test, and production graphs.
Projects whose dependencies read compile-time application configuration should declare the full root configuration set:
[target.attrs]
config = ["config/**/*.exs"]
config_entry = "config/config.exs"
The config globs must include every file imported by config_entry.
Map every active Mix path dependency through path_dependencies. Each value is
an ordinary first-party Once target reference. Graph loading fails instead of
silently dropping a path dependency when its application name is not mapped.
Once preserves each mapped target's workspace-relative source root in the
isolated build workspace, so relative paths in mix.exs keep their original
meaning.
The importer accepts locked Hex and Git sources whose build manager is Mix or Rebar 3. Make-only packages and dependency declarations with a custom compile command fail with an explicit error.
Lint and Release#
A mix_project can expose deterministic checks through once run:
[target.attrs]
run_tasks = [
["deps.unlock", "--check-unused"],
["format", "--check-formatted"],
["credo"],
]
run_cacheable = true
Assemble a release from a compiled project with mix_release:
[[target]]
name = "application_prod"
kind = "mix_project"
srcs = ["lib/**/*"]
deps = ["./dependencies_prod"]
[target.attrs]
app_name = "greeting"
mix_env = "prod"
[[target]]
name = "release"
kind = "mix_release"
srcs = ["mix.exs"]
deps = ["./application_prod"]
[target.attrs]
manifest = "mix.exs"
config = ["config/**/*.exs"]
Projects that need preparation can add the ordered pre_tasks documented by
mix_release. Declare any files and
executables used by those tasks through data and tools so they participate
in caching.
Build the release twice to confirm that the second invocation restores the release action from the configured cache:
once build release
once build release
Inspect the imported packages before building the local application:
once query targets --kind mix_package
once build greeting
elixir \
-pa .once/out/mix-locked-greeting/mix/prod/lib/locked_greeting/ebin \
-pa .once/out/local_helper/ebin \
-pa .once/out/greeting/ebin \
-e 'IO.write(Greeting.message("Once"))'
The bundled dependency starter compiles a locked locked_greeting package and
a mapped local helper, then compiles the first-party Greeting module against
both. Evaluating Greeting.message("Once") returns Hello, Once!, which
verifies both provider edges as well as the imported graph.
Target References and Limitations#
elixir_librarydocuments application metadata, configuration, private files, environment, dependencies, and thebytecodeoutput group.mix_dependenciesdocuments locked Mix and Hex graph expansion, vendored sources, and root selection.mix_packagedocuments the generated package target and its locked identity provider fields.mix_projectdocuments first-party Mix compilation and run tasks.mix_releasedocuments isolated, cacheable Mix release assembly.elixir_testdocuments direct ExUnit and Mix test modes, test arguments, labels, timeouts, results, and logs.
An elixir_test must depend on exactly one complete elixir_app provider built
with mix_env = "test". Set cacheable = false for tests that must contact an
external service on every run. Direct ExUnit options cannot be combined with
Mix mode.
Reserved compatibility attributes listed in the references fail validation when
set to non-empty values.
Next#
Build the discovered development target twice and confirm the second build comes from cache. Then run the derived lint and test targets before adding hand-authored targets. Continue with Testing and Scheduling for larger test graphs or Memory to inspect the durable context recorded for builds and tests.