1
1
mirror of https://github.com/anoma/juvix.git synced 2024-12-14 17:32:00 +03:00
juvix/app/Commands/Init.hs
Paul Cadman 68d4314c78
Migrate all Juvix projects from juvix.yaml to Package.juvix in the repository (#2503)
This PR:

* Modifies entry point `_entryPointBuildDir` to use the `BuildDir` type
instead of `SomeBase Dir`. This allows delayed resolution of the default
build directory which was useful for the Package -> Concrete translation
point below.
* Modifies `juvix dev root` to render the current package as a
Package.juvix file.
* Modifies the Package -> Concrete translation to recognise default
arguments. So, for example, an empty `juvix.yaml` file will be
translated into the following (instead of the `name`, `version`, and
`dependencies` arguments being populated).

        
        module Package;

        import Stdlib.Prelude open;
        import PackageDescription.V1 open;

        package : Package := defaultPackage;
        
* Adds a temporary command (removed when juvix.yaml support is removed)
`juvix dev migrate-juvix-yaml` that translates `juvix.yaml` into an
equivalent `Package.juvix` in the current project.
* Adds a temporary script `migrate-juvix-yaml.sh` (removed when
juvix.yaml support is removed) which can be run in the project to
translate all Juvix projects in the repository.
* Actually translate all of the `juvix.yaml` files to `Package.juvix`
using the script.

* Part of https://github.com/anoma/juvix/issues/2487
2023-11-07 18:11:02 +00:00

168 lines
5.1 KiB
Haskell

module Commands.Init where
import Commands.Extra.Package
import Commands.Init.Options
import Data.Text qualified as Text
import Data.Versions
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) => InitOptions -> Sem r ()
init opts = do
checkNotInProject
pkg <-
if
| isInteractive -> do
say "✨ Your next Juvix adventure is about to begin! ✨"
say "I will help you set it up"
getPackage
| otherwise -> do
cwd <- getCurrentDir
projectName <- getDefaultProjectName
let emptyPkg = emptyPackage DefaultBuildDir (cwd <//> packageFilePath)
return $ case projectName of
Nothing -> emptyPkg
Just n -> emptyPkg {_packageName = n}
when isInteractive (say ("creating " <> pack (toFilePath packageFilePath)))
cwd <- getCurrentDir
writePackageFile cwd pkg
checkPackage
when isInteractive (say "you are all set")
where
isInteractive :: Bool
isInteractive = not (opts ^. initOptionsNonInteractive)
checkNotInProject :: forall r. (Members '[Embed IO] r) => Sem r ()
checkNotInProject =
whenM (orM [doesFileExist juvixYamlFile, doesFileExist packageFilePath]) err
where
err :: Sem r ()
err = do
say "You are already in a Juvix project"
embed exitFailure
checkPackage :: forall r. (Members '[Embed IO] r) => Sem r ()
checkPackage = do
cwd <- getCurrentDir
ep <- runError @JuvixError (loadPackageFileIO cwd DefaultBuildDir)
case ep of
Left {} -> do
say "Package.juvix is invalid. Please raise an issue at https://github.com/anoma/juvix/issues"
embed exitFailure
Right {} -> return ()
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
}
getDefaultProjectName :: (Member (Embed IO) r) => Sem r (Maybe Text)
getDefaultProjectName = runFail $ do
dir <- map toLower . dropTrailingPathSeparator . toFilePath . dirname <$> getCurrentDir
Fail.fromRight (parse projectNameParser (pack dir))
getProjName :: forall r. (Members '[Embed IO] r) => Sem r Text
getProjName = do
d <- getDefaultProjectName
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
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