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 --versionIf the repository uses mise to pin them, install and activate that configuration first:
mise install
mise exec -- elixir --version
mise exec -- mix --versionFetching 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 --forceMaterialize 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-lockedRun 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. A project whose entire active
dependency graph consists of local path dependencies also works without a
lockfile.
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.
Inspect the Derived Graph#
From the directory that contains mix.exs, inspect the graph:
once query workspace
once query targetsNo once.toml is required. From the directory containing mix.exs, the
usual build and test commands select that project automatically:
once build
once testIn a Git repository, these commands use the nearest repository root as the
filesystem boundary while discovering only the current Mix project family.
This keeps sibling path dependencies inside the build and avoids resolving
unrelated native projects elsewhere in the repository. Outside a Git
repository, the project directory is the boundary; use a containing workspace
with an explicit target when dependencies live outside it. Neither command
writes a manifest. once build follows MIX_ENV, defaulting to dev, while
once test compiles and executes the test environment. The same boundary
applies to once run, once lint, and --all. Explicit targets matching
another native project marker in the current directory remain selectable.
Repository-root commands without an explicit native target retain ordinary
workspace discovery. An explicit package-relative native target selects its
owning project and target family.
An explicit once.toml between the project and repository root takes priority.
Native compilation conservatively hashes non-hidden project files outside generated build and dependency directories. This includes compiler resources, such as a version file or native sources outside the Elixir source directories. Changing one of these files invalidates compilation; unchanged inputs reuse the cached output. Native Mix bytecode cache keys also include the source location, since compiled modules can embed directory-sensitive resources. Tests use structured ExUnit events for totals, so summary-looking output from a test cannot rewrite the result. Invalidated tests count as failures, and excluded tests do not count as executed tests.
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_releaseThe 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. Native tests retain the project's
test aliases, configured test paths, and test file patterns. Once compiles
application and test-support code first, then runs the aliases and tests against
those outputs without recompiling dependencies or invoking project compilers
again.
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, dependency compilation, and release
assembly are cacheable. Automatically discovered tests run on every invocation
and inherit the host runtime environment, including host tools on PATH.
This matches ordinary Mix tests that depend on databases and other services.
Explicit test targets can opt into caching when their inputs are fully declared.
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_devThe 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.
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.
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.exsThe 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_testThe library exposes build. The test target exposes test. Neither target
exposes run.
Build and Test#
Build the application bytecode:
once build apps/greeting/greetingFor 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_testTest 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 testsmix_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.serverFor 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 = trueAssemble 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 releaseInspect 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.