Building Projects
- Run single-file Lean programs from the command line with
lean - Create and configure multi-file projects with Lake
- Write a lakefile to define executables and libraries
- Import code from other files in the same project with
import - Add external package dependencies with
require
- So far we have run Lean files one at a time
- Real projects have multiple files, dependencies, and build steps
- DEBT: explain doc comments
Single-File Programs
i
def main : IO Unit := do
IO.println "Hello from a single file!"
IO.println s!"2 + 2 = {2 + 2}"
i
- Run a single Lean file from the terminal with
lean filename.lean- Like
python script.py
- Like
def main : IO Unitis the entry point- Lean executes
mainwhen you run the compiled file
- Lean executes
- For experiments and learning, single files are fine
- For anything larger, use Lake
What Is Lake?
- Lake is Lean's build system and package manager
- Like Python's
uv, Rust'scargo, or Node'snpm
- Like Python's
- A lakefile called
lakefile.leandescribes the project- Name, source files, and dependencies
- It is a Lean file that uses the Lake DSL
import Lakeandopen Lake DSLbring the build language into scopepackagenames the project;lean_exedefines an executable target
Creating a Project
i
-- Contents of lakefile.lean created by: lake new MyProject
import Lake
open Lake DSL
package «MyProject» where
name := "MyProject"
lean_exe «MyProject» where
root := `Main
i
lake new MyProjectcreates a fresh project directory- Generates a
lakefile.lean, aMain.lean, and a.lake/cache directory - Like
uv initorcargo new
- Generates a
lake initdoes the same thing inside an existing directory- The workspace is the directory that contains
lakefile.lean- All build commands are run from the workspace root
Minimal Lake File
packagenames the project; the«»angle-quote syntax allows spaces in names- Type
\f<<and\f>>to get the angle quotes
- Type
lean_exedefines an executable target (a compiled binary you can run)root := \Maintells Lake to look fordef maininMain.lean`- The backtick before
Mainis Lean's syntax for aNameliteral - It's like writing
"Main"but using Lean's internal name type instead of a string
- The backtick before
Building from the Command Line
i
-- Main.lean: entry point for the compiled executable
def main : IO Unit := do
IO.println "Built with Lake!"
i
lake buildcompiles all targets in the project- Like
cargo buildoruv run python -m build
- Like
- The compiled binary lands in
.lake/build/bin/ - Lake only recompiles files that have changed
- Fast for large projects because the
.lake/directory caches intermediate results
- Fast for large projects because the
- Lake also accepts a target name:
lake build MyTarget
Running an Executable
lake exe targetnamebuilds and runs an executable in one step- Like
cargo runin Rust
- Like
- If the project is already built,
lake exeskips the compile step - The executable name is whatever you wrote after
lean_exeinlakefile.lean
Multiple Source Files
i
-- Helpers.lean: a helper module in the same project
namespace Helpers
def greet (name : String) : String :=
s!"Hello, {name}!"
def shout (msg : String) : String :=
msg.toUpper ++ "!!!"
end Helpers
i
- Split large projects into multiple
.leanfiles- Each file becomes a module that other files can import
- File names must start with a capital letter
Helpers.lean→ moduleMyProject.Helpers- The project name prefix comes from
packageinlakefile.lean
- Use
namespace … endto group related definitions within a file
Importing Your Own Modules
i
-- Main.lean: imports and uses the Helpers module
import MyProject.Helpers
def main : IO Unit := do
IO.println (Helpers.greet "Lake")
IO.println (Helpers.shout "lean")
i
Build completed successfully (0 jobs).
Hello, Lake!
LEAN!!!
import MyProject.HelpersbringsHelpers.leaninto scope- The full path is
PackageName.FileName(no.leanextension)
- The full path is
- After importing, call functions with the namespace prefix:
Helpers.greet- Or
open Helpers in greetto drop the prefix for one expression
- Or
- Import order matters: Lean processes files top to bottom
- Cannot have circular imports
Libraries vs. Executables
i
import Lake
open Lake DSL
package «myproject» where
name := "myproject"
-- A library target: compiled but not directly runnable
lean_lib «MathUtils» where
srcDir := "lib"
-- An executable target: produces a runnable binary
lean_exe «myproject» where
root := `Main
i
warning: /private/var/folders/jn/dg894s5542dbrw7ry6sks0r80000gn/T/tmp.c62TtdrWr9/lakefile.lean:5:2: unknown 'PackageConfig' field 'name'
info: myproject: no previous manifest, creating one from scratch
info: toolchain not updated; no toolchain information found
Build completed successfully (0 jobs).
lean_libcompiles source files into a reusable library- Produces
.oleanfiles (pre-compiled Lean objects) but no binary - Other packages can depend on it with
require - Lake knows to create a library because you used
lean_lib: the keyword determines the target type
- Produces
lean_exelinks source files into a runnable binary- Must have a
def main : IO Unitin the root file
- Must have a
- One project can define both: libraries and executables are separate targets
Running Lean in Project Context
i
-- script.lean: a standalone script that uses project modules
import MyProject.Helpers
#eval Helpers.greet "world"
i
Build completed successfully (0 jobs).
"Hello, world!"
lake env lean file.leanrunsleanwith the project's modules on the path- Without
lake env, imports that refer to your project's modules fail - Like Python's
uv run python script.pyvs. barepython script.py
- Without
- Use
lake env leanfor one-off scripts that need project modules but aren't targets
Adding a Dependency
i
import Lake
open Lake DSL
package «myproject» where
name := "myproject"
-- Add an external dependency from a git repository
require Std from git
"https://github.com/leanprover/std4" @ "main"
lean_exe «myproject» where
root := `Main
i
warning: /private/var/folders/jn/dg894s5542dbrw7ry6sks0r80000gn/T/tmp.dMhSDydxYA/lakefile.lean:5:2: unknown 'PackageConfig' field 'name'
info: myproject: no previous manifest, creating one from scratch
info: Std: cloning https://github.com/leanprover/std4
info: Std: checking out revision '708b057842c4cd0845fba132bd94b08493f6fc42'
info: updating toolchain to 'leanprover/lean4:v4.31.0-rc1'
info: restarting Lake via Elan
info: Version 4.2.2 of elan is available! Use `elan self update` to update.
info: downloading https://releases.lean-lang.org/lean4/v4.31.0-rc1/lean-4.31.0-rc1-darwin_aarch64.tar.zst
info: installing /Users/gvwilson/.elan/toolchains/leanprover--lean4---v4.31.0-rc1
warning: /private/var/folders/jn/dg894s5542dbrw7ry6sks0r80000gn/T/tmp.dMhSDydxYA/lakefile.lean:5:2: unknown 'PackageConfig' field 'name'
info: myproject: no previous manifest, creating one from scratch
info: toolchain not updated; already up-to-date
require PackageName from git "url" @ "ref"declares an external dependency- Lake fetches the package and caches it in
.lake/packages/
- Lake fetches the package and caches it in
- After editing
lakefile.lean, runlake updateto download the dependency- Then
importits modules just like your own
- Then
Stdis the Lean standard library extension;Mathlibis the math library- Both are added with
require
- Both are added with
- For local packages not on a git remote, use a path:
lean require MyLib from "../my-lib"- Like a Python editable install or a Rust path dependency
Namespaces Across Files
i
-- MathUtils/Basic.lean
-- The module path is MyProject.MathUtils.Basic
-- The namespace should match the module path by convention
namespace MathUtils.Basic
def square (n : Int) : Int := n * n
def cube (n : Int) : Int := n * n * n
end MathUtils.Basic
i
- By convention, namespace names should match the module import path
- File
MathUtils/Basic.lean→ moduleMyProject.MathUtils.Basic - Namespace inside that file:
MathUtils.Basic
- File
- This isn't enforced by the compiler, but it makes code easier to navigate
- Like Python's module names matching the file names
open Namespace in expruses the namespace for one expression withoutopenpolluting the whole file
Exercises
Fix: Missing Import
i
-- Fix: this file uses Utils.greet but forgets to import it
-- Add the missing import at the top
def main : IO Unit := do
IO.println (Utils.greet "world")
hint
Utils.greetis called butUtilsis never imported- Add
import MyProject.Utilsat the top of the file
Fix: Wrong Target Type
i
-- Fix: this target should be an executable, not a library
import Lake
open Lake DSL
package «app» where
name := "app"
lean_lib «app» where
root := `Main
hint
lean_libcreates a library; it cannot be run directly- Change
lean_libtolean_exeto create an executable target - Also change the field from
srcDirtoroot := \Main`
Fix: Wrong Module Name Case
i
-- Fix: the module name is wrong — Lean module names match the filename
-- The file is 'Helpers.lean' but the import uses a lowercase name
import MyProject.helpers
def main : IO Unit := do
IO.println (Helpers.greet "world")
hint
- Lean module names are derived from filenames, which start with a capital letter
helpersshould beHelpersto matchHelpers.lean- Change
import MyProject.helperstoimport MyProject.Helpers
Fix: Missing main Function
i
-- Fix: executables need a 'main' function
-- This file is set as the root of a lean_exe but has no main
def greet : String := "Hello!"
hint
- Executables need
def main : IO Unitas their entry point - Add
def main : IO Unit := IO.println greetto the file - Without
main, Lake cannot produce a runnable binary
Fix: Wrong Namespace in Call
i
-- Fix: the function is defined in namespace 'Foo' but called as 'Bar.func'
namespace Foo
def func : String := "from Foo"
end Foo
def main : IO Unit := do
IO.println Bar.func
hint
funcis defined insidenamespace Foo, so it isFoo.func- Change
Bar.functoFoo.functo match the actual namespace
Fix: Wrong Source Directory
i
-- Fix: srcDir points to the wrong location
-- The .lean files are in 'src/' but lakefile says 'source/'
import Lake
open Lake DSL
package «myapp» where
name := "myapp"
lean_exe «myapp» where
root := `Main
srcDir := "source"
hint
srcDir := "source"tells Lake to look for.leanfiles insource/- But the project files are in
src/ - Change
srcDir := "source"tosrcDir := "src"
Write: Complete a Lake File
i
-- Write: fill in the blanks to create a lakefile for an executable called 'greeter'
import Lake
open Lake DSL
package «greeter» where
name := "greeter"
-- TODO: add a lean_exe target called 'greeter' with root `Main
hint
- Replace the
TODOcomment withlean_exe «greeter» where - Add
root := \Main` indented under that line - The
«greeter»must match the name you want to use inlake exe greeter
Write: Add a main Function
i
-- Write: add a main function that prints "Hello, Lake!"
-- def main : IO Unit := ???
hint
- Add
def main : IO Unit := IO.println "Hello, Lake!"to the file IO.printlnprints a string followed by a newline- No
doblock needed for a single IO action
Write: Implement a Module Function
i
-- Write: implement greet using string interpolation
namespace Greeter
def greet (name : String) : String :=
"" -- replace with s!"Hello, {name}!"
end Greeter
#eval Greeter.greet "world"
hint
- Replace
""withs!"Hello, {name}!" - The
#evalcall at the bottom will print"Hello, world!"when correct
Write: Add an Import
i
-- Write: add the missing import and fix the main function
-- Assume MyProject.Greeter defines Greeter.greet
def main : IO Unit := do
IO.println (Greeter.greet "World")
hint
- Add
import MyProject.Greeterat the top of the file - The import path matches the file path:
MyProject/Greeter.lean - After importing,
Greeter.greetbecomes available
Write: Add a Library Target
i
-- Write: add a lean_lib target called 'MathLib'
import Lake
open Lake DSL
package «myproject» where
name := "myproject"
lean_exe «myproject» where
root := `Main
-- TODO: add a lean_lib target named 'MathLib'
hint
- Replace the
TODOcomment withlean_lib MathLib { } - A library target does not need a
rootunless you want a specific entry module - Build it with
lake build MathLib
Write: Fix the Namespace Name
i
-- Write: the namespace should match the module name 'StringUtils'
-- Currently it's 'StrUtil' — fix it
namespace StrUtil
def shout (s : String) : String := s.toUpper ++ "!!!"
end StrUtil
#eval StrUtil.shout "hello"
hint
- The namespace
StrUtildoesn't match the module nameStringUtils - Change both
namespace StrUtilandend StrUtiltoStringUtils - Then callers can use
StringUtils.shoutwhich matches the file name