SagaSaga
Guide

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" }                                  # path

Hex 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/, or pre_hooks / port_specs in the package's rebar.config. These are compiled with rebar3 bare compile, which handles native code (C, Rust, etc.) via the package's build hooks.

NIF packages require rebar3 on your PATH:

mise install rebar3

If 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, write saga.lock, install into deps/.
  • 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 base64url declares its own Hex requirements, those are fetched and compiled by saga install along with base64url itself.
  • 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 -> String

When 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 -> String

See Interop for the full FFI story, including type representations and the limitation on effectful callbacks.