Skip to main content
Version: master

Dependency Management

Leo programs can import functionality from other programs and libraries. Any imported program or library is referred to as a dependency. There are four types of dependencies:

  • Network dependencies: Programs already deployed on the Aleo network, fetched as pre-compiled bytecode.
  • Local dependencies: Code on your filesystem. The code can be compiled Leo code, Aleo Instructions code, or a Leo library.
  • Workspace dependencies: Other members of the same workspace, resolved automatically from workspace.json.
  • Git dependencies: Packages fetched from a git repository (a Leo program, a Leo library, or an Aleo Instructions file), pinned to an exact commit.

Programs vs. Libraries​

A regular Leo program has an on-chain identity (program foo.aleo { }). It can contain mappings and records. You can deploy a program to the Aleo network.

A Leo library is a source-only package with structs, constants, and helper functions. During compilation, Leo puts the library code in each program that uses it. Libraries can only be local or git dependencies. You cannot deploy a library.

See Leo Libraries for details on how to write and use libraries.

Adding Dependencies​

Network Dependencies​

To add a program already deployed on the Aleo network as a dependency:

leo add credits.aleo --network

or

leo add credits --network

For mainnet dependencies:

NETWORK=mainnet leo add credits --network

You can also set NETWORK=mainnet in .env. If you do not use --endpoint, Leo uses ENDPOINT from the environment. Leo makes sure that the program exists on the selected network before it changes program.json. Use --network-retries to set the number of retries.

Leo records the resolved network edition and checksum in leo.lock. Later builds reuse that edition. The built-in credits.aleo program comes from snarkVM and needs no network lock entry. For this built-in program, Leo ignores the manifest edition and uses the bundled edition zero.

This adds an entry to your program.json:

{
"program": "your_program.aleo",
"version": "0.0.0",
"description": "",
"license": "MIT",
"dependencies": [
{
"name": "credits.aleo",
"location": "network",
"path": null,
"edition": null
}
]
}

Local Dependencies​

To add a Leo package or Aleo Instructions file from your local filesystem as a dependency:

leo add my_library.aleo --local <PATH_TO_LIBRARY>

This records the path in program.json:

{
"dependencies": [
{
"name": "my_library.aleo",
"location": "local",
"path": "./path/to/my_library",
"edition": null
}
]
}

Local dependencies are compiled from source whenever you build. They never require network access.

Leo libraries are a special kind of local dependency. A library project (created with leo new --lib) contains only structs, constants, and helper functions — no program block, no mappings, no records. Because libraries have no on-chain identity, they can only be local dependencies and are never deployed. All library code is inlined into the consuming program at compile time. See Leo Libraries for more.

Workspace Dependencies​

Inside a workspace, members can depend on each other without specifying explicit paths. To add a workspace member as a dependency:

leo add --workspace token

or equivalently:

leo add -w token

This records a workspace dependency in program.json:

swap/program.json
{
"program": "swap.aleo",
"version": "0.1.0",
"description": "",
"license": "MIT",
"dependencies": [
{
"name": "token.aleo",
"location": "workspace",
"path": null,
"edition": null
}
],
"dev_dependencies": null
}

At build time, Leo resolves workspace dependencies by looking up the member in workspace.json and converting the entry to a local path automatically. The member is matched by directory name or program name (with or without the .aleo suffix).

Workspace dependencies require an enclosing workspace.json. If no workspace is found, or if the named member is not listed in the workspace, Leo reports an error.

To add a workspace member as a development dependency:

leo add --workspace --dev token

Git Dependencies​

To add a package from a git repository as a dependency:

leo add my_library --git https://github.com/example/my_library

