Skip to content

Commit 7aabb48

Browse files
tydeuJovanGerb
authored andcommitted
feat: update toolchain on lake update (leanprover#5684)
Lake will now update a package's `lean-toolchain` file on `lake update` if it finds the package's direct dependencies use a newer compatible toolchain. To skip this step, use the `--keep-toolchain` CLI option. Closes leanprover#2582. Closes leanprover#2752. Closes leanprover#5615. ### Toolchain update details To determine "newest compatible" toolchain, Lake parses the toolchain listed in the packages' `lean-toolchain` files into four categories: release , nightly, PR, and other. For newness, release toolchains are compared by semantic version (e.g., `"v4.4.0" < "v4.8.0"` and `"v4.6.0-rc1" < "v4.6.0"`) and nightlies are compared by date (e.g., `"nightly-2024-01-10" < "nightly-2014-10-01"`). All other toolchain types and mixtures are incompatible. If there is not a single newest toolchain, Lake will print a warning and continue updating without changing the toolchain. If Lake does find a new toolchain, Lake updates the workspace's `lean-toolchain` file accordingly and restarts the update process on the new Lake. If Elan is detected, it will spawn the new Lake process via `elan run` with the same arguments Lake was initially run with. If Elan is missing, it will prompt the user to restart Lake manually and exit with a special error code (4). ### Other changes To implement this new logic, various other refactors were needed. Here are some key highlights: * Logs emitted during package and workspace loading are now eagerly printed. * The Elan executable used by Lake is now configurable by the `ELAN` environment variable. * The `--lean` CLI option was removed. Use the `LEAN` environment variable instead. * `Package.deps` / `Package.opaqueDeps` have been removed. Use `findPackage?` with a dependency's name instead. * The dependency resolver now uses a pure breadth-first traversal to resolve dependencies. It also resolves dependencies in reverse order, which is done for consistency with targets. Latter targets shadow earlier ones and latter dependencies take precedence over earlier ones. **These changes mean the order of dependencies in a Lake manifest will change after the first `lake update` on this version of Lake.**
1 parent 7338e86 commit 7aabb48

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+886
-332
lines changed

src/lake/Lake/Build/Package.lean

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ open Lean (Name)
1919

2020
/-- Compute a topological ordering of the package's transitive dependencies. -/
2121
def Package.recComputeDeps (self : Package) : FetchM (Array Package) := do
22-
(·.toArray) <$> self.deps.foldlM (init := OrdPackageSet.empty) fun deps dep => do
22+
(·.toArray) <$> self.depConfigs.foldlM (init := OrdPackageSet.empty) fun deps cfg => do
23+
let some dep ← findPackage? cfg.name
24+
| error s!"{self.name}: package not found for dependency '{cfg.name}' \
25+
(this is likely a bug in Lake)"
2326
return (← fetch <| dep.facet `deps).foldl (·.insert ·) deps |>.insert dep
2427

2528
/-- The `PackageFacetConfig` for the builtin `depsFacet`. -/

src/lake/Lake/Build/Topological.lean

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ fetch functions, but not all fetch functions need build something.
3636
abbrev DFetchFn (α : Type u) (β : α → Type v) (m : Type v → Type w) :=
3737
(a : α) → m (β a)
3838

39+
/-- A `DFetchFn` that is not dependently typed. -/
40+
abbrev FetchFn (α : Type u) (β : Type v) (m : Type v → Type w) :=
41+
α → m β
42+
3943
/-!
4044
In order to nest builds / fetches within one another,
4145
we equip the monad `m` with a fetch function of its own.

src/lake/Lake/CLI/Help.lean

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ BASIC OPTIONS:
4141
--help, -h print help of the program or a command and exit
4242
--dir, -d=file use the package configuration in a specific directory
4343
--file, -f=file use a specific file for the package configuration
44-
--lean=cmd specify the `lean` command used by Lake
4544
-K key[=value] set the configuration file option named key
4645
--old only rebuild modified modules (ignore transitive deps)
4746
--rehash, -H hash all files for traces (do not trust `.hash` files)
48-
--update, -U update manifest before building
47+
--update, -U update dependencies on load (e.g., before a build)
4948
--reconfigure, -R elaborate configuration files instead of using OLeans
49+
--keep-toolchain do not update toolchain on workspace update
5050
--no-build exit immediately if a build target is not up-to-date
5151
--no-cache build packages locally; do not download build caches
5252
--try-cache attempt to download build caches for supported packages

src/lake/Lake/CLI/Init.lean

+1-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Authors: Gabriel Ebner, Sebastian Ullrich, Mac Malone
55
-/
66
import Lake.Util.Git
77
import Lake.Util.Sugar
8+
import Lake.Util.Version
89
import Lake.Config.Lang
910
import Lake.Config.Package
1011
import Lake.Config.Workspace
@@ -18,9 +19,6 @@ open Lean (Name)
1819
/-- The default module of an executable in `std` package. -/
1920
def defaultExeRoot : Name := `Main
2021

21-
/-- `elan` toolchain file name -/
22-
def toolchainFileName : FilePath := "lean-toolchain"
23-
2422
def gitignoreContents :=
2523
s!"/{defaultLakeDir}
2624
"

src/lake/Lake/CLI/Main.lean

+15-8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ namespace Lake
2626
/-! ## General options for top-level `lake` -/
2727

2828
structure LakeOptions where
29+
args : List String := []
2930
rootDir : FilePath := "."
3031
configFile : FilePath := defaultConfigFile
3132
elanInstall? : Option ElanInstall := none
@@ -36,6 +37,7 @@ structure LakeOptions where
3637
wantsHelp : Bool := false
3738
verbosity : Verbosity := .normal
3839
updateDeps : Bool := false
40+
updateToolchain : Bool := true
3941
reconfigure : Bool := false
4042
oldMode : Bool := false
4143
trustHash : Bool := true
@@ -72,12 +74,15 @@ def LakeOptions.computeEnv (opts : LakeOptions) : EIO CliError Lake.Env := do
7274
/-- Make a `LoadConfig` from a `LakeOptions`. -/
7375
def LakeOptions.mkLoadConfig (opts : LakeOptions) : EIO CliError LoadConfig :=
7476
return {
77+
lakeArgs? := opts.args.toArray
7578
lakeEnv := ← opts.computeEnv
7679
wsDir := opts.rootDir
7780
relConfigFile := opts.configFile
7881
lakeOpts := opts.configOpts
7982
leanOpts := Lean.Options.empty
8083
reconfigure := opts.reconfigure
84+
updateDeps := opts.updateDeps
85+
updateToolchain := opts.updateToolchain
8186
}
8287

8388
/-- Make a `BuildConfig` from a `LakeOptions`. -/
@@ -101,7 +106,7 @@ abbrev CliM := ArgsT CliStateM
101106

102107
def CliM.run (self : CliM α) (args : List String) : BaseIO ExitCode := do
103108
let (elanInstall?, leanInstall?, lakeInstall?) ← findInstall?
104-
let main := self.run' args |>.run' {elanInstall?, leanInstall?, lakeInstall?}
109+
let main := self.run' args |>.run' {args, elanInstall?, leanInstall?, lakeInstall?}
105110
let main := main.run >>= fun | .ok a => pure a | .error e => error e.toString
106111
main.run
107112

@@ -111,6 +116,12 @@ def CliM.run (self : CliM α) (args : List String) : BaseIO ExitCode := do
111116

112117
instance (priority := low) : MonadLift LogIO CliStateM := ⟨CliStateM.runLogIO⟩
113118

119+
@[inline] def CliStateM.runLoggerIO (x : LoggerIO α) : CliStateM α := do
120+
let opts ← get
121+
MainM.runLoggerIO x opts.outLv opts.ansiMode
122+
123+
instance (priority := low) : MonadLift LoggerIO CliStateM := ⟨CliStateM.runLoggerIO⟩
124+
114125
/-! ## Argument Parsing -/
115126

116127
def takeArg (arg : String) : CliM String := do
@@ -141,10 +152,6 @@ def noArgsRem (act : CliStateM α) : CliM α := do
141152
def getWantsHelp : CliStateM Bool :=
142153
(·.wantsHelp) <$> get
143154

144-
def setLean (lean : String) : CliStateM PUnit := do
145-
let leanInstall? ← findLeanCmdInstall? lean
146-
modify ({· with leanInstall?})
147-
148155
def setConfigOpt (kvPair : String) : CliM PUnit :=
149156
let pos := kvPair.posOf '='
150157
let (key, val) :=
@@ -171,6 +178,7 @@ def lakeLongOption : (opt : String) → CliM PUnit
171178
| "--quiet" => modifyThe LakeOptions ({· with verbosity := .quiet})
172179
| "--verbose" => modifyThe LakeOptions ({· with verbosity := .verbose})
173180
| "--update" => modifyThe LakeOptions ({· with updateDeps := true})
181+
| "--keep-toolchain" => modifyThe LakeOptions ({· with updateToolchain := false})
174182
| "--reconfigure" => modifyThe LakeOptions ({· with reconfigure := true})
175183
| "--old" => modifyThe LakeOptions ({· with oldMode := true})
176184
| "--no-build" => modifyThe LakeOptions ({· with noBuild := true})
@@ -193,7 +201,6 @@ def lakeLongOption : (opt : String) → CliM PUnit
193201
| "--file" => do
194202
let configFile ← takeOptArg "--file" "path"
195203
modifyThe LakeOptions ({· with configFile})
196-
| "--lean" => do setLean <| ← takeOptArg "--lean" "path or command"
197204
| "--help" => modifyThe LakeOptions ({· with wantsHelp := true})
198205
| "--" => do
199206
let subArgs ← takeArgs
@@ -331,7 +338,7 @@ protected def build : CliM PUnit := do
331338
processOptions lakeOption
332339
let opts ← getThe LakeOptions
333340
let config ← mkLoadConfig opts
334-
let ws ← loadWorkspace config opts.updateDeps
341+
let ws ← loadWorkspace config
335342
let targetSpecs ← takeArgs
336343
let specs ← parseTargetSpecs ws targetSpecs
337344
let buildConfig := mkBuildConfig opts (out := .stdout)
@@ -350,7 +357,7 @@ protected def resolveDeps : CliM PUnit := do
350357
let opts ← getThe LakeOptions
351358
let config ← mkLoadConfig opts
352359
noArgsRem do
353-
discard <| loadWorkspace config opts.updateDeps
360+
discard <| loadWorkspace config
354361

355362
protected def update : CliM PUnit := do
356363
processOptions lakeOption

src/lake/Lake/CLI/Serve.lean

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def setupFile
3838
IO.eprintln s!"Invalid Lake configuration. Please restart the server after fixing the Lake configuration file."
3939
exit 1
4040
let outLv := buildConfig.verbosity.minLogLv
41-
let ws ← MainM.runLogIO (minLv := outLv) (ansiMode := .noAnsi) do
41+
let ws ← MainM.runLoggerIO (minLv := outLv) (ansiMode := .noAnsi) do
4242
loadWorkspace loadConfig
4343
let imports := imports.foldl (init := #[]) fun imps imp =>
4444
if let some mod := ws.findModule? imp.toName then imps.push mod else imps
@@ -71,7 +71,7 @@ with the given additional `args`.
7171
-/
7272
def serve (config : LoadConfig) (args : Array String) : IO UInt32 := do
7373
let (extraEnv, moreServerArgs) ← do
74-
let (ws?, log) ← (loadWorkspace config).run?
74+
let (ws?, log) ← (loadWorkspace config).captureLog
7575
log.replay (logger := MonadLog.stderr)
7676
if let some ws := ws? then
7777
let ctx := mkLakeContext ws

src/lake/Lake/Config/Env.lean

+16-5
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def compute
6363
lake, lean, elan?,
6464
pkgUrlMap := ← computePkgUrlMap
6565
reservoirApiUrl := ← getUrlD "RESERVOIR_API_URL" s!"{reservoirBaseUrl}/v1"
66-
noCache := (noCache <|> (← IO.getEnv "LAKE_NO_CACHE").bind toBool?).getD false
66+
noCache := (noCache <|> (← IO.getEnv "LAKE_NO_CACHE").bind envToBool?).getD false
6767
githashOverride := (← IO.getEnv "LEAN_GITHASH").getD ""
6868
initToolchain := (← IO.getEnv "ELAN_TOOLCHAIN").getD ""
6969
initLeanPath := ← getSearchPath "LEAN_PATH",
@@ -72,10 +72,6 @@ def compute
7272
initPath := ← getSearchPath "PATH"
7373
}
7474
where
75-
toBool? (o : String) : Option Bool :=
76-
if ["y", "yes", "t", "true", "on", "1"].contains o.toLower then true
77-
else if ["n", "no", "f", "false", "off", "0"].contains o.toLower then false
78-
else none
7975
computePkgUrlMap := do
8076
let some urlMapStr ← IO.getEnv "LAKE_PKG_URL_MAP" | return {}
8177
match Json.parse urlMapStr |>.bind fromJson? with
@@ -144,14 +140,29 @@ Combines the initial path of the environment with that of the Lean installation.
144140
def sharedLibPath (env : Env) : SearchPath :=
145141
env.lean.sharedLibPath ++ env.initSharedLibPath
146142

143+
/-- Unset toolchain-specific environment variables. -/
144+
def noToolchainVars : Array (String × Option String) :=
145+
#[
146+
("ELAN_TOOLCHAIN", none),
147+
("LAKE", none),
148+
("LAKE_OVERRIDE_LEAN", none),
149+
("LAKE_HOME", none),
150+
("LEAN", none),
151+
("LEAN_GITHASH", none),
152+
("LEAN_SYSROOT", none),
153+
("LEAN_AR", none)
154+
]
155+
147156
/-- Environment variable settings that are not augmented by a Lake workspace. -/
148157
def baseVars (env : Env) : Array (String × Option String) :=
149158
#[
159+
("ELAN", env.elan?.map (·.elan.toString)),
150160
("ELAN_HOME", env.elan?.map (·.home.toString)),
151161
("ELAN_TOOLCHAIN", if env.toolchain.isEmpty then none else env.toolchain),
152162
("LAKE", env.lake.lake.toString),
153163
("LAKE_HOME", env.lake.home.toString),
154164
("LAKE_PKG_URL_MAP", toJson env.pkgUrlMap |>.compress),
165+
("LEAN", env.lean.lean.toString),
155166
("LEAN_GITHASH", env.leanGithash),
156167
("LEAN_SYSROOT", env.lean.sysroot.toString),
157168
("LEAN_AR", env.lean.ar.toString),

src/lake/Lake/Config/InstallPath.lean

+62-26
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ import Lake.Config.Defaults
99
open System
1010
namespace Lake
1111

12-
/-! ## Data Structures -/
12+
/-- Convert the string value of an environment variable to a boolean. -/
13+
def envToBool? (o : String) : Option Bool :=
14+
if ["y", "yes", "t", "true", "on", "1"].contains o.toLower then true
15+
else if ["n", "no", "f", "false", "off", "0"].contains o.toLower then false
16+
else none
1317

14-
/-- Standard path of `elan` in a Elan installation. -/
15-
def elanExe (home : FilePath) :=
16-
home / "bin" / "elan" |>.addExtension FilePath.exeExtension
18+
/-! ## Data Structures -/
1719

1820
/-- Information about the local Elan setup. -/
1921
structure ElanInstall where
2022
home : FilePath
21-
elan := elanExe home
23+
elan : FilePath
2224
binDir := home / "bin"
2325
toolchainsDir := home / "toolchains"
2426
deriving Inhabited, Repr
@@ -57,7 +59,7 @@ def initSharedLib : FilePath :=
5759
/-- Path information about the local Lean installation. -/
5860
structure LeanInstall where
5961
sysroot : FilePath
60-
githash : String
62+
githash : String := ""
6163
srcDir := sysroot / "src" / "lean"
6264
leanLibDir := sysroot / "lib" / "lean"
6365
includeDir := sysroot / "include"
@@ -67,9 +69,9 @@ structure LeanInstall where
6769
leanc := leancExe sysroot
6870
sharedLib := leanSharedLibDir sysroot / leanSharedLib
6971
initSharedLib := leanSharedLibDir sysroot / initSharedLib
70-
ar : FilePath
71-
cc : FilePath
72-
customCc : Bool
72+
ar : FilePath := "ar"
73+
cc : FilePath := "cc"
74+
customCc : Bool := false
7375
deriving Inhabited, Repr
7476

7577
/--
@@ -110,12 +112,16 @@ def LakeInstall.ofLean (lean : LeanInstall) : LakeInstall where
110112
/-! ## Detection Functions -/
111113

112114
/--
113-
Attempt to detect a Elan installation by checking the `ELAN_HOME`
114-
environment variable for a installation location.
115+
Attempt to detect an Elan installation by checking the `ELAN` and `ELAN_HOME`
116+
environment variables. If `ELAN` is set but empty, Elan is considered disabled.
115117
-/
116118
def findElanInstall? : BaseIO (Option ElanInstall) := do
117119
if let some home ← IO.getEnv "ELAN_HOME" then
118-
return some {home}
120+
let elan := (← IO.getEnv "ELAN").getD "elan"
121+
if elan.trim.isEmpty then
122+
return none
123+
else
124+
return some {elan, home}
119125
return none
120126

121127
/--
@@ -149,9 +155,9 @@ set to the empty string.
149155
150156
For (2), if `LEAN_AR` or `LEAN_CC` are defined, it uses those paths.
151157
Otherwise, if Lean is packaged with an `llvm-ar` and/or `clang`, use them.
152-
If not, use the `ar` and/or `cc` in the system's `PATH`. This last step is
153-
needed because internal builds of Lean do not bundle these tools
154-
(unlike user-facing releases).
158+
If not, use the `ar` and/or `cc` from the `AR` / `CC` environment variables
159+
or the system's `PATH`. This last step is needed because internal builds of
160+
Lean do not bundle these tools (unlike user-facing releases).
155161
156162
We also track whether `LEAN_CC` was set to determine whether it should
157163
be set in the future for `lake env`. This is because if `LEAN_CC` was not set,
@@ -187,18 +193,29 @@ where
187193
return FilePath.mk ar
188194
else
189195
let ar := leanArExe sysroot
190-
if (← ar.pathExists) then pure ar else pure "ar"
196+
if (← ar.pathExists) then
197+
return ar
198+
else if let some ar ← IO.getEnv "AR" then
199+
return ar
200+
else
201+
return "ar"
191202
findCc := do
192203
if let some cc ← IO.getEnv "LEAN_CC" then
193204
return (FilePath.mk cc, true)
194205
else
195206
let cc := leanCcExe sysroot
196-
let cc := if (← cc.pathExists) then cc else "cc"
207+
let cc ←
208+
if (← cc.pathExists) then
209+
pure cc
210+
else if let some cc ← IO.getEnv "CC" then
211+
pure cc
212+
else
213+
pure "cc"
197214
return (cc, false)
198215

199216
/--
200217
Attempt to detect the installation of the given `lean` command
201-
by calling `findLeanCmdHome?`. See `LeanInstall.get` for how it assumes the
218+
by calling `findLeanSysroot?`. See `LeanInstall.get` for how it assumes the
202219
Lean install is organized.
203220
-/
204221
def findLeanCmdInstall? (lean := "lean") : BaseIO (Option LeanInstall) :=
@@ -235,14 +252,28 @@ def getLakeInstall? (lake : FilePath) : BaseIO (Option LakeInstall) := do
235252
return none
236253

237254
/--
238-
Attempt to detect Lean's installation by first checking the
239-
`LEAN_SYSROOT` environment variable and then by trying `findLeanCmdHome?`.
255+
Attempt to detect Lean's installation by using the `LEAN` and `LEAN_SYSROOT`
256+
environment variables.
257+
258+
If `LEAN_SYSROOT` is set, use it. Otherwise, check `LEAN` for the `lean`
259+
executable. If `LEAN` is set but empty, Lean will be considered disabled.
260+
Otherwise, Lean's location will be determined by trying `findLeanSysroot?`
261+
using value of `LEAN` or, if unset, the `lean` in `PATH`.
262+
240263
See `LeanInstall.get` for how it assumes the Lean install is organized.
241264
-/
242265
def findLeanInstall? : BaseIO (Option LeanInstall) := do
243266
if let some sysroot ← IO.getEnv "LEAN_SYSROOT" then
244267
return some <| ← LeanInstall.get sysroot
245-
if let some sysroot ← findLeanSysroot? then
268+
let lean ← do
269+
if let some lean ← IO.getEnv "LEAN" then
270+
if lean.trim.isEmpty then
271+
return none
272+
else
273+
pure lean
274+
else
275+
pure "lean"
276+
if let some sysroot ← findLeanSysroot? lean then
246277
return some <| ← LeanInstall.get sysroot
247278
return none
248279

@@ -271,7 +302,8 @@ Then it attempts to detect if Lake and Lean are part of a single installation
271302
where the `lake` executable is co-located with the `lean` executable (i.e., they
272303
are in the same directory). If Lean and Lake are not co-located, Lake will
273304
attempt to find the their installations separately by calling
274-
`findLeanInstall?` and `findLakeInstall?`.
305+
`findLeanInstall?` and `findLakeInstall?`. Setting `LAKE_OVERRIDE_LEAN` to true
306+
will force Lake to use `findLeanInstall?` even if co-located.
275307
276308
When co-located, Lake will assume that Lean and Lake's binaries are located in
277309
`<sysroot>/bin`, their Lean libraries in `<sysroot>/lib/lean`, Lean's source files
@@ -280,9 +312,13 @@ following the pattern of a regular Lean toolchain.
280312
-/
281313
def findInstall? : BaseIO (Option ElanInstall × Option LeanInstall × Option LakeInstall) := do
282314
let elan? ← findElanInstall?
283-
if let some home ← findLakeLeanJointHome? then
284-
let lean ← LeanInstall.get home (collocated := true)
285-
let lake := LakeInstall.ofLean lean
286-
return (elan?, lean, lake)
315+
if let some sysroot ← findLakeLeanJointHome? then
316+
if (← IO.getEnv "LAKE_OVERRIDE_LEAN").bind envToBool? |>.getD false then
317+
let lake := LakeInstall.ofLean {sysroot}
318+
return (elan?, ← findLeanInstall?, lake)
319+
else
320+
let lean ← LeanInstall.get sysroot (collocated := true)
321+
let lake := LakeInstall.ofLean lean
322+
return (elan?, lean, lake)
287323
else
288324
return (elan?, ← findLeanInstall?, ← findLakeInstall?)

0 commit comments

Comments
 (0)