diff --git a/app/Spago.hs b/app/Spago.hs index d5e97e14d..c881559f4 100644 --- a/app/Spago.hs +++ b/app/Spago.hs @@ -19,6 +19,7 @@ import qualified Spago.Version import qualified Spago.Command.Ls as Ls import qualified Spago.Command.Path as Path import qualified Spago.Command.Verify as Verify +import qualified Spago.Command.Init as Init main :: IO () @@ -45,7 +46,7 @@ main = withUtf8 $ do -- ### Commands that need only a basic global env Init force noComments tag - -> void $ Spago.Packages.initProject force noComments tag + -> void $ Init.initProject force noComments tag Freeze -> Spago.PackageSet.freeze Spago.PackageSet.packagesPath Version @@ -88,16 +89,19 @@ main = withUtf8 $ do $ Verify.verify checkUniqueModules Nothing -- ### Commands that need a build environment: a config, build options and access to purs - Build buildOptions -> Run.withBuildEnv globalUsePsa - $ Spago.Build.build buildOptions Nothing - Search -> Run.withBuildEnv globalUsePsa + Build buildOptions -> Run.withBuildEnv globalUsePsa buildOptions + $ Spago.Build.build Nothing + Search -> Run.withBuildEnv globalUsePsa defaultBuildOptions $ Spago.Build.search - Docs format sourcePaths depsOnly noSearch openDocs -> Run.withBuildEnv globalUsePsa - $ Spago.Build.docs format sourcePaths depsOnly noSearch openDocs - Test modName buildOptions nodeArgs -> Run.withBuildEnv globalUsePsa - $ Spago.Build.test modName buildOptions nodeArgs - Run modName buildOptions nodeArgs -> Run.withBuildEnv globalUsePsa - $ Spago.Build.run modName buildOptions nodeArgs + Docs format sourcePaths depsOnly noSearch openDocs -> + let + opts = defaultBuildOptions { depsOnly = depsOnly, sourcePaths = sourcePaths } + in Run.withBuildEnv globalUsePsa opts + $ Spago.Build.docs format noSearch openDocs + Test modName buildOptions nodeArgs -> Run.withBuildEnv globalUsePsa buildOptions + $ Spago.Build.test modName nodeArgs + Run modName buildOptions nodeArgs -> Run.withBuildEnv globalUsePsa buildOptions + $ Spago.Build.run modName nodeArgs -- ### Legacy commands, here for smoother migration path to new ones Bundle -> die [ display Messages.bundleCommandRenamed ] diff --git a/package.yaml b/package.yaml index 28043878a..ce23d30cc 100644 --- a/package.yaml +++ b/package.yaml @@ -98,13 +98,11 @@ library: - dhall >= 1.38.0 - directory >= 1.3.4.0 - either - - exceptions - file-embed - filepath - foldl - fsnotify - generic-lens - - github - Glob - http-types - http-client @@ -134,7 +132,6 @@ library: - unliftio - unordered-containers - utf8-string - - vector - versions - with-utf8 - zlib @@ -163,6 +160,7 @@ executables: - -rtsopts - -with-rtsopts=-N dependencies: + - ansi-terminal - base >= 4.7 && < 5 - spago - text < 1.3 diff --git a/spago.cabal b/spago.cabal index 58998d653..80baa6821 100644 --- a/spago.cabal +++ b/spago.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: ab7469432b7c2f686c6414d0dae5b3cd0c8204f64bdd993199ec586d21964698 +-- hash: 77b5d785b2feb191ce5732a72494d5d950758a3c9dd77a3c98827bf19c3bafa3 name: spago version: 0.20.0 @@ -43,8 +43,8 @@ library Spago.Async Spago.Bower Spago.Build - Spago.Build.Parser Spago.CLI + Spago.Command.Init Spago.Command.Ls Spago.Command.Path Spago.Command.Verify @@ -89,13 +89,11 @@ library , dhall >=1.38.0 , directory >=1.3.4.0 , either - , exceptions , file-embed , filepath , foldl , fsnotify , generic-lens - , github , http-client , http-conduit , http-types @@ -124,7 +122,6 @@ library , unliftio , unordered-containers , utf8-string - , vector , versions , with-utf8 , zlib @@ -158,7 +155,6 @@ test-suite spec main-is: Main.hs other-modules: BumpVersionSpec - Spago.Build.ParserSpec Spago.PursSpec SpagoSpec Spec diff --git a/src/Spago/Build.hs b/src/Spago/Build.hs index 6d30d106f..0fd6575d4 100644 --- a/src/Spago/Build.hs +++ b/src/Spago/Build.hs @@ -18,7 +18,6 @@ import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Map as Map import qualified Data.Set as Set import qualified Data.Text as Text -import qualified Data.Versions as Version import System.Directory (getCurrentDirectory) import System.FilePath (splitDirectories) import qualified System.FilePath.Glob as Glob @@ -29,7 +28,6 @@ import qualified Turtle import qualified System.Process as Process import qualified Web.Browser as Browser -import qualified Spago.Build.Parser as Parse import qualified Spago.Command.Path as Path import qualified Spago.RunEnv as Run import qualified Spago.Config as Config @@ -51,31 +49,19 @@ prepareBundleDefaults maybeModuleName maybeTargetPath = (moduleName, targetPath) targetPath = fromMaybe (TargetPath "index.js") maybeTargetPath -- eventually running some other action after the build -build - :: HasBuildEnv env - => BuildOptions -> Maybe (RIO Env ()) - -> RIO env () -build BuildOptions{..} maybePostBuild = do +build :: HasBuildEnv env => Maybe (RIO Env ()) -> RIO env () +build maybePostBuild = do logDebug "Running `spago build`" + BuildOptions{..} <- view (the @BuildOptions) Config{..} <- view (the @Config) - PursCmd { compilerVersion } <- view (the @PursCmd) deps <- Packages.getProjectDeps - case noInstall of - DoInstall -> Fetch.fetchPackages deps - NoInstall -> pure () let partitionedGlobs@(Packages.Globs{..}) = Packages.getGlobs deps depsOnly configSourcePaths allPsGlobs = Packages.getGlobsSourcePaths partitionedGlobs <> sourcePaths allJsGlobs = Packages.getJsGlobs deps depsOnly configSourcePaths <> sourcePaths - checkImports globs = do - minVersion <- case Version.semver "0.14.0" of - Left _ -> die [ "Unable to parse min version for imports check" ] - Right minVersion -> pure minVersion - when (compilerVersion >= minVersion) $ do - graph <- Purs.graph globs - case graph of - Left err -> logWarn $ displayShow err - Right (Purs.ModuleGraph moduleGraph) -> do + checkImports = do + maybeGraph <- view (the @Graph) + for_ maybeGraph $ \(Purs.ModuleGraph moduleGraph) -> do let matchesGlob :: Sys.FilePath -> SourcePath -> Bool matchesGlob path sourcePath = @@ -88,11 +74,11 @@ build BuildOptions{..} maybePostBuild = do projectModules :: [ModuleName] projectModules = map fst - $ filter (\(_, Purs.ModuleGraphNode{..}) -> isProjectFile (Text.unpack path)) + $ filter (\(_, Purs.ModuleGraphNode{..}) -> isProjectFile (Text.unpack graphNodePath)) $ Map.toList moduleGraph getImports :: ModuleName -> Set ModuleName - getImports = maybe Set.empty (Set.fromList . Purs.depends) . flip Map.lookup moduleGraph + getImports = maybe Set.empty (Set.fromList . graphNodeDepends) . flip Map.lookup moduleGraph -- All package modules that are imported from our project files importedPackageModules :: Set ModuleName @@ -113,7 +99,7 @@ build BuildOptions{..} maybePostBuild = do importedPackages :: Set PackageName importedPackages = Set.fromList - $ mapMaybe (getPackageFromPath . Purs.path <=< flip Map.lookup moduleGraph) + $ mapMaybe (getPackageFromPath . graphNodePath <=< flip Map.lookup moduleGraph) $ Set.toList importedPackageModules dependencyPackages :: Set PackageName @@ -157,7 +143,7 @@ build BuildOptions{..} maybePostBuild = do shell backendCmd empty >>= \case ExitSuccess -> pure () ExitFailure n -> die [ "Backend " <> displayShow backend <> " exited with error:" <> repr n ] - checkImports globs + checkImports buildAction globs = do env <- Run.getEnv @@ -254,38 +240,29 @@ repl newPackages sourcePaths pursArgs depsOnly = do -- | Test the project: compile and run "Test.Main" -- (or the provided module name) with node -test - :: HasBuildEnv env - => Maybe ModuleName -> BuildOptions -> [BackendArg] - -> RIO env () -test maybeModuleName buildOpts extraArgs = do +test :: HasBuildEnv env => Maybe ModuleName -> [BackendArg] -> RIO env () +test maybeModuleName extraArgs = do let moduleName = fromMaybe (ModuleName "Test.Main") maybeModuleName - Config.Config { alternateBackend, configSourcePaths } <- view (the @Config) - liftIO (foldMapM (Glob.glob . Text.unpack . unSourcePath) configSourcePaths) >>= \paths -> do - results <- forM paths $ \path -> do - content <- readFileBinary path - pure $ Parse.checkModuleNameMatches (encodeUtf8 $ unModuleName moduleName) content - if or results - then do + Config.Config { alternateBackend } <- view (the @Config) + maybeGraph <- view (the @Graph) + -- We check if the test module is included in the build and spit out a nice error if it isn't (see #383) + for_ maybeGraph $ \(ModuleGraph moduleMap) -> case Map.lookup moduleName moduleMap of + Nothing -> die [ "Module '" <> (display . unModuleName) moduleName <> "' not found! Are you including it in your build?" ] + Just _ -> do sourceDir <- Turtle.pwd let dirs = RunDirectories sourceDir sourceDir - runBackend alternateBackend dirs moduleName (Just "Tests succeeded.") "Tests failed: " buildOpts extraArgs - else do - die [ "Module '" <> (display . unModuleName) moduleName <> "' not found! Are you including it in your build?" ] + runBackend alternateBackend dirs moduleName (Just "Tests succeeded.") "Tests failed: " extraArgs -- | Run the project: compile and run "Main" -- (or the provided module name) with node -run - :: HasBuildEnv env - => Maybe ModuleName -> BuildOptions -> [BackendArg] - -> RIO env () -run maybeModuleName buildOpts extraArgs = do +run :: HasBuildEnv env => Maybe ModuleName -> [BackendArg] -> RIO env () +run maybeModuleName extraArgs = do Config.Config { alternateBackend } <- view (the @Config) let moduleName = fromMaybe (ModuleName "Main") maybeModuleName sourceDir <- Turtle.pwd let dirs = RunDirectories sourceDir sourceDir - runBackend alternateBackend dirs moduleName Nothing "Running failed; " buildOpts extraArgs + runBackend alternateBackend dirs moduleName Nothing "Running failed; " extraArgs -- | Run the select module as a script: init, compile, and run the provided module @@ -296,7 +273,7 @@ script -> [PackageName] -> ScriptBuildOptions -> RIO env () -script modulePath tag packageDeps opts@ScriptBuildOptions{..} = do +script modulePath tag packageDeps opts = do logDebug "Running `spago script`" absoluteModulePath <- fmap Text.pack (makeAbsolute (Text.unpack modulePath)) currentDir <- Turtle.pwd @@ -325,21 +302,10 @@ script modulePath tag packageDeps opts@ScriptBuildOptions{..} = do let runDirs :: RunDirectories runDirs = RunDirectories scriptDirPath currentDir - Run.withBuildEnv' (Just config) NoPsa (runAction runDirs) + Run.withBuildEnv' (Just config) NoPsa buildOpts (runAction runDirs) where - runAction dirs = do - let - buildOpts = BuildOptions - { shouldClear = NoClear - , shouldWatch = BuildOnce - , allowIgnored = DoAllowIgnored - , sourcePaths = [] - , withSourceMap = WithoutSrcMap - , noInstall = DoInstall - , depsOnly = AllSources - , .. - } - runBackend Nothing dirs (ModuleName "Main") Nothing "Script failed to run; " buildOpts [] + buildOpts = fromScriptOptions defaultBuildOptions opts + runAction dirs = runBackend Nothing dirs (ModuleName "Main") Nothing "Script failed to run; " [] data RunDirectories = RunDirectories { sourceDir :: FilePath, executeDir :: FilePath } @@ -353,13 +319,13 @@ runBackend -> ModuleName -> Maybe Text -> Text - -> BuildOptions -> [BackendArg] -> RIO env () -runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybeSuccessMessage failureMessage buildOpts@BuildOptions{pursArgs} extraArgs = do +runBackend maybeBackend RunDirectories{ sourceDir, executeDir } moduleName maybeSuccessMessage failureMessage extraArgs = do logDebug $ display $ "Running with backend: " <> fromMaybe "nodejs" maybeBackend + BuildOptions{ pursArgs } <- view (the @BuildOptions) let postBuild = maybe (nodeAction $ Path.getOutputPath pursArgs) backendAction maybeBackend - build buildOpts (Just postBuild) + build (Just postBuild) where fromFilePath = Text.pack . Turtle.encodeString runJsSource = fromFilePath (sourceDir Turtle. ".spago/run.js") @@ -409,7 +375,7 @@ bundleApp withMain maybeModuleName maybeTargetPath noBuild buildOpts usePsa = let (moduleName, targetPath) = prepareBundleDefaults maybeModuleName maybeTargetPath bundleAction = Purs.bundle withMain (withSourceMap buildOpts) moduleName targetPath in case noBuild of - DoBuild -> Run.withBuildEnv usePsa $ build buildOpts (Just bundleAction) + DoBuild -> Run.withBuildEnv usePsa buildOpts $ build (Just bundleAction) NoBuild -> Run.getEnv >>= (flip runRIO) bundleAction -- | Bundle into a CommonJS module @@ -436,21 +402,20 @@ bundleModule maybeModuleName maybeTargetPath noBuild buildOpts usePsa = do Right _ -> logInfo $ display $ "Make module succeeded and output file to " <> unTargetPath targetPath Left (n :: SomeException) -> die [ "Make module failed: " <> repr n ] case noBuild of - DoBuild -> Run.withBuildEnv usePsa $ build buildOpts (Just bundleAction) + DoBuild -> Run.withBuildEnv usePsa buildOpts $ build (Just bundleAction) NoBuild -> Run.getEnv >>= (flip runRIO) bundleAction -- | Generate docs for the `sourcePaths` and run `purescript-docs-search build-index` to patch them. docs - :: (HasLogFunc env, HasConfig env) + :: HasBuildEnv env => Maybe Purs.DocsFormat - -> [SourcePath] - -> Packages.DepsOnly -> NoSearch -> OpenDocs -> RIO env () -docs format sourcePaths depsOnly noSearch open = do +docs format noSearch open = do logDebug "Running `spago docs`" + BuildOptions { sourcePaths, depsOnly } <- view (the @BuildOptions) Config{..} <- view (the @Config) deps <- Packages.getProjectDeps logInfo "Generating documentation for the project. This might take a while..." @@ -486,9 +451,7 @@ docs format sourcePaths depsOnly noSearch open = do openLink link = liftIO $ Browser.openBrowser (Text.unpack link) -- | Start a search REPL. -search - :: (HasPurs env, HasLogFunc env, HasConfig env) - => RIO env () +search :: HasBuildEnv env => RIO env () search = do Config{..} <- view (the @Config) deps <- Packages.getProjectDeps diff --git a/src/Spago/Build/Parser.hs b/src/Spago/Build/Parser.hs deleted file mode 100644 index 816ed5a28..000000000 --- a/src/Spago/Build/Parser.hs +++ /dev/null @@ -1,143 +0,0 @@ -module Spago.Build.Parser - ( ModuleExportType (..) - , PsModule (..) - , DataMembers (..) - , moduleDecl - , checkModuleNameMatches - ) where - -import qualified RIO.ByteString as ByteString -import qualified RIO.Char as Char -import qualified RIO.NonEmpty.Partial as NonEmpty -import Spago.Prelude hiding (many, try, some) - -import qualified Data.ByteString.Internal as ByteString (w2c) - -import Text.Megaparsec -import Text.Megaparsec.Byte -import qualified Text.Megaparsec.Byte.Lexer as Lexer - -type Parser = Parsec Void ByteString - -data PsModule = PsModule - { psModuleName :: ByteString - , psModuleExportList :: Maybe (NonEmpty ModuleExportType) - } deriving (Eq, Show) - -data ModuleExportType - = ExportValue ByteString - | ExportOp ByteString - | ExportType ByteString (Maybe DataMembers) - | ExportTypeOp ByteString - | ExportClass ByteString - | ExportKind ByteString - | ExportModule ByteString - deriving (Eq, Show) - -data DataMembers - = DataAll - | DataEnumerated [ByteString] - deriving (Eq, Show) - -moduleDecl :: Parser PsModule -moduleDecl = between (sc *> symbol "module") (symbol "where") $ - PsModule - <$> lexeme moduleFullName - <*> optional moduleExports - -moduleFullName :: Parser ByteString -moduleFullName = lexeme (ByteString.intercalate "." <$> sepBy moduleName (symbol ".")) - -moduleName :: Parser ByteString -moduleName = toName <$> upperChar <*> many alphaNumChar - where - toName pre rest = ByteString.pack (pre:rest) - -moduleExports :: Parser (NonEmpty ModuleExportType) -moduleExports = lexeme $ between (symbol "(") (symbol ")") moduleExportList - -moduleExportList :: Parser (NonEmpty ModuleExportType) -moduleExportList = do - exports <- sepBy moduleExport (symbol ",") - if null exports then - fail "Invalid syntax: export list" - else - return $ NonEmpty.fromList exports - -moduleExport :: Parser ModuleExportType -moduleExport = choice - [ try $ choice [ exportTypeOp, exportClass, exportKind, exportModule ] - , exportType - , exportOp - , exportValue - ] - -exportTypeOp :: Parser ModuleExportType -exportTypeOp = ExportTypeOp <$ symbol "type" <*> exportSymbol - -exportClass :: Parser ModuleExportType -exportClass = ExportClass <$ symbol "class" <*> properNameUpper - -exportKind :: Parser ModuleExportType -exportKind = ExportKind <$ symbol "kind" <*> properNameUpper - -exportModule :: Parser ModuleExportType -exportModule = ExportModule <$ symbol "module" <*> moduleFullName <* notFollowedBy identChar - -exportValue :: Parser ModuleExportType -exportValue = ExportValue <$> properNameLower - -exportOp :: Parser ModuleExportType -exportOp = ExportOp <$> exportSymbol - -exportSymbol :: Parser ByteString -exportSymbol = ByteString.pack <$> between (string "(") (string ")") (many (satisfy isSymbolChar)) - where - isSymbolChar c = c `ByteString.elem` ":!#$%&*+./<=>?@\\^|-~" - || ((not . Char.isAscii) $ ByteString.w2c c) && (Char.isSymbol $ ByteString.w2c c) - -exportType :: Parser ModuleExportType -exportType = ExportType <$> properNameUpper <*> optional dataMembers - -properNameUpper :: Parser ByteString -properNameUpper = lexeme (properName upperChar) - -properNameLower :: Parser ByteString -properNameLower = lexeme (properName lowerChar) - -properName :: Parser Word8 -> Parser ByteString -properName pre = f <$> pre <*> many identChar - where - f x xs = ByteString.pack (x:xs) - -dataMembers :: Parser DataMembers -dataMembers = lexeme $ choice - [ DataAll <$ string "(..)" - , DataEnumerated [] <$ string "()" - , DataEnumerated <$> between (string "(") (string ")") (sepBy properNameUpper (symbol ",")) - ] - -underscore :: Parser Word8 -underscore = char 95 - -identChar :: Parser Word8 -identChar = alphaNumChar <|> underscore <|> tick - where - tick = char 39 -- ' - -lexeme :: Parser a -> Parser a -lexeme = Lexer.lexeme sc - -symbol :: ByteString -> Parser ByteString -symbol = Lexer.symbol sc - -sc :: Parser () -sc = Lexer.space - space1 - (Lexer.skipLineComment "--") - (Lexer.skipBlockComment "{-" "-}") - -checkModuleNameMatches :: ByteString -> ByteString -> Bool -checkModuleNameMatches expectedModuleName content = case parse moduleDecl "" content of - Left _ -> False - Right (PsModule name _) -> name == expectedModuleName diff --git a/src/Spago/Command/Init.hs b/src/Spago/Command/Init.hs new file mode 100644 index 000000000..364c06481 --- /dev/null +++ b/src/Spago/Command/Init.hs @@ -0,0 +1,65 @@ +module Spago.Command.Init ( initProject ) where + +import Spago.Prelude +import Spago.Env + +import qualified Spago.Config as Config +import qualified Spago.Dhall as Dhall +import qualified Spago.Messages as Messages +import qualified Spago.PackageSet as PackageSet +import qualified Spago.Templates as Templates +import qualified Spago.RunEnv as Run + + +-- | Init a new Spago project: +-- - create `packages.dhall` to manage the package set, overrides, etc +-- - create `spago.dhall` to manage project config: name, deps, etc +-- - create an example `src` folder (if needed) +-- - create an example `test` folder (if needed) +initProject + :: HasEnv env + => Force -> Dhall.TemplateComments -> Maybe Text + -> RIO env Config +initProject force comments tag = do + logInfo "Initializing a sample project or migrating an existing one.." + + -- packages.dhall and spago.dhall overwrite can be forced + PackageSet.makePackageSetFile force comments + config <- Config.makeConfig force comments + + -- Use the specified version of the package set (if specified). + -- Otherwise, get the latest version of the package set if possible + Run.withPursEnv NoPsa $ do + PackageSet.updatePackageSetVersion tag + + -- If these directories (or files) exist, we skip copying "sample sources" + -- Because you might want to just init a project with your own source files, + -- or just migrate a psc-package project + whenDirNotExists "src" $ do + copyIfNotExists "src/Main.purs" Templates.srcMain + + whenDirNotExists "test" $ do + copyIfNotExists "test/Main.purs" Templates.testMain + + copyIfNotExists ".gitignore" Templates.gitignore + + copyIfNotExists ".purs-repl" Templates.pursRepl + + logInfo "Set up a local Spago project." + logInfo "Try running `spago build`" + pure config + + where + whenDirNotExists dir action = do + let dirPath = pathFromText dir + dirExists <- testdir dirPath + case dirExists of + True -> logInfo $ display $ Messages.foundExistingDirectory dir + False -> do + mktree dirPath + action + + copyIfNotExists dest srcTemplate = do + testfile dest >>= \case + True -> logInfo $ display $ Messages.foundExistingFile dest + False -> writeTextFile dest srcTemplate \ No newline at end of file diff --git a/src/Spago/Env.hs b/src/Spago/Env.hs index 60a0aa0db..8fc9823bf 100644 --- a/src/Spago/Env.hs +++ b/src/Spago/Env.hs @@ -67,6 +67,8 @@ type HasEnv env = type HasConfig env = ( HasType Config env, HasPackageSet env ) type HasMaybeConfig env = ( HasType (Maybe Config) env, HasPackageSet env ) +type HasMaybeGraph env = HasType (Maybe ModuleGraph) env +type HasBuildOptions env = HasType BuildOptions env type HasVerifyEnv env = ( HasLogFunc env @@ -90,6 +92,8 @@ type HasBuildEnv env = , HasPurs env , HasGit env , HasConfig env + , HasMaybeGraph env + , HasBuildOptions env ) type HasPursEnv env = @@ -147,6 +151,8 @@ data BuildEnv = BuildEnv , envGitCmd :: !GitCmd , envPackageSet :: !PackageSet , envConfig :: !Config + , envGraph :: !(Maybe ModuleGraph) + , envBuildOptions :: !BuildOptions } deriving (Generic) data PursEnv = PursEnv diff --git a/src/Spago/Packages.hs b/src/Spago/Packages.hs index eebea9d7b..842bcc126 100644 --- a/src/Spago/Packages.hs +++ b/src/Spago/Packages.hs @@ -1,6 +1,5 @@ module Spago.Packages - ( initProject - , install + ( install , sources , getGlobs , getGlobsSourcePaths @@ -23,66 +22,7 @@ import qualified Data.Set as Set import qualified Data.Text as Text import qualified Spago.Config as Config -import qualified Spago.Dhall as Dhall import qualified Spago.FetchPackage as Fetch -import qualified Spago.Messages as Messages -import qualified Spago.PackageSet as PackageSet -import qualified Spago.Templates as Templates -import qualified Spago.RunEnv as Run - - --- | Init a new Spago project: --- - create `packages.dhall` to manage the package set, overrides, etc --- - create `spago.dhall` to manage project config: name, deps, etc --- - create an example `src` folder (if needed) --- - create an example `test` folder (if needed) -initProject - :: HasEnv env - => Force -> Dhall.TemplateComments -> Maybe Text - -> RIO env Config -initProject force comments tag = do - logInfo "Initializing a sample project or migrating an existing one.." - - -- packages.dhall and spago.dhall overwrite can be forced - PackageSet.makePackageSetFile force comments - config <- Config.makeConfig force comments - - -- Use the specified version of the package set (if specified). - -- Otherwise, get the latest version of the package set if possible - Run.withPursEnv NoPsa $ do - PackageSet.updatePackageSetVersion tag - - -- If these directories (or files) exist, we skip copying "sample sources" - -- Because you might want to just init a project with your own source files, - -- or just migrate a psc-package project - whenDirNotExists "src" $ do - copyIfNotExists "src/Main.purs" Templates.srcMain - - whenDirNotExists "test" $ do - copyIfNotExists "test/Main.purs" Templates.testMain - - copyIfNotExists ".gitignore" Templates.gitignore - - copyIfNotExists ".purs-repl" Templates.pursRepl - - logInfo "Set up a local Spago project." - logInfo "Try running `spago build`" - pure config - - where - whenDirNotExists dir action = do - let dirPath = pathFromText dir - dirExists <- testdir dirPath - case dirExists of - True -> logInfo $ display $ Messages.foundExistingDirectory dir - False -> do - mktree dirPath - action - - copyIfNotExists dest srcTemplate = do - testfile dest >>= \case - True -> logInfo $ display $ Messages.foundExistingFile dest - False -> writeTextFile dest srcTemplate data Globs = Globs diff --git a/src/Spago/Purs.hs b/src/Spago/Purs.hs index fe273cc3e..553f5268c 100644 --- a/src/Spago/Purs.hs +++ b/src/Spago/Purs.hs @@ -41,18 +41,6 @@ compile sourcePaths extraArgs = do "Build succeeded." "Failed to build." - -newtype ModuleGraph = ModuleGraph { unModuleGraph :: Map ModuleName ModuleGraphNode } - deriving newtype (FromJSON) - -data ModuleGraphNode = ModuleGraphNode - { path :: Text - , depends :: [ModuleName] - } deriving (Generic) - -instance FromJSON ModuleGraphNode - - graph :: (HasPurs env, HasLogFunc env) => [SourcePath] @@ -72,8 +60,8 @@ graph sourcePaths = do Nothing -> Left $ Messages.failedToParseCommandOutput cmd graphText Just p -> Right p - (_, _out, _err) -> - pure $ Left $ "Failed to run '" <> cmd <> "''" + (_, _out, err) -> + pure $ Left $ "Failed to run `" <> cmd <> "`. Error was:\n" <> tshow err repl :: HasPurs env => [SourcePath] -> [PursArg] -> RIO env () diff --git a/src/Spago/RunEnv.hs b/src/Spago/RunEnv.hs index dfb88cdbe..c46ee1ed9 100644 --- a/src/Spago/RunEnv.hs +++ b/src/Spago/RunEnv.hs @@ -6,6 +6,7 @@ import Spago.Env import System.Console.ANSI (hSupportsANSIWithoutEmulation) import qualified System.Environment as Env import qualified Distribution.System as OS +import qualified Data.Versions as Version import qualified RIO import qualified Turtle @@ -15,6 +16,7 @@ import qualified Spago.FetchPackage as FetchPackage import qualified Spago.Dhall as Dhall import qualified Spago.Messages as Messages import qualified Spago.PackageSet as PackageSet +import qualified Spago.Packages as Packages import qualified Spago.Purs as Purs -- | Given the global CLI options, it creates the Env for the Spago context @@ -128,21 +130,28 @@ withBuildEnv' :: HasEnv env => Maybe Config -> UsePsa + -> BuildOptions -> RIO BuildEnv a -> RIO env a -withBuildEnv' maybeConfig usePsa app = do +withBuildEnv' maybeConfig usePsa envBuildOptions@BuildOptions{ noInstall } app = do Env{..} <- getEnv envPursCmd <- getPurs usePsa envConfig@Config{..} <- case maybeConfig of Nothing -> getConfig Just c -> pure c let envPackageSet = packageSet + deps <- runRIO InstallEnv{..} $ do + deps <- Packages.getProjectDeps + when (noInstall == DoInstall) $ FetchPackage.fetchPackages deps + pure deps + envGraph <- runRIO PursEnv{..} (getMaybeGraph envBuildOptions envConfig deps) envGitCmd <- getGit runRIO BuildEnv{..} app withBuildEnv :: HasEnv env => UsePsa + -> BuildOptions -> RIO BuildEnv a -> RIO env a withBuildEnv = withBuildEnv' Nothing @@ -194,3 +203,21 @@ getPackageSet = do Config.ensureConfig >>= \case Right Config{ packageSet } -> pure packageSet Left err -> die [ display Messages.couldNotVerifySet, "Error was:", display err ] + +getMaybeGraph :: HasPursEnv env => BuildOptions -> Config -> [(PackageName, Package)] -> RIO env Graph +getMaybeGraph BuildOptions{ depsOnly, sourcePaths } Config{ configSourcePaths } deps = do + let partitionedGlobs@(Packages.Globs{..}) = Packages.getGlobs deps depsOnly configSourcePaths + globs = Packages.getGlobsSourcePaths partitionedGlobs <> sourcePaths + PursCmd { compilerVersion } <- view (the @PursCmd) + minVersion <- case Version.semver "0.14.0" of + Left _ -> die [ "Unable to parse min version for imports check" ] + Right minVersion -> pure minVersion + if compilerVersion < minVersion + then pure Nothing + else do + maybeGraph <- Purs.graph globs + case maybeGraph of + Right graph -> pure $ Just graph + Left err -> do + logWarn $ displayShow err + pure Nothing diff --git a/src/Spago/Types.hs b/src/Spago/Types.hs index ce2be2ee4..b82eacb6a 100644 --- a/src/Spago/Types.hs +++ b/src/Spago/Types.hs @@ -123,6 +123,7 @@ data NoBuild = NoBuild | DoBuild -- | Flag to skip the automatic installation of libraries on build data NoInstall = NoInstall | DoInstall + deriving Eq -- Should we clear the screen on rebuild? data ClearScreen = DoClear | NoClear @@ -148,6 +149,29 @@ data BuildOptions = BuildOptions , elseCommands :: [Text] } +defaultBuildOptions :: BuildOptions +defaultBuildOptions = BuildOptions + { shouldClear = NoClear + , shouldWatch = BuildOnce + , allowIgnored = DoAllowIgnored + , sourcePaths = [] + , withSourceMap = WithoutSrcMap + , noInstall = DoInstall + , depsOnly = AllSources + , pursArgs = [] + , beforeCommands = [] + , thenCommands = [] + , elseCommands = [] + } + +fromScriptOptions :: BuildOptions -> ScriptBuildOptions -> BuildOptions +fromScriptOptions opts ScriptBuildOptions{..} = opts + { pursArgs = pursArgs + , beforeCommands = beforeCommands + , thenCommands = thenCommands + , elseCommands = elseCommands + } + -- TODO: Figure out how `Watch` would work for `spago script` and include it data ScriptBuildOptions = ScriptBuildOptions { pursArgs :: [PursArg] @@ -186,3 +210,19 @@ newtype GitCmd = GitCmd Text newtype BowerCmd = BowerCmd Text data GlobalCache = GlobalCache !GHC.IO.FilePath !(Maybe CacheFlag) + +newtype ModuleGraph = ModuleGraph { unModuleGraph :: Map ModuleName ModuleGraphNode } + deriving newtype (FromJSON) + +data ModuleGraphNode = ModuleGraphNode + { graphNodePath :: Text + , graphNodeDepends :: [ModuleName] + } deriving (Generic) + +instance FromJSON ModuleGraphNode where + parseJSON = withObject "ModuleGraphNode" $ \o -> + ModuleGraphNode + <$> o .: "path" + <*> o .: "depends" + +type Graph = Maybe ModuleGraph diff --git a/test/Spago/Build/ParserSpec.hs b/test/Spago/Build/ParserSpec.hs deleted file mode 100644 index b6d482857..000000000 --- a/test/Spago/Build/ParserSpec.hs +++ /dev/null @@ -1,129 +0,0 @@ -module Spago.Build.ParserSpec (spec) where - -import Data.List.NonEmpty -import Prelude -import Test.Hspec -import Test.Hspec.Megaparsec -import qualified Text.Megaparsec as Parser - -import Spago.Build.Parser (PsModule (..), ModuleExportType(..), DataMembers(..)) -import qualified Spago.Build.Parser as Parser - -spec :: Spec -spec = do - describe "Parser for module declarations" $ do - it "should fail on bad inputs" $ do - let p = Parser.parse Parser.moduleDecl "" - - p `shouldFailOn` "module Test.Main () where" - p `shouldFailOn` "module Test.Main (,) where" - p `shouldFailOn` "module 1 where" - p `shouldFailOn` "module test where" - p `shouldFailOn` "module _ where" - p `shouldFailOn` "module Test_Main where" - p `shouldFailOn` "module Test' where" - p `shouldFailOn` "module Test.Main (..) where" - p `shouldFailOn` "module A..B where" - p `shouldFailOn` "module A (T( .. )) where" - p `shouldFailOn` "module A (M.T) where" - p `shouldFailOn` "module A (T (f)) where" - - it "should succeed" $ do - let p = Parser.parse Parser.moduleDecl "" - - p "module Test where" - `shouldParse` - PsModule "Test" Nothing - - p "module Test123 where" - `shouldParse` - PsModule "Test123" Nothing - - p "module Test.Main where" - `shouldParse` - PsModule "Test.Main" Nothing - - p "module Test.Main (main) where" - `shouldParse` - PsModule "Test.Main" (Just $ fromList [ExportValue "main"]) - - p "module Test.Main (m_a_i_n) where" - `shouldParse` - PsModule "Test.Main" (Just $ fromList [ExportValue "m_a_i_n"]) - - p "module Test.Main (main, test) where" - `shouldParse` - PsModule "Test.Main" (Just $ fromList [ExportValue "main", ExportValue "test"]) - - p "module A (module B) where" - `shouldParse` - PsModule "A" (Just $ fromList [ExportModule "B"]) - - p "module A (module B.C) where" - `shouldParse` - PsModule "A" (Just $ fromList [ExportModule "B.C"]) - - p "module A (module A, module B) where" - `shouldParse` - PsModule "A" (Just $ fromList [ExportModule "A", ExportModule "B"]) - - p "module Test (class Foldable, foldr, foldl, foldMap) where" - `shouldParse` - PsModule "Test" (Just $ fromList $ [ ExportClass "Foldable" - , ExportValue "foldr" - , ExportValue "foldl" - , ExportValue "foldMap" - ] - ) - - p "module Test (($)) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportOp "$"]) - - p "module Test (T) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportType "T" Nothing]) - - p "module Test (T (..)) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportType "T" (Just DataAll)]) - - p "module Test (T ()) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportType "T" (Just $ DataEnumerated [])]) - - p "module Test (T (S, U)) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportType "T" (Just $ DataEnumerated ["S", "U"])]) - - p "module Test (T (S, U)) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportType "T" (Just $ DataEnumerated ["S", "U"])]) - - p "module Test (type (++)) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportTypeOp "++"]) - - p "module Test (kind T) where" - `shouldParse` - PsModule "Test" (Just $ fromList [ExportKind "T"]) - - p "-- | comment \nmodule T where" - `shouldParse` - PsModule "T" Nothing - - p "module T (HalogenIO\n, module Data.Lazy,module B) where" - `shouldParse` - PsModule "T" (Just $ fromList [ ExportType "HalogenIO" Nothing - , ExportModule "Data.Lazy" - , ExportModule "B" - ] - ) - - p "module A (class_) where" - `shouldParse` - PsModule "A" (Just $ fromList [ExportValue "class_"]) - - p "module A (module_) where" - `shouldParse` - PsModule "A" (Just $ fromList [ExportValue "module_"]) \ No newline at end of file diff --git a/test/SpagoSpec.hs b/test/SpagoSpec.hs index ac4576306..d6030eeea 100644 --- a/test/SpagoSpec.hs +++ b/test/SpagoSpec.hs @@ -10,7 +10,7 @@ import Turtle (ExitCode (..), cd, cp, decodeString, empty, mkdir, mktree, mv, pwd, readTextFile, rm, shell, shellStrictWithErr, testdir, writeTextFile, ()) import Utils (checkFileHasInfix, checkFixture, checkFileExist, outputShouldEqual, - readFixture, runFor, shouldBeFailure, + readFixture, runFor, shouldBeFailure, shouldBeFailureInfix, shouldBeFailureStderr, shouldBeSuccess, shouldBeSuccessOutput, shouldBeSuccessOutputWithErr, shouldBeSuccessStderr, spago, withCwd, withEnvVar) @@ -582,7 +582,7 @@ spec = around_ setup $ do spago ["init"] >>= shouldBeSuccess mv "test" "test2" - spago ["test"] >>= shouldBeFailureStderr "spago-test-not-found.txt" + spago ["test"] >>= shouldBeFailureInfix "Module 'Test.Main' not found! Are you including it in your build?" it "Spago should test in custom output folder" $ do diff --git a/test/fixtures/spago-test-not-found.txt b/test/fixtures/spago-test-not-found.txt deleted file mode 100644 index b1b29f972..000000000 --- a/test/fixtures/spago-test-not-found.txt +++ /dev/null @@ -1 +0,0 @@ -[error] Module 'Test.Main' not found! Are you including it in your build?