Compiling Executables
Build standalone executables from your Lean code. Lean compiles to efficient native code through C.
The Main Function
Every executable needs a main function:
1-- Simplest main2def main : IO Unit :=3 IO.println "Hello, World!"45-- Main with arguments6def main (args : List String) : IO Unit := do7 IO.println s!"Arguments: {args}"89-- Main with exit code10def main : IO UInt32 := do11 IO.println "Running..."12 return 0 -- 0 = success, non-zero = failure1314-- (These are three ALTERNATIVES. A module defines main exactly once.)15-- Note: args does NOT include the program name, unlike C's argv[0].Building with Lake
Define an executable in your lakefile:
1-- lakefile.toml (the default format)2-- name = "myapp"3-- defaultTargets = ["myapp"]4--5-- [[lean_exe]]6-- name = "myapp"7-- root = "Main"89-- The same thing in lakefile.lean:10import Lake11open Lake DSL1213package myapp1415@[default_target]16lean_exe myapp where17 root := `Main1# Build the executable2lake build34# Run it5lake exe myapp67# Or run directly8./.lake/build/bin/myappBuild Types
Lake's optimisation level is the package's buildType, set in the lakefile rather than on the command line. The options are debug, relWithDebInfo (the default), minSizeRel, and release; they control the flags passed to the C compiler.
1name = "myapp"2buildType = "release"34[[lean_exe]]5name = "myapp"6root = "Main"1package myapp where2 buildType := .releaselake build -Krelease in older material. That does not enable optimisations — -K sets an arbitrary configuration key, and Lake has no built-in key called release. The command succeeds and changes nothing, which is why the myth survives. Set buildType instead.In practice the default relWithDebInfois fine for almost everything: Lean's own optimiser does most of the work before the C compiler ever sees the code. Reach for release when you are shipping a binary, and debug only when you need to step through the generated C in a debugger.
Command Line Argument Parsing
Pattern matching on the args list provides a simple way to handle command-line options. For complex CLIs, consider a dedicated parsing library.
1def main (args : List String) : IO UInt32 := do2 match args with3 | [] =>4 IO.println "Usage: myapp <command> [options]"5 return 16 | ["--help"] =>7 IO.println "myapp - A sample application"8 IO.println ""9 IO.println "Commands:"10 IO.println " run Run the main task"11 IO.println " check Check configuration"12 return 013 | ["run"] =>14 IO.println "Running..."15 return 016 | ["check"] =>17 IO.println "Checking configuration..."18 return 019 | cmd :: _ =>20 IO.println s!"Unknown command: {cmd}"21 return 1Multiple Executables
A single Lake project can produce multiple executables. Each one has its ownmain function in a separate root module, but they can all share library code.
1-- lakefile.lean2import Lake3open Lake DSL45package myproject67-- Library with shared code8lean_lib MyProject910-- Main CLI11lean_exe myapp where12 root := `Main1314-- Additional tool15lean_exe mytool where16 root := `Tool.Main1718-- Test runner19lean_exe tests where20 root := `Tests.Main1# Build all2lake build34# Build specific executable5lake build myapp6lake build mytool78# Run specific executable9lake exe myapp10lake exe mytoolmain function. They can share code through library targets.Cross-Platform Considerations
1def main : IO Unit := do2 -- Check platform3 if System.Platform.isWindows then4 IO.println "Running on Windows"5 else if System.Platform.isOSX then6 IO.println "Running on macOS"7 else8 IO.println "Running on Linux/Unix"910 -- Platform-specific paths11 let sep := if System.Platform.isWindows then "\\" else "/"12 IO.println s!"Path separator: {sep}"Environment Variables
Environment variables let you configure your program without changing code. Read them with IO.getEnv and set them for child processes.
1def main : IO Unit := do2 -- Read environment variable3 let home ← IO.getEnv "HOME"4 match home with5 | none => IO.println "HOME not set"6 | some path => IO.println s!"Home: {path}"78 -- Set environment variable (for child processes)9 -- Note: This affects the current process10 IO.Process.run {11 cmd := "echo"12 args := #["$MY_VAR"]13 env := #[("MY_VAR", some "hello")]14 }Running External Commands
Lean can invoke external programs and capture their output. This is useful for integrating with existing tools or shell scripts.
1def main : IO Unit := do2 -- Run a command and get output3 let output ← IO.Process.output {4 cmd := "ls"5 args := #["-la"]6 }7 IO.println s!"Exit code: {output.exitCode}"8 IO.println s!"Output: {output.stdout}"910 -- Run interactively (inherits stdin/stdout)11 let exitCode ← IO.Process.spawn {12 cmd := "vim"13 args := #["file.txt"]14 stdin := .inherit15 stdout := .inherit16 stderr := .inherit17 } >>= (·.wait)18 IO.println s!"Editor exited with: {exitCode}"Deep Dive: Compilation Pipeline
Lean compilation works in stages:
- Lean → IR: Lean code to internal representation
- IR → C: IR to C code (in
.lake/build/ir/) - C → Object: C to object files via system C compiler
- Link: Object files linked to final executable
The generated C is heavily optimized and uses reference counting for memory management.
Distribution
For distributing your executable:
1# Build (with buildType = "release" set in the lakefile)2lake build34# The binary lands here:5# .lake/build/bin/myapp (Linux, macOS)6# .lake/build/bin/myapp.exe (Windows)78# Check what it links against before you ship it:9ldd .lake/build/bin/myapp # Linux10otool -L .lake/build/bin/myapp # macOSlibgmp for arbitrary-precision Nat/Int). So a binary is portable across machines with the same OS and architecture and a compatible glibc — but it is not a fully static executable you can drop into a scratch container without checking. Run ldd and see.Create a main that prints the number of arguments and exits.
1def main (args : List String) : IO UInt32 := do2 IO.println s!"args: {args.length}"3 return 0Example: Complete CLI Application
1def version := "1.0.0"23def printHelp : IO Unit := do4 IO.println "myapp - A command line tool"5 IO.println ""6 IO.println "Usage: myapp [options] <command>"7 IO.println ""8 IO.println "Options:"9 IO.println " --help, -h Show this help"10 IO.println " --version, -v Show version"11 IO.println ""12 IO.println "Commands:"13 IO.println " init Initialize a new project"14 IO.println " build Build the project"15 IO.println " run Run the project"1617def doInit : IO UInt32 := do18 IO.println "Initializing..."19 IO.FS.writeFile "config.json" "{}"20 IO.println "Created config.json"21 return 02223def doBuild : IO UInt32 := do24 IO.println "Building..."25 -- Build logic here26 IO.println "Done!"27 return 02829def main (args : List String) : IO UInt32 := do30 match args with31 | [] | ["--help"] | ["-h"] =>32 printHelp33 return 034 | ["--version"] | ["-v"] =>35 IO.println s!"myapp version {version}"36 return 037 | ["init"] => doInit38 | ["build"] => doBuild39 | ["run"] =>40 IO.println "Running..."41 return 042 | cmd :: _ =>43 IO.println s!"Error: Unknown command '{cmd}'"44 IO.println "Run 'myapp --help' for usage"45 return 1