303 lines
9.8 KiB
Text
303 lines
9.8 KiB
Text
/-
|
||
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.Resolve
|
||
import Lake.Config.Util
|
||
import Lake.Util.Error
|
||
import Lake.Util.MainM
|
||
import Lake.Util.Cli
|
||
import Lake.CLI.Init
|
||
import Lake.CLI.Help
|
||
import Lake.CLI.Build
|
||
|
||
open System
|
||
open Lean (Name Json toJson)
|
||
|
||
namespace Lake
|
||
|
||
-- # CLI
|
||
|
||
structure LakeOptions where
|
||
rootDir : FilePath := "."
|
||
configFile : FilePath := defaultConfigFile
|
||
leanInstall? : Option LeanInstall := none
|
||
lakeInstall? : Option LakeInstall := none
|
||
subArgs : List String := []
|
||
wantsHelp : Bool := false
|
||
|
||
abbrev CliStateM := StateT LakeOptions <| MainM
|
||
abbrev CliM := ArgsT CliStateM
|
||
|
||
namespace Cli
|
||
|
||
-- ## Basic Actions
|
||
|
||
/-- Print out a line wih the given message and then exit with an error code. -/
|
||
protected def error (msg : String) (rc : UInt32 := 1) : MainM α := do
|
||
IO.eprintln s!"error: {msg}" |>.catchExceptions fun _ => ()
|
||
exit rc
|
||
|
||
instance : MonadError MainM := ⟨Cli.error⟩
|
||
instance : MonadLift IO MainM := ⟨MonadError.runIO⟩
|
||
|
||
-- ## Basic State Management
|
||
|
||
def getRootDir : CliStateM FilePath :=
|
||
(·.rootDir) <$> get
|
||
|
||
def setRootDir (dir : FilePath) : CliStateM PUnit :=
|
||
modify fun st => {st with rootDir := dir}
|
||
|
||
def getConfigFile : CliStateM FilePath :=
|
||
(·.configFile) <$> get
|
||
|
||
def setConfigFile (file : FilePath) : CliStateM PUnit :=
|
||
modify ({· with configFile := file})
|
||
|
||
def getSubArgs : CliStateM (List String) :=
|
||
(·.subArgs) <$> get
|
||
|
||
def setSubArgs (args : List String) : CliStateM PUnit :=
|
||
modify fun st => {st with subArgs := args}
|
||
|
||
def getWantsHelp : CliStateM Bool :=
|
||
(·.wantsHelp) <$> get
|
||
|
||
def setWantsHelp : CliStateM PUnit :=
|
||
modify fun st => {st with wantsHelp := true}
|
||
|
||
def setLean (lean : String) : CliStateM PUnit := do
|
||
let leanInstall? ← findLeanCmdInstall? lean
|
||
modify fun st => {st with leanInstall?}
|
||
|
||
def getLeanInstall? : CliStateM (Option LeanInstall) :=
|
||
(·.leanInstall?) <$> get
|
||
|
||
def getLakeInstall? : CliStateM (Option LakeInstall) :=
|
||
(·.lakeInstall?) <$> get
|
||
|
||
-- ## Complex State Management
|
||
|
||
def loadPkg (args : List String := []) : CliStateM Package := do
|
||
let dir ← getRootDir; let file ← getConfigFile
|
||
setupLeanSearchPath (← getLeanInstall?) (← getLakeInstall?)
|
||
Package.load dir args (dir / file)
|
||
|
||
def loadConfig (args : List String := []) : CliStateM (Workspace × Package) := do
|
||
let pkg ← loadPkg args
|
||
let ws ← Workspace.ofPackage pkg
|
||
let packageMap ← resolveDeps ws pkg |>.run LogMethods.eio (m := IO)
|
||
let packageMap := packageMap.insert pkg.name pkg
|
||
({ws with packageMap}, pkg)
|
||
|
||
/-- Get the Lean installation. Error if missing. -/
|
||
def getLeanInstall : CliStateM LeanInstall := do
|
||
if let some leanInstall ← getLeanInstall? then
|
||
return leanInstall
|
||
else
|
||
error "could not detect a Lean installation"
|
||
|
||
/-- Get the Lake installation. Error if missing. -/
|
||
def getLakeInstall : CliStateM LakeInstall := do
|
||
if let some lakeInstall ← getLakeInstall? then
|
||
return lakeInstall
|
||
else
|
||
error "could not detect the configuration of the Lake installation"
|
||
|
||
/-- Get the Lean and Lake installation. Error if either is missing. -/
|
||
def getInstall : CliStateM (LeanInstall × LakeInstall) := do
|
||
return (← getLeanInstall, ← getLakeInstall)
|
||
|
||
/-- Perform the given build action using information from CLI. -/
|
||
def runBuildM (ws : Workspace) (x : BuildM α) : CliStateM α := do
|
||
let (leanInstall, lakeInstall) ← getInstall
|
||
let ctx ← mkBuildContext ws leanInstall lakeInstall
|
||
x.run LogMethods.io ctx
|
||
|
||
/-- Variant of `runBuildM` that discards the build monad's output. -/
|
||
def runBuildM_ (ws : Workspace) (x : BuildM α) : CliStateM PUnit :=
|
||
discard <| runBuildM ws x
|
||
|
||
-- ## Argument Parsing
|
||
|
||
def takeArg (errMsg : String := "missing argument") : CliM String := do
|
||
match (← takeArg?) with
|
||
| none => error errMsg
|
||
| some arg => 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
|
||
error s!"unexpected arguments: {" ".intercalate args}"
|
||
|
||
-- ## Option Parsing
|
||
|
||
def unknownShortOption (opt : Char) : CliM PUnit :=
|
||
error s!"unknown short option '-{opt}'"
|
||
|
||
def shortOption : (opt : Char) → CliM PUnit
|
||
| 'h' => setWantsHelp
|
||
| 'd' => do setRootDir <| ← takeArg "missing path after -d"
|
||
| 'f' => do setConfigFile <| ← takeArg "missing path after -f"
|
||
| opt => unknownShortOption opt
|
||
|
||
def unknownLongOption (opt : String) : CliM PUnit :=
|
||
error s!"unknown long option '{opt}'"
|
||
|
||
def longOption : (opt : String) → CliM PUnit
|
||
| "--help" => setWantsHelp
|
||
| "--dir" => do setRootDir <| ← takeArg "missing path after --dir"
|
||
| "--file" => do setConfigFile <| ← takeArg "missing path after --file"
|
||
| "--lean" => do setLean <| ← takeArg "missing command after --lean"
|
||
| "--" => do setSubArgs <| ← takeArgs
|
||
| opt => unknownLongOption opt
|
||
|
||
def lakeOption :=
|
||
option {
|
||
short := shortOption
|
||
long := longOption
|
||
longShort := shortOptionWithArg shortOption
|
||
}
|
||
|
||
-- ## Commands
|
||
|
||
def withPackage [MonadLiftT m CliStateM] (x : Package → LakeT m α) : CliStateM α := do
|
||
let (ws, pkg) ← loadConfig
|
||
let (lean, lake) ← getInstall
|
||
liftM <| x pkg |>.run {lean, lake, opaqueWs := ws}
|
||
|
||
def withContext [MonadLiftT m CliStateM] (x : LakeT m α) : CliStateM α :=
|
||
withPackage fun _ => x
|
||
|
||
/-- Run the given script from the given package with the given arguments. -/
|
||
def script (pkg : Package) (name : String) (args : List String) : CliStateM PUnit := do
|
||
if let some script := pkg.scripts.find? name then
|
||
if (← getWantsHelp) then
|
||
if let some help := script.help? then
|
||
IO.print help
|
||
else
|
||
error s!"no documentation provided for `{name}`"
|
||
else
|
||
exit <| ← withContext <| script.run args
|
||
else
|
||
pkg.scripts.forM (m := CliStateM) fun name _ => do
|
||
IO.println <| name.toString (escape := false)
|
||
error s!"unknown script '{name}'"
|
||
|
||
/-- Verify the Lean version Lake was built with matches that of the Lean installation. -/
|
||
def verifyLeanVersion : CliStateM PUnit := do
|
||
let lean ← getLeanInstall
|
||
unless lean.githash == Lean.githash do
|
||
let githash := if lean.githash.isEmpty then "nothing" else lean.githash
|
||
error s!"expected Lean commit {Lean.githash}, but got {lean.githash}"
|
||
|
||
/-- Output the detected installs and verify the Lean version. -/
|
||
def verifyInstall : CliStateM PUnit := do
|
||
IO.println s!"Lean:\n{repr <| ← getLeanInstall?}"
|
||
IO.println s!"Lake:\n{repr <| ← getLakeInstall?}"
|
||
verifyLeanVersion
|
||
|
||
/-- Exit code to return if `print-paths` cannot find the config file. -/
|
||
def noConfigFileCode : ExitCode := 2
|
||
|
||
/--
|
||
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 (imports : List String := []) : CliStateM PUnit := do
|
||
let (lean, lake) ← getInstall
|
||
let configFile := (← getRootDir) / (← getConfigFile)
|
||
if (← configFile.pathExists) then
|
||
let (ws, pkg) ← loadConfig (← getSubArgs)
|
||
let ctx ← mkBuildContext ws lean lake
|
||
pkg.buildImportsAndDeps imports |>.run LogMethods.eio ctx
|
||
IO.println <| Json.compress <| toJson ws.leanPaths
|
||
else
|
||
exit noConfigFileCode
|
||
|
||
def env (cmd : String) (args : Array String := #[]) : LakeT IO UInt32 := do
|
||
IO.Process.spawn {cmd, args, env := ← getLeanEnv} >>= (·.wait)
|
||
|
||
def serve (pkg : Package) (args : Array String := #[]) : LakeT IO UInt32 := do
|
||
env (← getLean).toString <| #["--server"] ++ pkg.moreServerArgs ++ args
|
||
|
||
def command : (cmd : String) → CliM PUnit
|
||
| "new" => do
|
||
processOptions lakeOption
|
||
let pkgName ← takeArg "missing package name"
|
||
noArgsRem <| new pkgName
|
||
| "init" => do
|
||
processOptions lakeOption
|
||
let pkgName ← takeArg "missing package name"
|
||
noArgsRem <| init pkgName
|
||
| "run" => do
|
||
processOptions lakeOption
|
||
let scriptName ← takeArg "missing script name"
|
||
noArgsRem <| script (← loadPkg) scriptName (← getSubArgs)
|
||
| "env" => do
|
||
let cmd ← takeArg "missing command"; let args ← takeArgs
|
||
exit <| ← withContext <| env cmd args.toArray
|
||
| "serve" => do
|
||
let args ← getSubArgs
|
||
noArgsRem <| exit <| ← withPackage fun pkg => serve pkg args.toArray
|
||
| "configure" => do
|
||
processOptions lakeOption
|
||
let (ws, pkg) ← loadConfig (← getSubArgs)
|
||
noArgsRem <| runBuildM ws pkg.buildDepOleans
|
||
| "print-paths" => do
|
||
processOptions lakeOption
|
||
printPaths (← takeArgs)
|
||
| "build" => do
|
||
processOptions lakeOption
|
||
let (ws, pkg) ← loadConfig (← getSubArgs)
|
||
runBuildM ws <| build pkg (← takeArgs)
|
||
| "clean" => do
|
||
processOptions lakeOption
|
||
noArgsRem <| (← loadPkg (← getSubArgs)).clean
|
||
| "self-check" => do
|
||
processOptions lakeOption
|
||
noArgsRem <| verifyInstall
|
||
| "help" => do
|
||
IO.println <| help (← takeArg?)
|
||
| cmd =>
|
||
error s!"unknown command '{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 error "expected command"
|
||
|
||
end Cli
|
||
|
||
open Cli in
|
||
def CliM.run (self : CliM α) (args : List String) : IO UInt32 := do
|
||
let (leanInstall?, lakeInstall?) ← findInstall?
|
||
match (← self args |>.run' {leanInstall?, lakeInstall?} |>.toIO') with
|
||
| Except.ok _ => pure 0
|
||
| Except.error rc => pure rc
|
||
|
||
def cli (args : List String) : IO UInt32 :=
|
||
Cli.processArgs.run args
|