lean4-htt/Lake/CLI/Main.lean
2021-12-23 23:43:01 -05:00

303 lines
9.8 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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