Julia Module 8 – Modules, packages and reproducible environments
Julia environments: Project.toml and Manifest.toml
Create a Julia project environment, read what Project.toml and Manifest.toml record, rebuild an analysis exactly with instantiate and catch hidden dependencies.
What you will learn
- Create and activate a project environment and add a package to it
- Explain what Project.toml and Manifest.toml each record, and which files an analysis commits
- Reproduce an environment exactly with instantiate from a committed Manifest.toml
- Set compat bounds and predict which versions the resolver may choose
- Detect a dependency that only the shared default environment provides
Before you start
What you’ll learn · Before you start
What you will learn
- Create and activate a project environment and add a package to it
- Explain what Project.toml and Manifest.toml each record, and which files an analysis commits
- Reproduce an environment exactly with instantiate from a committed Manifest.toml
- Set compat bounds and predict which versions the resolver may choose
- Detect a dependency that only the shared default environment provides
Before you start
On this page
Ten days of rain, smoothed over three days, gave 8 numbers on one machine and 10 on another, from the same script. The package that computed them was 1.1.0 on the first and 2.0.0 on the second, and nothing in the project said which one it needed.
A Julia environment is the answer to “which versions?”: a Project.toml that names what the code uses, and a
Manifest.toml that pins every version exactly. This lesson creates one, reads both files, and rebuilds an analysis
on a second machine with Pkg.instantiate. The sessions run Julia 1.13.1 without network, from a small offline
registry with three versions of an invented package, Smoothing.
A project environment is two files: one names, one pins
A project environment: A folder with a Project.toml, and usually a Manifest.toml, that decides which packages and versions code can load. julia --project=<folder> or Pkg.activate makes it the active one. is a folder with a Project.toml. Adding a package
writes both files, and the session shows them:
# Three-day means of ten days of an invented rain gauge (mm), with the version of Smoothing that computed them.
using Smoothing
rain = [12.4, 0.0, 3.1, 31.0, 8.2, 0.4, 0.0, 15.6, 2.2, 4.8]
println("Smoothing ", pkgversion(Smoothing), ": ", round.(moving_average(rain, 3), digits = 2)) from session import env, files, run
# The session has no network, so Pkg installs from an offline registry, Lab, built by lab_registry.jl. Pkg is told not
# to try the package server, and not to precompile or clean up by itself, so every run prints the same text.
files("lab_registry.jl", "analyse.jl=rainfall/analyse.jl")
run("julia lab_registry.jl 1.0.0 1.1.0")
env.update(JULIA_PKG_SERVER="", JULIA_PKG_PRECOMPILE_AUTO="0", JULIA_PKG_GC_AUTO="false")
run("""
julia -e 'println(Base.active_project()); println(LOAD_PATH)'
cd rainfall
julia --project=. -e 'using Pkg; Pkg.add("Smoothing")'
cat Project.toml
cat Manifest.toml
julia --project=. analyse.jl
""") Output
$ julia lab_registry.jl 1.0.0 1.1.0
registry Lab: Smoothing 1.0.0, 1.1.0
$ julia -e 'println(Base.active_project()); println(LOAD_PATH)'
~/.julia/environments/v1.13/Project.toml
["@", "@v#.#", "@stdlib"]
$ cd rainfall
$ julia --project=. -e 'using Pkg; Pkg.add("Smoothing")'
Resolving package versions...
Cloning [a4c3e2d1-7b6a-4c5d-8e9f-0a1b2c3d4e51] Smoothing from ~/demo/lab/Smoothing
Installed Smoothing ─ v1.1.0
Updating `~/demo/rainfall/Project.toml`
[a4c3e2d1] + Smoothing v1.1.0
Updating `~/demo/rainfall/Manifest.toml`
[a4c3e2d1] + Smoothing v1.1.0
$ cat Project.toml
[deps]
Smoothing = "a4c3e2d1-7b6a-4c5d-8e9f-0a1b2c3d4e51"
$ cat Manifest.toml
# This file is machine-generated - editing it directly is not advised
julia_version = "1.13.1"
manifest_format = "2.1"
project_hash = "7b2eecaf3f63e0ebd2764632433a20cdb9e580dc"
[[deps.Smoothing]]
git-tree-sha1 = "618578fdec8f11d576183cf1b2d51f407e3419ab"
registries = "Lab"
uuid = "a4c3e2d1-7b6a-4c5d-8e9f-0a1b2c3d4e51"
version = "1.1.0"
[registries.Lab]
uuid = "e1a7c0de-5b1a-4f6c-9d2e-3c8a1b7f0a11"
$ julia --project=. analyse.jl
Smoothing 1.1.0: [5.17, 11.37, 14.1, 13.2, 2.87, 5.33, 5.93, 7.53]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 first_env_session.py
The Project.toml file names one dependency, Smoothing, by name and UUID, and no version. The
Manifest.toml: The file Pkg writes to record every package of an environment, direct or indirect, at one exact version with the hash of its source tree, and the Julia version that resolved it. file pins it: version 1.1.0, the git-tree-sha1 of the exact source tree,
the Julia version that resolved it, 1.13.1, and, new in Pkg 1.13, the registry it came from. The two files have
different jobs:
What each file of an environment records, and which command connects them
Text description of the diagram
Three boxes in a column, joined by two arrows.
- Project.toml, which you write, has a [deps] section naming Smoothing with its UUID, and an optional [compat] section, for example Smoothing = "1.1.0".
- An arrow labelled "resolve: Pkg.add, Pkg.update" leads to Manifest.toml, which Pkg writes. It records julia_version = "1.13.1", a [[deps.Smoothing]] entry with version = "1.1.0" and the git-tree-sha1 of the source tree, and, from Pkg 1.13, registries = "Lab".
- An arrow labelled "install: Pkg.instantiate" leads to the depot, ~/.julia/packages, where the exact source tree the Manifest names is installed.
For an analysis, a report or an application, commit both files: the Project.toml file says what the code needs, and the Manifest.toml file lets anyone rebuild exactly what ran. A library that other projects depend on commits its Project.toml with compat bounds instead, because its users’ environments pick the versions. Compat bounds are entries in the Project.toml file that list the versions of each dependency the project accepts.
Checkpoint
Check: What Project.toml and Manifest.toml record
The active environment decides what using finds
Before Pkg.add, the first command of the session printed the active project:
~/.julia/environments/v1.13/Project.toml, the shared default environment: The shared environment Julia uses when no project is active, ~/.julia/environments/v1.13 for Julia 1.13, shown as @v1.13. It stays on the load path behind any active project.,
named @v1.13 in the Pkg prompt. Without --project, every package you add goes there, for every script on the
machine. julia --project=. or Pkg.activate(".") makes the current folder the active project instead.
LOAD_PATH printed ["@", "@v#.#", "@stdlib"]: the active project, then the default environment of this Julia
version, then the standard library. using searches them in that order, as an environment stack. Step through one
search:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A using statement. analyse.jl runs using Smoothing with --project=rainfall, so the active project is rainfall/Project.toml.
- The active project first. The first entry of LOAD_PATH, "@", is the active project. Its Project.toml lists Smoothing, so the search stops here and Julia loads the version the Manifest pins, 1.1.0.
- A project that does not list it. Now take a project whose Project.toml does not list Smoothing. The search does not stop at "@"; it moves on.
- The default environment. Next comes "@v#.#", the shared default environment of this Julia version, ~/.julia/environments/v1.13. If Smoothing was added there, the search finds it, although the project never asked for it.
- The standard library. Then "@stdlib": packages that come with Julia, such as Test and Dates, are found here without being added anywhere.
- Nowhere. When no entry has the package, using throws ArgumentError: Package Smoothing not found in current path, with a hint to run Pkg.add.
The stack is convenient for tools you want everywhere, such as a profiler, but it has a cost: a script can load a package that its own project does not list.
Adding an analysis's packages without --project
Wrong: julia -e 'using Pkg; Pkg.add("Smoothing")' for one analysis. The package lands in the shared default
environment, @v1.13, where every script finds it, so a project that never lists it still runs on your machine.
Right: start with julia --project=., so Pkg.add records the package in that analysis’s own Project.toml and
Manifest.toml.
Checkpoint
Check: The active environment and the load path
instantiate rebuilds the exact versions the Manifest pins
The worked example of this lesson: the rainfall analysis ran with Smoothing 1.1.0. Later, version 2.0.0 is
published, and a colleague receives the project twice, once with its Manifest.toml file and once without, on a
machine whose depot has no packages:
from session import env, files, run
# The same offline set-up as before. A colleague's machine is a new home folder, so its depot has no packages in it.
files("lab_registry.jl", "analyse.jl=rainfall/analyse.jl")
run("julia lab_registry.jl 1.0.0 1.1.0")
env.update(JULIA_PKG_SERVER="", JULIA_PKG_PRECOMPILE_AUTO="0", JULIA_PKG_GC_AUTO="false")
run("""
julia --project=rainfall -e 'using Pkg; Pkg.add("Smoothing")' 2>&1 | tail -n 1
julia --project=rainfall rainfall/analyse.jl
# Later, Smoothing 2.0.0 is published. It also averages the partial windows at the start.
julia lab_registry.jl 2.0.0
# The colleague receives the project twice: once with its Manifest.toml, once without.
mkdir -p with-manifest without-manifest ~/colleague/.julia/registries
cp rainfall/Project.toml rainfall/Manifest.toml rainfall/analyse.jl with-manifest
cp rainfall/Project.toml rainfall/analyse.jl without-manifest
cp -R ~/.julia/registries/Lab ~/colleague/.julia/registries
HOME=~/colleague julia --project=with-manifest -e 'using Pkg; Pkg.instantiate()'
HOME=~/colleague julia --project=with-manifest with-manifest/analyse.jl
HOME=~/colleague julia --project=without-manifest -e 'using Pkg; Pkg.instantiate()'
HOME=~/colleague julia --project=without-manifest without-manifest/analyse.jl
HOME=~/colleague julia --project=with-manifest -e 'using Pkg; Pkg.status()'
""") Output
$ julia lab_registry.jl 1.0.0 1.1.0
registry Lab: Smoothing 1.0.0, 1.1.0
$ julia --project=rainfall -e 'using Pkg; Pkg.add("Smoothing")' 2>&1 | tail -n 1
[a4c3e2d1] + Smoothing v1.1.0
$ julia --project=rainfall rainfall/analyse.jl
Smoothing 1.1.0: [5.17, 11.37, 14.1, 13.2, 2.87, 5.33, 5.93, 7.53]
# Later, Smoothing 2.0.0 is published. It also averages the partial windows at the start.
$ julia lab_registry.jl 2.0.0
registry Lab: Smoothing 1.0.0, 1.1.0, 2.0.0
# The colleague receives the project twice: once with its Manifest.toml, once without.
$ mkdir -p with-manifest without-manifest ~/colleague/.julia/registries
$ cp rainfall/Project.toml rainfall/Manifest.toml rainfall/analyse.jl with-manifest
$ cp rainfall/Project.toml rainfall/analyse.jl without-manifest
$ cp -R ~/.julia/registries/Lab ~/colleague/.julia/registries
$ HOME=~/colleague julia --project=with-manifest -e 'using Pkg; Pkg.instantiate()'
Cloning [a4c3e2d1-7b6a-4c5d-8e9f-0a1b2c3d4e51] Smoothing from ~/demo/lab/Smoothing
Installed Smoothing ─ v1.1.0
$ HOME=~/colleague julia --project=with-manifest with-manifest/analyse.jl
Smoothing 1.1.0: [5.17, 11.37, 14.1, 13.2, 2.87, 5.33, 5.93, 7.53]
$ HOME=~/colleague julia --project=without-manifest -e 'using Pkg; Pkg.instantiate()'
Installed Smoothing ─ v2.0.0
Updating `~/demo/without-manifest/Project.toml`
[a4c3e2d1] + Smoothing v2.0.0
Updating `~/demo/without-manifest/Manifest.toml`
[a4c3e2d1] + Smoothing v2.0.0
$ HOME=~/colleague julia --project=without-manifest without-manifest/analyse.jl
Smoothing 2.0.0: [12.4, 6.2, 5.17, 11.37, 14.1, 13.2, 2.87, 5.33, 5.93, 7.53]
$ HOME=~/colleague julia --project=with-manifest -e 'using Pkg; Pkg.status()'
Status `~/demo/with-manifest/Project.toml`
⌃ [a4c3e2d1] Smoothing v1.1.0
Info Packages marked with ⌃ have new versions available and may be upgradable.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 instantiate_session.py
With the Manifest, Pkg.instantiate() cloned Smoothing, installed exactly v1.1.0, and the analysis printed the
same 8 numbers. Without it, instantiate had nothing to follow, so it resolved afresh, picked the newest version, 2.0.0,
and wrote a new Manifest; the analysis printed 10 numbers, whose first two, 12.4 and 6.2, come from partial windows
that 1.1.0 never computed. Pkg.status() marked the pinned copy with ⌃: a newer version exists, and the Manifest
holds the project where it was.
Key idea
The Project.toml file says what a project needs, and the Manifest.toml file says exactly what ran; commit both
for an analysis, and Pkg.instantiate() rebuilds the same versions anywhere.
Two deeper tools build on this. Compat bounds let a project accept new versions up to a breaking one, and the
resolver explains every refusal. A clean run with JULIA_LOAD_PATH set to the project alone exposes a package that
only the default environment supplied.
Checkpoint
Check: Rebuilding an environment with instantiate
Practise: environments
The questions use the rainfall project and its colleague. Your least secure idea comes up first, and every answer you choose is explained on the spot.
Practice that adapts to you
Practise this lesson
9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Compat bounds: what the resolver may choose, and what it refuses Pro: opens with a Pro, Premium or Ultimate pass
A Manifest freezes one moment; compat bounds decide what the next update may bring. This part computes what each form of entry admits and drives the resolver into its wall on purpose.
In this part:
- Reading a compat entry as a range of versions, computed
- Holding a project below a breaking release, and the error at the wall
- Choosing bounds that a registry accepts and your users can live with
2 runnable examples (Julia, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Hidden dependencies, two Julia versions and what to commit Pro: opens with a Pro, Premium or Ultimate pass
A project can work on your machine for reasons that are not in its files. This part reproduces the most common one, shows how one folder can serve two Julia versions, and turns everything into a checklist.
In this part:
- A script that runs only because the default environment has its package, reproduced
- One folder for two Julia versions: Manifest-v1.13.toml
- What to commit, decided case by case
- Rebuilding the environment on a clean machine, as a routine
2 runnable examples (Python) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
Warm-up (fresher to 2–5 years): how do Project.toml and Manifest.toml differ?
Project.toml lists the packages a project uses directly, by name and UUID, with optional [compat] bounds, and,
for a package, its own name, UUID and version. Manifest.toml is written by Pkg: it records every package in the
environment, direct and indirect, at one exact version with the hash of its source tree, plus the Julia version that
resolved it. Pkg.instantiate() installs exactly what the Manifest says. Commit both for an analysis or an
application; a library commits its Project.toml with compat bounds and leaves the versions to its users.
Three design questions follow: numbers a reader cannot reproduce, a fresh clone that fails, and what a library should commit. Say your first move for each, then open its first step:
-
Fresher to senior
Your paper's figures come out of an analysis that reproduces on your laptop. A reader who reruns it after installing the packages listed in Project.toml gets different numbers. Explain what probably happened, how you would confirm it, and what the repository should contain so that the reader's run matches yours.
-
Mid to senior
A project runs on every developer's machine, but on a new machine a fresh clone fails with Package Smoothing not found in current path, although Pkg.instantiate() finished without errors. How do you find the cause, and how do you stop it happening again?
-
Mid to staff
Your team keeps a library and an analysis repository that uses it. Someone proposes committing Manifest.toml in both repositories and deleting every compat entry, because the Manifest pins everything anyway. Argue for the right setup of each repository.
Key takeaways
- A project environment is a folder with a Project.toml file, which names the direct dependencies, and a Manifest.toml file, which pins every version and source tree exactly.
- The active project comes first on
LOAD_PATH,["@", "@v#.#", "@stdlib"], before the shared default environment@v1.13and the standard library. Pkg.instantiate()installed exactly Smoothing 1.1.0 from a committed Manifest; without the Manifest it took the newest version, 2.0.0, and the analysis changed.
Terms in this lesson
- default environment
- The shared environment Julia uses when no project is active, ~/.julia/environments/v1.13 for Julia 1.13, shown as @v1.13. It stays on the load path behind any active project.
- Manifest.toml
- The file Pkg writes to record every package of an environment, direct or indirect, at one exact version with the hash of its source tree, and the Julia version that resolved it.
- project environment
- A folder with a Project.toml, and usually a Manifest.toml, that decides which packages and versions code can load. julia --project=<folder> or Pkg.activate makes it the active one.
References
- Pkg: working with environments (The Julia Project)
- Pkg: Project.toml and Manifest.toml (The Julia Project)
- Pkg: compatibility (The Julia Project)
- Code loading: environments and environment stacks (The Julia Project)
- Environment variables: JULIA_LOAD_PATH and the JULIA_PKG variables (The Julia Project)
- AutoMerge guidelines of the General registry: compat bounds (JuliaRegistries)
- Julia 1.13 highlights: registries recorded in the manifest (The Julia Project)
- Pkg release notes: compat --current in Pkg 1.13 (The Julia Project)
- Julia 1.12 release notes: --project=@temp (The Julia Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress