/- Copyright (c) 2021 Mac Malone. All rights reserved. Released under Apache 2.0 license as described in the file LICENSE. Authors: Mac Malone -/ import Lake.Config.Load import Lake.Config.SearchPath import Lake.Config.InstallPath import Lake.Config.Manifest import Lake.Config.Resolve import Lake.Util.Error import Lake.Util.MainM import Lake.Util.Cli import Lake.CLI.Init import Lake.CLI.Help import Lake.CLI.Build import Lake.CLI.Error open System open Lean (Json toJson fromJson?) namespace Lake def Package.clean (self : Package) : IO PUnit := do if (← self.buildDir.pathExists) then IO.FS.removeDirAll self.buildDir -- # Loading Lake Config structure LakeConfig where rootDir : FilePath := "." configFile : FilePath := defaultConfigFile leanInstall : LeanInstall lakeInstall : LakeInstall args : List String := [] def loadPkg (config : LakeConfig) : LogIO Package := do setupLeanSearchPath config.leanInstall config.lakeInstall Package.load config.rootDir config.args (config.rootDir / config.configFile) def loadManifestMap (manifestFile : FilePath) : LogIO (Lean.NameMap PackageEntry) := do if let Except.ok contents ← IO.FS.readFile manifestFile |>.toBaseIO then match Json.parse contents with | Except.ok json => match fromJson? json with | Except.ok (manifest : Manifest) => pure manifest.toMap | Except.error e => logWarning s!"improperly formatted package manifest: {e}" pure {} | Except.error e => logWarning s!"invalid JSON in package manifest: {e}" pure {} else pure {} def loadWorkspace (config : LakeConfig) (updateDeps := false) : LogIO Workspace := do let pkg ← loadPkg config let ws := Workspace.ofPackage pkg let manifestMap ← loadManifestMap ws.manifestFile let (packageMap, resolvedMap) ← resolveDeps ws pkg updateDeps |>.run manifestMap unless resolvedMap.isEmpty do IO.FS.writeFile ws.manifestFile <| Json.pretty <| toJson <| Manifest.fromMap resolvedMap let packageMap := packageMap.insert pkg.name pkg return {ws with packageMap} -- # CLI structure LakeOptions where rootDir : FilePath := "." configFile : FilePath := defaultConfigFile leanInstall? : Option LeanInstall := none lakeInstall? : Option LakeInstall := none subArgs : List String := [] wantsHelp : Bool := false abbrev CliMainM := ExceptT CliError MainM abbrev CliStateM := StateT LakeOptions CliMainM abbrev CliM := ArgsT CliStateM namespace Cli -- ## Option Management def getWantsHelp : CliStateM Bool := (·.wantsHelp) <$> get def setLean (lean : String) : CliStateM PUnit := do let leanInstall? ← findLeanCmdInstall? lean modify ({· with leanInstall?}) -- ## Other Option Helpers /-- Get the Lean installation. Error if missing. -/ def getLeanInstall (opts : LakeOptions) : Except CliError LeanInstall := do if let some leanInstall := opts.leanInstall? then return leanInstall else throw CliError.unknownLeanInstall /-- Get the Lake installation. Error if missing. -/ def getLakeInstall (opts : LakeOptions) : Except CliError LakeInstall := do if let some lakeInstall := opts.lakeInstall? then return lakeInstall else throw CliError.unknownLakeInstall /-- Get the Lean and Lake installation. Error if either is missing. -/ def getInstall (opts : LakeOptions) : Except CliError (LeanInstall × LakeInstall) := do return (← getLeanInstall opts, ← getLakeInstall opts) /-- Make a `LakeConfig` from a `LakeOptions`. -/ def mkLakeConfig (opts : LakeOptions) (args : List String := []) : Except CliError LakeConfig := do let (leanInstall, lakeInstall) ← getInstall opts return { rootDir := opts.rootDir, configFile := opts.configFile, leanInstall, lakeInstall, args } /-- Make a Lake `Context` from a `Workspace` and `LakeConfig`. -/ def mkLakeContext (ws : Workspace) (config : LakeConfig) : Context where lean := config.leanInstall lake := config.lakeInstall opaqueWs := ws /-- Load configuration using CLI state and run the `LakeT` action. -/ def runLakeT [MonadLiftT m CliStateM] (x : LakeT m α) : CliStateM α := do let config ← mkLakeConfig (← get) let ws ← loadWorkspace config liftM <| x.run <| mkLakeContext ws config -- ## Argument Parsing def takeArg (arg : String) : CliM String := do match (← takeArg?) with | none => throw <| CliError.missingArg arg | some arg => pure arg def takeOptArg (opt arg : String) : CliM String := do match (← takeArg?) with | none => throw <| CliError.missingOptArg opt arg | some arg => pure arg /-- Verify that there are no CLI arguments remaining before running the given action. -/ def noArgsRem (act : CliStateM α) : CliM α := do let args ← getArgs if args.isEmpty then act else throw <| CliError.unexpectedArguments args -- ## Option Parsing def shortOption : (opt : Char) → CliM PUnit | 'h' => modifyThe LakeOptions ({· with wantsHelp := true}) | 'd' => do let rootDir ← takeOptArg "-d" "path"; modifyThe LakeOptions ({· with rootDir}) | 'f' => do let configFile ← takeOptArg "-f" "path"; modifyThe LakeOptions ({· with configFile}) | opt => throw <| CliError.unknownShortOption opt def longOption : (opt : String) → CliM PUnit | "--help" => modifyThe LakeOptions ({· with wantsHelp := true}) | "--dir" => do let rootDir ← takeOptArg "--dir" "path"; modifyThe LakeOptions ({· with rootDir}) | "--file" => do let configFile ← takeOptArg "--file" "path"; modifyThe LakeOptions ({· with configFile}) | "--lean" => do setLean <| ← takeOptArg "--lean" "path or command" | "--" => do let subArgs ← takeArgs; modifyThe LakeOptions ({· with subArgs}) | opt => throw <| CliError.unknownLongOption opt def lakeOption := option { short := shortOption long := longOption longShort := shortOptionWithArg shortOption } -- ## Commands /-- Verify the Lean version Lake was built with matches that of the give Lean installation. -/ def verifyLeanVersion (leanInstall : LeanInstall) : Except CliError PUnit := do unless leanInstall.githash == Lean.githash do throw <| CliError.leanRevMismatch Lean.githash leanInstall.githash /-- Output the detected installs and verify the Lean version. -/ def verifyInstall (opts : LakeOptions) : ExceptT CliError MainM PUnit := do IO.println s!"Lean:\n{repr <| opts.leanInstall?}" IO.println s!"Lake:\n{repr <| opts.lakeInstall?}" let (leanInstall, _) ← getInstall opts verifyLeanVersion leanInstall /-- Exit code to return if `print-paths` cannot find the config file. -/ def noConfigFileCode : ExitCode := 2 /-- Environment variable that is set when `lake serve` cannot parse the Lake configuration file and falls back to plain `lean --server`. -/ def invalidConfigEnvVar := "LAKE_INVALID_CONFIG" /-- Build a list of imports of the package and print the `.olean` and source directories of every used package. If no configuration file exists, exit silently with `noConfigFileCode` (i.e, 2). The `print-paths` command is used internally by Lean 4 server. -/ def printPaths (config : LakeConfig) (imports : List String := []) : MainM PUnit := do let configFile := config.rootDir / config.configFile if (← configFile.pathExists) then if (← IO.getEnv invalidConfigEnvVar) matches some .. then IO.eprintln s!"Error parsing '{configFile}'. Please restart the lean server after fixing the Lake configuration file." exit 1 let ws ← loadWorkspace config let ctx ← mkBuildContext ws config.leanInstall config.lakeInstall let dynlibs ← ws.root.buildImportsAndDeps imports |>.run MonadLog.eio ctx IO.println <| Json.compress <| toJson {ws.leanPaths with loadDynlibPaths := dynlibs} else exit noConfigFileCode def env (cmd : String) (args : Array String := #[]) : LakeT IO UInt32 := do IO.Process.spawn {cmd, args, env := ← getLeanEnv} >>= (·.wait) def serve (config : LakeConfig) (args : Array String) : LogIO UInt32 := do let (extraEnv, moreServerArgs) ← try let ws ← loadWorkspace config let ctx := mkLakeContext ws config pure (← LakeT.run ctx getLeanEnv, ws.root.moreServerArgs) catch _ => logWarning "package configuration has errors, falling back to plain `lean --server`" pure (#[(invalidConfigEnvVar, "1")], #[]) (← IO.Process.spawn { cmd := config.leanInstall.lean.toString args := #["--server"] ++ moreServerArgs ++ args env := extraEnv }).wait def parseScriptSpec (ws : Workspace) (spec : String) : Except CliError (Package × String) := match spec.splitOn "/" with | [script] => return (ws.root, script) | [pkg, script] => return (← parsePackageSpec ws pkg, script) | _ => throw <| CliError.invalidScriptSpec spec def script : (cmd : String) → CliM PUnit | "list" => do processOptions lakeOption let config ← mkLakeConfig (← getThe LakeOptions) noArgsRem do let ws ← loadWorkspace config ws.packageMap.forM fun _ pkg => do let pkgName := pkg.name.toString (escape := false) pkg.scripts.forM fun name _ => let scriptName := name.toString (escape := false) IO.println s!"{pkgName}/{scriptName}" | "run" => do processOptions lakeOption let spec ← takeArg "script spec"; let args ← takeArgs let config ← mkLakeConfig (← getThe LakeOptions) let ws ← loadWorkspace config let (pkg, scriptName) ← parseScriptSpec ws spec if let some script := pkg.scripts.find? scriptName then exit <| ← script.run args |>.run { lean := config.leanInstall, lake := config.lakeInstall, opaqueWs := ws } else do throw <| CliError.unknownScript scriptName | "doc" => do processOptions lakeOption let spec ← takeArg "script spec" let config ← mkLakeConfig (← getThe LakeOptions) noArgsRem do let ws ← loadWorkspace config let (pkg, scriptName) ← parseScriptSpec ws spec if let some script := pkg.scripts.find? scriptName then match script.doc? with | some doc => IO.println doc | none => throw <| CliError.missingScriptDoc scriptName else throw <| CliError.unknownScript scriptName | "help" => do IO.println <| helpScript <| (← takeArg?).getD "" | cmd => throw <| CliError.unknownCommand cmd def parseTemplateSpec (spec : String) : Except CliError InitTemplate := if spec.isEmpty then pure default else if let some tmp := InitTemplate.parse? spec then pure tmp else throw <| CliError.unknownTemplate spec def command : (cmd : String) → CliM PUnit | "new" => do processOptions lakeOption let pkgName ← takeArg "package name" let template ← parseTemplateSpec <| (← takeArg?).getD "" noArgsRem <| new pkgName template | "init" => do processOptions lakeOption let pkgName ← takeArg "package name" let template ← parseTemplateSpec <| (← takeArg?).getD "" noArgsRem <| init pkgName template | "build" => do processOptions lakeOption let opts ← getThe LakeOptions let config ← mkLakeConfig opts opts.subArgs let ws ← loadWorkspace config let targetSpecs ← takeArgs let target ← show Except _ _ from do let targets ← targetSpecs.mapM <| parseTargetSpec ws if targets.isEmpty then resolveDefaultPackageTarget ws ws.root else return Target.collectOpaqueList targets let ctx ← mkBuildContext ws config.leanInstall config.lakeInstall BuildM.run MonadLog.io ctx target.build | "update" => do processOptions lakeOption let opts ← getThe LakeOptions let config ← mkLakeConfig opts opts.subArgs noArgsRem <| discard <| loadWorkspace config (updateDeps := true) | "print-paths" => do processOptions lakeOption let config ← mkLakeConfig (← getThe LakeOptions) printPaths config (← takeArgs) | "clean" => do processOptions lakeOption let opts ← getThe LakeOptions let config ← mkLakeConfig opts opts.subArgs noArgsRem <| (← loadPkg config).clean | "script" => do if let some cmd ← takeArg? then processLeadingOptions lakeOption -- between command and args if (← getWantsHelp) then IO.println <| helpScript cmd else script cmd else throw <| CliError.missingCommand | "serve" => do processOptions lakeOption let opts ← getThe LakeOptions let args := opts.subArgs.toArray let config ← mkLakeConfig opts noArgsRem do exit <| ← serve config args | "env" => do let cmd ← takeArg "command"; let args ← takeArgs exit <| ← runLakeT <| env cmd args.toArray | "self-check" => do processOptions lakeOption noArgsRem <| verifyInstall (← getThe LakeOptions) | "help" => do IO.println <| help <| (← takeArg?).getD "" | cmd => throw <| CliError.unknownCommand cmd def processArgs : CliM PUnit := do match (← getArgs) with | [] => IO.println usage | ["--version"] => IO.println uiVersionString | _ => -- normal CLI processLeadingOptions lakeOption -- between `lake` and command if let some cmd ← takeArg? then processLeadingOptions lakeOption -- between command and args if (← getWantsHelp) then IO.println <| help cmd else command cmd else if (← getWantsHelp) then IO.println usage else throw <| CliError.missingCommand end Cli open Cli in def CliM.run (self : CliM α) (args : List String) : BaseIO UInt32 := do let (leanInstall?, lakeInstall?) ← findInstall? let main := self args |>.run' {leanInstall?, lakeInstall?} let main := main.run >>= fun | .ok a => pure a | .error e => error e.toString main.run def cli (args : List String) : BaseIO UInt32 := Cli.processArgs.run args