Ecosystem
Saga projects can pull in code from three places: the Hex package
registry (BEAM ecosystem packages), git repositories, and
local filesystem paths. All three are declared in project.toml under a
[deps] section, fetched and compiled by saga install, and pinned by a
saga.lock lockfile.
Declaring Dependencies
[project]
name = "my-app"
[bin]
main = "Main.saga"
[deps]
base64url = { version = "1.0.1" } # Hex
saga_csv = { git = "https://github.com/dylantf/saga_csv" } # git
mathlib = { path = "../mathlib" } # pathHex is the default source — if a dep entry has no path or git field, it's
treated as a Hex package and the dep key is the Hex package name.
Hex Packages
Hex is the package registry for the BEAM ecosystem (Erlang and Elixir). Hex packages are compiled to BEAM bytecode and available on the code path. They are not typechecked by Saga — to call into them you write FFI declarations (see Interop).
[deps]
base64url = { version = "1.0.1" }
jason = { version = "1.4.0" }
argon2 = { version = "1.2.0" }saga install downloads the tarball from repo.hex.pm, extracts it, compiles
it, and installs the result into your project's deps/{name}/ directory.
Pure Erlang vs NIFs
Hex packages are compiled with one of two strategies:
- Pure Erlang packages (no native code): compiled directly with
erlc. Fast, no extra tools needed. - Packages with NIFs or build hooks: detected by the presence of
c_src/,native/, orpre_hooks/port_specsin the package'srebar.config. These are compiled withrebar3 bare compile, which handles native code (C, Rust, etc.) via the package's build hooks.
NIF packages require rebar3 on your PATH:
mise install rebar3If saga install reports a missing rebar3, the failing package needs it.
Version requirements
For now, Hex deps use exact versions. Transitive dependencies from Hex
packages may specify ~> requirements (e.g., ~> 1.0), which are resolved
to the latest compatible version automatically.
Git Dependencies
For libraries not on Hex, or for your own projects:
[deps]
math = { git = "https://github.com/someone/math-lib", tag = "v1.0.0" }
utils = { git = "https://github.com/someone/utils", branch = "main" }
http = { git = "https://github.com/someone/http", rev = "abc123f" }Specify exactly one of tag, branch, or rev. If none is given, the
default is HEAD.
A git dependency must be a Saga project (have a project.toml with a
[library] section). See Project & Modules for
how to declare a library.
Path Dependencies
For local libraries during development:
[deps]
mathlib = { path = "../mathlib" }Path dependencies must also have a project.toml with a [library] section.
This is the most direct way to develop a library and consumer in tandem
without publishing or pushing.
Aliasing with as
Both path and git deps support as to remap the library's module prefix:
[deps]
http = { path = "deps/http", as = "Net" }If the dep declares module = "HTTP" in its [library] section and the
consumer specifies as = "Net", then HTTP.Client becomes Net.Client in
the consumer's code. Useful when two deps would otherwise collide on a name,
or when you want to use a shorter local name.
The Lockfile
saga install writes a saga.lock file that pins each dependency to an
exact resolved state, so subsequent builds are reproducible:
# saga.lock (auto-generated, do not edit by hand)
[deps.math]
git = "https://github.com/someone/math-lib"
ref = "v1.0.0"
commit = "abc123def456789..."
[deps.base64url]
hex = "base64url"
version = "1.0.1"
checksum = "f9b3add4731a02a9..."Workflow:
saga install— resolve all deps, writesaga.lock, install intodeps/.- Subsequent
saga build/saga run— use pinned versions from the lock, skip resolution. saga update— re-resolve refs (e.g., follow a branch to its latest commit, pick up new Hex versions), write a new lockfile.
Commit saga.lock to version control so collaborators and CI build against
the same exact dependencies.
Transitive Dependencies
If your dep has its own deps, they're handled differently depending on source:
- Hex transitives are resolved and installed automatically. If
base64urldeclares its own Hex requirements, those are fetched and compiled bysaga installalong withbase64urlitself. - Path and git transitives are not automatically exposed to your
project. They're compiled (since they're needed at runtime) but their
modules don't appear in your import resolution. To use them, the
intermediate dep must list them in its
[library].expose, or you must add them to your own[deps].
This prevents transitive implementation details from leaking into your project's namespace.
Collision Detection
If two deps expose the same module name (after as aliasing), the compiler
errors and asks you to add an as alias to one of them.
Build Output
After saga install and a build, your project layout looks like:
my-app/
project.toml
saga.lock
src/
Main.saga
deps/
base64url/
ebin/ # compiled .beam files
priv/ # package assets, if any
saga_csv/
_build/ # compiled Saga library output
_build/
dev/ # your project's compiled .beam files
.stdlib/ # precompiled stdlib (per project, keyed by compiler version)
To reinstall everything from scratch, delete deps/ and run saga install
again. Source downloads are cached globally to avoid re-downloading:
- Hex tarballs:
~/.saga/cache/hex/ - Git repos:
~/.saga/cache/git/(bare clones, shared across projects)
Wrapping Hex Packages
Hex packages are opaque to the type system. To call into them from Saga, use
@external to declare typed wrappers around their Erlang functions:
@external("erlang", "base64url", "encode")
pub fun encode : String -> StringWhen the Erlang function's calling convention doesn't map cleanly, write a small Erlang bridge file. Bridge files can call Hex deps directly because Erlang module calls are late-bound (resolved at runtime, not compile time):
%% argon2_bridge.erl
-module(argon2_bridge).
-export([hash/1]).
hash(Password) ->
{ok, Hash} = argon2:hash(Password),
Hash.
@external("erlang", "argon2_bridge", "hash")
pub fun argon2_hash : String -> StringSee Interop for the full FFI story, including type representations and the limitation on effectful callbacks.