Leo fetches the repository (so it can read the package's manifest and detect whether it is a program or a library, just like --local). By default the repository's default branch is tracked. Pin to a branch, tag, or revision:

leo add my_library --git https://github.com/example/my_library --branch main
leo add my_library --git https://github.com/example/my_library --tag v0.1.0
leo add my_library --git https://github.com/example/my_library --rev 0a1b2c3

This records a git dependency in program.json. For a concrete, working example, the package below depends on helloworld.aleo from the Leo examples repository, pinned to a commit:

program.json
{
"program": "git_dep.aleo",
"version": "0.1.0",
"description": "",
"license": "MIT",
"dependencies": [
{
"name": "helloworld.aleo",
"location": "git",
"git": {
"url": "https://github.com/ProvableHQ/leo-examples",
"rev": "e8d20b8690f42ae0c12c8626aa5dbcbb282a98e3"
}
}
]
}

A git repository can contain a Leo program, a Leo library, or an Aleo Instructions (.aleo) file. Leo uses the dependency name to find the package in the checkout. It searches the repository for a program.json with a matching program field. If it does not find one, it searches the repository root for a <name>.aleo bytecode file.

Only public repositories are supported, fetched over HTTP(S). Leo never sends credentials, so private repositories and SSH URLs (git@…) are not supported.

The lock file​

When Leo first resolves a git dependency, it writes a leo.lock file. This file is next to program.json or at the workspace root. It records the exact commit for each git dependency. For the preceding example:

leo.lock
{
"version": 2,
"git": [
{
"name": "helloworld.aleo",
"git": "https://github.com/ProvableHQ/leo-examples",
"reference": "rev=e8d20b8690f42ae0c12c8626aa5dbcbb282a98e3",
"commit": "e8d20b8690f42ae0c12c8626aa5dbcbb282a98e3"
}
]
}

Builds reuse the locked commit for every Git reference, including branches. If the cached checkout is missing, Leo downloads the repository and restores that commit. Commit leo.lock to version control to share exact dependency versions.

Use leo update NAME to update a branch dependency, or leo update to update all eligible dependencies. Packages from the same Git repository and reference update together. Locked tags and revisions stay fixed. A change to branch, tag, or rev in program.json makes Leo resolve the new reference.

Pass --offline to leo build to use only locked Git commits with cached checkouts. For Git dependencies, leo remove also removes the corresponding lock entries.

dependencies vs. dev_dependencies​

A manifest has two dependency lists, and they differ only in what can see them:

  • dependencies are visible to your src program and to the package's in-package tests. A dependency your program is built from is automatically available to the tests too.
  • dev_dependencies are visible only to the in-package tests, never to src. Use this list for libraries or programs that the tests need but the program itself does not.

Tests can already use dependencies. Thus, put a library that src and tests use only in dependencies. Do not repeat it in dev_dependencies. Leo permits and deduplicates a repeated local library, but the entry is redundant.

Manifest field reference​

This reference applies to each dependency kind, not only workspace members. leo add completes these fields. If you edit program.json manually, Leo validates each dependency when it loads the manifest. Leo rejects incompatible field combinations. Each entry has a location, and the other fields depend on it:

locationpathedition
networknot allowedoptional
localrequirednot allowed
workspacenot allowednot allowed
gitnot allowednot allowed

The manifest does not select a network. Commands use their network option or the NETWORK environment variable. leo add --network selects the dependency source; use NETWORK to select its verification network. Network lock entries are stored separately for each network.

The same rules apply to entries in dev_dependencies. workspace entries are looked up in workspace.json and resolved to a local path automatically. git entries additionally take a git object with a url and at most one of branch/tag/rev.

Removing Dependencies​

leo remove credits.aleo

Using Dependencies​

In your main.leo file, import dependencies before the program declaration:

import credits.aleo;
import my_library.aleo;

program my_program.aleo {
// ...

fn ping() {}

@noupgrade
constructor() {}
}

Dependency Resolution Process​

When you run a Leo command, dependencies are resolved as follows:

  1. Read program.json to find declared dependencies
  2. For each dependency:
    • Workspace: Look up the member in workspace.json and resolve to a local path
    • Local: Read the Leo source from the specified path and compile it, or use Aleo Instructions file
    • Network: Use the locked edition and check its checksum against cached or downloaded bytecode. If no lock entry exists, resolve the manifest edition or the latest edition and record its checksum.
    • Git: Reuse the commit pinned in leo.lock if present. Otherwise clone the repository, resolve the reference to a commit, and check it out. The checked-out package is then treated exactly like a local one.
  3. Resolve transitive dependencies - if your dependency imports other programs, those are fetched too
  4. Topologically sort all programs so dependencies are processed before dependents

Caching Behavior​

Network dependencies are cached locally at ~/.aleo/registry/{network}/{program_name}/{edition}/ to avoid repeated downloads.

Git dependencies are checked out under ~/.aleo/git/checkouts/{repo}-{url_hash}/{commit}/, keyed by the repository URL and the exact commit. Once a commit is checked out it is reused across all projects and dependencies without re-cloning.

Different commands handle caching differently:

CommandCache Behavior
leo buildUses cache
leo runUses cache
leo executeAlways fetches fresh
leo deployAlways fetches fresh
leo upgradeAlways fetches fresh
leo synthesizeAlways fetches fresh

leo execute, leo deploy, leo upgrade, and leo synthesize download network dependency bytecode again. Project dependencies still use their locked editions and checksums. A fresh download does not update the lock to a newer edition. Remote commands outside a project can resolve programs without a supplied lock.

To force a fresh fetch during build or run:

leo build --no-cache

This downloads locked network editions again and verifies their checksums. Use leo update to select newer editions. The first download trusts the configured endpoint; a checksum detects later changes but does not authenticate the first response.

Program Editions​

An edition is the version number of a deployed program on the Aleo network:

  • Edition 0: Initial deployment
  • Edition 1: First upgrade
  • Edition 2: Second upgrade
  • ...and so on

When no manifest edition or lock entry exists, Leo fetches the latest edition and records it in leo.lock. Later builds keep that edition, even if a newer one is deployed. To require an exact edition in the manifest:

leo add some_program.aleo --edition 3

This records the pinned edition in the manifest:

{
"name": "some_program.aleo",
"location": "network",
"path": null,
"edition": 3
}

The lock provides reproducible dependency resolution without an exact manifest edition. Set an exact edition when you also want leo update to keep it fixed. To change an exact requirement, change the manifest or use leo add NAME --edition EDITION.

To update dependencies whose manifest requirements permit a newer version:

leo update --dry-run
leo update
leo update some_program.aleo

Updates leave program.json unchanged. In a workspace, they resolve all members and development dependencies against the shared lock. A named update preserves unrelated versions where possible. If resolution fails, the existing lock stays unchanged. See leo update for options.

Note: Local dependencies do not have editions - they are always compiled from your current source code.

Deploying Programs with Dependencies​

Recursive Deployment​

When deploying a program that has local dependencies, use:

leo deploy --recursive

This deploys all local dependencies in topological order, then deploys your main program. Dependencies already deployed on-chain are skipped.

Network Dependencies at Deploy Time​

During deployment, Leo downloads the locked network dependency editions and verifies their checksums. Missing lock entries are resolved and recorded. Use leo update when you want to select newer editions that the manifest permits. Since the V8 consensus upgrade, an edition 0 network dependency must have a constructor. If the constructor does not exist, Leo reports that you must first upgrade the dependency on-chain.