mirror of
https://github.com/anoma/juvix.git
synced 2024-12-15 10:03:22 +03:00
dad3963c00
This PR adds lock file support to the compiler pipeline. The lock file is generated whenever a compiler pipeline command (`juvix {compile, typecheck, repl}`) is run. The lock file contains all the information necessary to reproduce the whole dependency source tree. In particular for git dependencies, branch/tag references are resolved to git hash references. ## Lock file format The lock file is a YAML `juvix.lock.yaml` file written by the compiler alongside the package's `juvix.yaml` file. ``` LOCKFILE_SPEC: { dependencies: { DEPENDENCY_SPEC, dependencies: LOCKFILE_SPEC } DEPENDENCY_SPEC: PATH_SPEC | GIT_SPEC PATH_SPEC: { path: String } GIT_SPEC: { git: {url: String, ref: String, name: String } } ``` ## Example Consider a project containing the following `juvix.yaml`: ```yaml dependencies: - .juvix-build/stdlib/ - git: url: https://github.com/anoma/juvix-containers ref: v0.7.1 name: containers name: example version: 1.0.0 ``` After running `juvix compile` the following lockfile `juvix.lock.yaml` is generated. ```yaml # This file was autogenerated by Juvix version 0.5.1. # Do not edit this file manually. dependencies: - path: .juvix-build/stdlib/ dependencies: [] - git: name: containers ref: 3debbc7f5776924eb9652731b3c1982a2ee0ff24 url: https://github.com/anoma/juvix-containers dependencies: - git: name: stdlib ref: 4facf14d9b2d06b81ce1be1882aa9050f768cb45 url: https://github.com/anoma/juvix-stdlib dependencies: [] - git: name: test ref: a7ac74cac0db92e0b5e349f279d797c3788cdfdd url: https://github.com/anoma/juvix-test dependencies: - git: name: stdlib ref: 4facf14d9b2d06b81ce1be1882aa9050f768cb45 url: https://github.com/anoma/juvix-stdlib dependencies: [] ``` For subsequent runs of the juvix compile pipeline, the lock file dependency information is used. ## Behaviour when package file and lock file are out of sync If a dependency is specified in `juvix.yaml` that is not present in the lock file, an error is raised. Continuing the example above, say we add an additional dependency: ``` dependencies: - .juvix-build/stdlib/ - git: url: https://github.com/anoma/juvix-containers ref: v0.7.1 name: containers - git: url: https://github.com/anoma/juvix-test ref: v0.6.1 name: test name: example version: 1.0.0 ``` `juvix compile` will throw an error: ``` /Users/paul/tmp/lockfile/dep/juvix.yaml:1:1: error: The dependency test is declared in the package's juvix.yaml but is not declared in the lockfile: /Users/paul/tmp/lockfile/dep/juvix.lock.json Try removing /Users/paul/tmp/lockfile/dep/juvix.lock.yaml and then run Juvix again. ``` Closes: * https://github.com/anoma/juvix/issues/2334
141 lines
4.1 KiB
Haskell
141 lines
4.1 KiB
Haskell
module Commands.Init where
|
|
|
|
import Data.Text qualified as Text
|
|
import Data.Versions
|
|
import Data.Yaml (encodeFile)
|
|
import Juvix.Compiler.Pipeline.Package
|
|
import Juvix.Data.Effect.Fail.Extra qualified as Fail
|
|
import Juvix.Extra.Paths
|
|
import Juvix.Prelude
|
|
import Juvix.Prelude.Pretty
|
|
import Text.Megaparsec (Parsec)
|
|
import Text.Megaparsec qualified as P
|
|
import Text.Megaparsec.Char qualified as P
|
|
|
|
type Err = Text
|
|
|
|
parse :: Parsec Void Text a -> Text -> Either Err a
|
|
parse p t = mapLeft ppErr (P.runParser p "<stdin>" t)
|
|
|
|
ppErr :: P.ParseErrorBundle Text Void -> Text
|
|
ppErr = pack . errorBundlePretty
|
|
|
|
init :: forall r. (Members '[Embed IO] r) => Sem r ()
|
|
init = do
|
|
checkNotInProject
|
|
say "✨ Your next Juvix adventure is about to begin! ✨"
|
|
say "I will help you set it up"
|
|
pkg <- getPackage
|
|
say ("creating " <> pack (toFilePath juvixYamlFile))
|
|
embed (encodeFile (toFilePath juvixYamlFile) (rawPackage pkg))
|
|
say "you are all set"
|
|
|
|
checkNotInProject :: forall r. (Members '[Embed IO] r) => Sem r ()
|
|
checkNotInProject =
|
|
whenM (doesFileExist juvixYamlFile) err
|
|
where
|
|
err :: Sem r ()
|
|
err = do
|
|
say "You are already in a Juvix project"
|
|
embed exitFailure
|
|
|
|
getPackage :: forall r. (Members '[Embed IO] r) => Sem r Package
|
|
getPackage = do
|
|
tproj <- getProjName
|
|
say "Write the version of your project [leave empty for 0.0.0]"
|
|
tversion :: SemVer <- getVersion
|
|
cwd <- getCurrentDir
|
|
return
|
|
Package
|
|
{ _packageName = tproj,
|
|
_packageVersion = tversion,
|
|
_packageBuildDir = Nothing,
|
|
_packageMain = Nothing,
|
|
_packageDependencies = [defaultStdlibDep DefaultBuildDir],
|
|
_packageFile = cwd <//> juvixYamlFile,
|
|
_packageLockfile = Nothing
|
|
}
|
|
|
|
getProjName :: forall r. (Members '[Embed IO] r) => Sem r Text
|
|
getProjName = do
|
|
d <- getDefault
|
|
let defMsg :: Text
|
|
defMsg = case d of
|
|
Nothing -> mempty
|
|
Just d' -> " [leave empty for '" <> d' <> "']"
|
|
say
|
|
( "Write the name of your project"
|
|
<> defMsg
|
|
<> " (lower case letters, numbers and dashes are allowed): "
|
|
)
|
|
readName d
|
|
where
|
|
getDefault :: Sem r (Maybe Text)
|
|
getDefault = runFail $ do
|
|
dir <- map toLower . dropTrailingPathSeparator . toFilePath . dirname <$> getCurrentDir
|
|
Fail.fromRight (parse projectNameParser (pack dir))
|
|
readName :: Maybe Text -> Sem r Text
|
|
readName def = go
|
|
where
|
|
go :: Sem r Text
|
|
go = do
|
|
txt <- embed getLine
|
|
if
|
|
| Text.null txt, Just def' <- def -> return def'
|
|
| otherwise ->
|
|
case parse projectNameParser txt of
|
|
Right p
|
|
| Text.length p <= projextNameMaxLength -> return p
|
|
| otherwise -> do
|
|
say ("The project name cannot exceed " <> prettyText projextNameMaxLength <> " characters")
|
|
retry
|
|
Left err -> do
|
|
say err
|
|
retry
|
|
where
|
|
retry :: Sem r Text
|
|
retry = do
|
|
tryAgain
|
|
go
|
|
|
|
say :: (Members '[Embed IO] r) => Text -> Sem r ()
|
|
say = embed . putStrLn
|
|
|
|
tryAgain :: (Members '[Embed IO] r) => Sem r ()
|
|
tryAgain = say "Please, try again:"
|
|
|
|
getVersion :: forall r. (Members '[Embed IO] r) => Sem r SemVer
|
|
getVersion = do
|
|
txt <- embed getLine
|
|
if
|
|
| Text.null txt -> return defaultVersion
|
|
| otherwise -> case parse semver' txt of
|
|
Right r -> return r
|
|
Left err -> do
|
|
say err
|
|
say "The version must follow the 'Semantic Versioning 2.0.0' specification"
|
|
retry
|
|
where
|
|
retry :: Sem r SemVer
|
|
retry = do
|
|
tryAgain
|
|
getVersion
|
|
|
|
projextNameMaxLength :: Int
|
|
projextNameMaxLength = 100
|
|
|
|
projectNameParser :: Parsec Void Text Text
|
|
projectNameParser = do
|
|
h <- P.satisfy validFirstChar
|
|
t <- P.takeWhileP (Just "project name character") validChar
|
|
P.hspace
|
|
P.eof
|
|
return (Text.cons h t)
|
|
where
|
|
validFirstChar :: Char -> Bool
|
|
validFirstChar c =
|
|
isAscii c
|
|
&& (isLower c || isNumber c)
|
|
validChar :: Char -> Bool
|
|
validChar c = c == '-' || validFirstChar c
|