diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6bb0e2c39..f4f545a46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,10 @@ jobs: env: cache-name: cache-node-modules with: - path: ~/.npm + path: | + ~/.npm + $APPDATA/npm + node_modules key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d8dbb3d..82e513f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Other improvements: help catch typos in field names. - When the `publish.location` field is missing, `spago publish` will attempt to figure out the location from Git remotes and write it back to `spago.yaml`. +- Internally Spago uses stricter-typed file paths. ## [0.21.0] - 2023-05-04 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 46151bfcb..a926abb5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -111,3 +111,50 @@ Learn by doing and get your hands dirty! [f-f]: https://github.com/f-f [discord]: https://purescript.org/chat [spago-issues]: https://github.com/purescript/spago/issues + +## Working with file paths + +File paths are very important in Spago. A very big chunk of Spago does is +shuffling files around and manipulating their paths. Representing them as plain +strings is not enough. + +Spago has three different kinds of paths, represented as distinct types: + +- `RootPath` can generally be the root of anything, but in practice it usually + points to the root directory of the current workspace. It is constructed in + `Main.purs` close to the entry point and is available in all `ReaderT` + environments as `rootPath :: RootPath` +- `LocalPath` is path of a particular file or directory within the workspace. It + doesn't have to be literally _within_ the workspace directory - e.g. a custom + dependency that lives somewhere on the local file system, - but it's still + _relative_ to the workspace. A `LocalPath` is explicitly broken into two + parts: a `RootPath` and the "local" part relative to the root. This is useful + for printing out workspace-relative paths in user-facing output, while still + retaining the full path for actual file operations. A `LocalPath` can be + constructed by appending to a `RootPath`. Once so constructed, the `LocalPath` + always retains the same root, no matter what subsequent manipulations are done + to it. Therefore, if you have a `LocalPath` value, its root is probably + pointing to the current workspace directory. +- `GlobalPath` is for things that are not related to the current workspace. + Examples include paths to executables, such as `node` and `purs`, and global + directories, such as `registryPath` and `globalCachePath`. + +Paths can be appended by using the `` operator. It is overloaded for all +three path types and allows to append string segments to them. When appending to +a `RootPath`, the result comes out as `LocalPath`. You cannot produce a new +`RootPath` by appending. + +Most code that deals with the workspace operates in `LocalPath` values. Most +code that deals with external and global things operates in `GlobalPath` values. +Lower-level primitives, such as in the `Spago.FS` module, are polymorphic and +can take all three path types as parameters. + +For example: + +```haskell +rootPath <- Path.mkRootPath =<< Paths.cwd +config <- readConfig (rootPath "spago.yaml") +let srcDir = rootPath "src" +compileResult <- callCompiler [ srcDir "Main.purs", srcDir "Lib.purs" ] +FS.writeFile (rootPath "result.json") (serialize compileResult) +``` diff --git a/bin/src/Flags.purs b/bin/src/Flags.purs index 28e3d0e27..cae73a8c6 100644 --- a/bin/src/Flags.purs +++ b/bin/src/Flags.purs @@ -336,7 +336,7 @@ depsOnly = <> O.help "Build depedencies only" ) -publicKeyPath :: Parser FilePath +publicKeyPath :: Parser RawFilePath publicKeyPath = O.strOption ( O.short 'i' @@ -344,7 +344,7 @@ publicKeyPath = <> O.help "Select the path of the public key to use for authenticating operations of the package" ) -privateKeyPath :: Parser FilePath +privateKeyPath :: Parser RawFilePath privateKeyPath = O.strOption ( O.short 'i' diff --git a/bin/src/Main.purs b/bin/src/Main.purs index 2665fc7aa..4056387d3 100644 --- a/bin/src/Main.purs +++ b/bin/src/Main.purs @@ -15,7 +15,6 @@ import Data.Set as Set import Effect.Aff as Aff import Effect.Aff.AVar as AVar import Effect.Now as Now -import Node.Process as Process import Options.Applicative (CommandFields, Mod, Parser, ParserPrefs(..)) import Options.Applicative as O import Options.Applicative.Types (Backtracking(..)) @@ -52,6 +51,7 @@ import Spago.Generated.BuildInfo as BuildInfo import Spago.Git as Git import Spago.Json as Json import Spago.Log (LogVerbosity(..)) +import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs as Purs import Spago.Registry as Registry @@ -163,7 +163,7 @@ type BundleArgs = { minify :: Boolean , sourceMaps :: Boolean , module :: Maybe String - , outfile :: Maybe FilePath + , outfile :: Maybe String , platform :: Maybe String , selectedPackage :: Maybe String , pursArgs :: List String @@ -536,7 +536,8 @@ main = do \c -> Aff.launchAff_ case c of Cmd'SpagoCmd (SpagoCmd globalArgs@{ offline, migrateConfig } command) -> do logOptions <- mkLogOptions startingTime globalArgs - runSpago { logOptions } case command of + rootPath <- Path.mkRoot =<< Paths.cwd + runSpago { logOptions, rootPath } case command of Sources args -> do { env } <- mkFetchEnv { packages: mempty @@ -551,11 +552,9 @@ main = do void $ runSpago env (Sources.run { json: args.json }) Init args@{ useSolver } -> do -- Fetch the registry here so we can select the right package set later - env <- mkRegistryEnv offline - + env <- mkRegistryEnv offline <#> Record.union { rootPath } setVersion <- parseSetVersion args.setVersion void $ runSpago env $ Init.run { mode: args.mode, setVersion, useSolver } - Fetch args -> do { env, fetchOpts } <- mkFetchEnv (Record.merge { isRepl: false, migrateConfig, offline } args) void $ runSpago env (Fetch.run fetchOpts) @@ -600,7 +599,7 @@ main = do void $ runSpago publishEnv (Publish.publish {}) Repl args@{ selectedPackage } -> do - packages <- FS.exists "spago.yaml" >>= case _ of + packages <- FS.exists (rootPath "spago.yaml") >>= case _ of true -> do -- if we have a config then we assume it's a workspace, and we can run a repl in the project pure mempty -- TODO newPackages @@ -609,9 +608,10 @@ main = do logWarn "No configuration found, creating a temporary project to run a repl in..." tmpDir <- mkTemp FS.mkdirp tmpDir - logDebug $ "Creating repl project in temp dir: " <> tmpDir - liftEffect $ Process.chdir tmpDir - env <- mkRegistryEnv offline + logDebug $ "Creating repl project in temp dir: " <> Path.quote tmpDir + Paths.chdir tmpDir + tmpRootPath <- Path.mkRoot tmpDir + env <- mkRegistryEnv offline <#> Record.union { rootPath: tmpRootPath } void $ runSpago env $ Init.run { setVersion: Nothing , mode: Init.InitWorkspace { packageName: Just "repl" } @@ -661,12 +661,12 @@ main = do testEnv <- runSpago env (mkTestEnv args buildEnv) runSpago testEnv Test.run LsPaths args -> do - runSpago { logOptions } $ Ls.listPaths args + runSpago { logOptions, rootPath } $ Ls.listPaths args LsPackages args@{ pure } -> do let fetchArgs = { packages: mempty, selectedPackage: Nothing, pure, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } { env: env@{ workspace }, fetchOpts } <- mkFetchEnv fetchArgs dependencies <- runSpago env (Fetch.run fetchOpts) - let lsEnv = { workspace, dependencies, logOptions } + let lsEnv = { workspace, dependencies, logOptions, rootPath } runSpago lsEnv (Ls.listPackageSet args) LsDeps { selectedPackage, json, transitive, pure } -> do let fetchArgs = { packages: mempty, selectedPackage, pure, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } @@ -691,12 +691,12 @@ main = do { env, fetchOpts } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } dependencies <- runSpago env (Fetch.run fetchOpts) purs <- Purs.getPurs - runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphModules args) + runSpago { dependencies, logOptions, rootPath, purs, workspace: env.workspace } (Graph.graphModules args) GraphPackages args -> do { env, fetchOpts } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline } dependencies <- runSpago env (Fetch.run fetchOpts) purs <- Purs.getPurs - runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphPackages args) + runSpago { dependencies, logOptions, rootPath, purs, workspace: env.workspace } (Graph.graphPackages args) Cmd'VersionCmd v -> when v do output (OutputLines [ BuildInfo.packages."spago-bin" ]) @@ -721,7 +721,7 @@ main = do mkBundleEnv :: forall a. BundleArgs -> Spago (Fetch.FetchEnv a) (Bundle.BundleEnv ()) mkBundleEnv bundleArgs = do - { workspace, logOptions } <- ask + { workspace, logOptions, rootPath } <- ask logDebug $ "Bundle args: " <> show bundleArgs selected <- case workspace.selected of @@ -770,18 +770,19 @@ mkBundleEnv bundleArgs = do , sourceMaps: bundleArgs.sourceMaps , extraArgs } + argsOutput = bundleArgs.output <#> (rootPath _) newWorkspace = workspace { buildOptions - { output = bundleArgs.output <|> workspace.buildOptions.output + { output = argsOutput <|> workspace.buildOptions.output } } esbuild <- Esbuild.getEsbuild - let bundleEnv = { esbuild, logOptions, workspace: newWorkspace, selected, bundleOptions } + let bundleEnv = { esbuild, logOptions, rootPath, workspace: newWorkspace, selected, bundleOptions } pure bundleEnv mkRunEnv :: forall a b. RunArgs -> Build.BuildEnv b -> Spago (Fetch.FetchEnv a) (Run.RunEnv ()) mkRunEnv runArgs { dependencies, purs } = do - { workspace, logOptions } <- ask + { workspace, logOptions, rootPath } <- ask logDebug $ "Run args: " <> show runArgs node <- Run.getNode @@ -816,17 +817,18 @@ mkRunEnv runArgs { dependencies, purs } = do runOptions = { moduleName , execArgs - , executeDir: Paths.cwd + , executeDir: Path.toGlobal rootPath , successMessage: Nothing , failureMessage: "Running failed." } - let newWorkspace = workspace { buildOptions { output = runArgs.output <|> workspace.buildOptions.output } } - let runEnv = { logOptions, workspace: newWorkspace, selected, node, runOptions, dependencies, purs } + let argsOutput = runArgs.output <#> (rootPath _) + let newWorkspace = workspace { buildOptions { output = argsOutput <|> workspace.buildOptions.output } } + let runEnv = { logOptions, rootPath, workspace: newWorkspace, selected, node, runOptions, dependencies, purs } pure runEnv mkTestEnv :: forall a b. TestArgs -> Build.BuildEnv b -> Spago (Fetch.FetchEnv a) (Test.TestEnv ()) mkTestEnv testArgs { dependencies, purs } = do - { workspace, logOptions } <- ask + { workspace, logOptions, rootPath } <- ask logDebug $ "Test args: " <> show testArgs node <- Run.getNode @@ -860,8 +862,9 @@ mkTestEnv testArgs { dependencies, purs } = do logDebug $ "Selected packages to test: " <> Json.stringifyJson (CJ.Common.nonEmptyArray PackageName.codec) (map _.selected.package.name selectedPackages) - let newWorkspace = workspace { buildOptions { output = testArgs.output <|> workspace.buildOptions.output } } - let testEnv = { logOptions, workspace: newWorkspace, selectedPackages, node, dependencies, purs } + let argsOutput = testArgs.output <#> (rootPath _) + let newWorkspace = workspace { buildOptions { output = argsOutput <|> workspace.buildOptions.output } } + let testEnv = { logOptions, rootPath, workspace: newWorkspace, selectedPackages, node, dependencies, purs } pure testEnv mkBuildEnv @@ -876,12 +879,13 @@ mkBuildEnv -> Fetch.PackageTransitiveDeps -> Spago (Fetch.FetchEnv ()) (Build.BuildEnv ()) mkBuildEnv buildArgs dependencies = do - { logOptions, workspace, git } <- ask + { logOptions, rootPath, workspace, git } <- ask purs <- Purs.getPurs let + argsOutput = buildArgs.output <#> (rootPath _) newWorkspace = workspace { buildOptions - { output = buildArgs.output <|> workspace.buildOptions.output + { output = argsOutput <|> workspace.buildOptions.output , statVerbosity = buildArgs.statVerbosity <|> workspace.buildOptions.statVerbosity } -- Override the backend args from the config if they are passed in through a flag @@ -895,6 +899,7 @@ mkBuildEnv buildArgs dependencies = do pure { logOptions + , rootPath , purs , git , dependencies @@ -925,7 +930,7 @@ mkPublishEnv dependencies = do mkReplEnv :: forall a. ReplArgs -> Fetch.PackageTransitiveDeps -> PackageMap -> Spago (Fetch.FetchEnv a) (Repl.ReplEnv ()) mkReplEnv replArgs dependencies supportPackage = do - { workspace, logOptions } <- ask + { workspace, logOptions, rootPath } <- ask logDebug $ "Repl args: " <> show replArgs purs <- Purs.getPurs @@ -941,16 +946,17 @@ mkReplEnv replArgs dependencies supportPackage = do , supportPackage , depsOnly: false , logOptions + , rootPath , pursArgs: Array.fromFoldable replArgs.pursArgs , selected } -mkFetchEnv :: forall a b. { offline :: OnlineStatus, migrateConfig :: Boolean, isRepl :: Boolean | FetchArgsRow b } -> Spago (LogEnv a) { env :: Fetch.FetchEnv (), fetchOpts :: Fetch.FetchOpts } +mkFetchEnv :: forall a b. { offline :: OnlineStatus, migrateConfig :: Boolean, isRepl :: Boolean | FetchArgsRow b } -> Spago (SpagoBaseEnv a) { env :: Fetch.FetchEnv (), fetchOpts :: Fetch.FetchOpts } mkFetchEnv args@{ migrateConfig, offline } = do let - parsePackageName p = case PackageName.parse p of - Right pkg -> Right pkg - Left err -> Left ("- Could not parse package " <> show p <> ": " <> err) + parsePackageName p = + PackageName.parse p + # lmap \err -> "- Could not parse package " <> show p <> ": " <> err let { right: packageNames, left: failedPackageNames } = partitionMap parsePackageName (Array.fromFoldable args.packages) unless (Array.null failedPackageNames) do die $ [ toDoc "Failed to parse some package name: " ] <> map (indent <<< toDoc) failedPackageNames @@ -960,25 +966,28 @@ mkFetchEnv args@{ migrateConfig, offline } = do Left _err -> die $ "Failed to parse selected package name, was: " <> show args.selectedPackage env <- mkRegistryEnv offline - workspace <- runSpago env (Config.readWorkspace { maybeSelectedPackage, pureBuild: args.pure, migrateConfig }) + { rootPath } <- ask + workspace <- + runSpago (Record.union env { rootPath }) + (Config.readWorkspace { maybeSelectedPackage, pureBuild: args.pure, migrateConfig }) let fetchOpts = { packages: packageNames, ensureRanges: args.ensureRanges, isTest: args.testDeps, isRepl: args.isRepl } - pure { fetchOpts, env: Record.union { workspace } env } + pure { fetchOpts, env: Record.union { workspace, rootPath } env } -mkRegistryEnv :: forall a. OnlineStatus -> Spago (LogEnv a) (Registry.RegistryEnv ()) +mkRegistryEnv :: forall a. OnlineStatus -> Spago (SpagoBaseEnv a) (Registry.RegistryEnv ()) mkRegistryEnv offline = do - logDebug $ "CWD: " <> Paths.cwd + { logOptions, rootPath } <- ask -- Take care of the caches FS.mkdirp Paths.globalCachePath - FS.mkdirp Paths.localCachePath - FS.mkdirp Paths.localCachePackagesPath - logDebug $ "Global cache: " <> show Paths.globalCachePath - logDebug $ "Local cache: " <> show Paths.localCachePath + FS.mkdirp $ rootPath Paths.localCachePath + FS.mkdirp $ rootPath Paths.localCachePackagesPath + logDebug $ "Workspace root path: " <> Path.quote rootPath + logDebug $ "Global cache: " <> Path.quote Paths.globalCachePath + logDebug $ "Local cache: " <> Paths.localCachePath -- Make sure we have git and purs git <- Git.getGit purs <- Purs.getPurs - { logOptions } <- ask db <- liftEffect $ Db.connect { database: Paths.databasePath , logger: \str -> Reader.runReaderT (logDebug $ "DB: " <> str) { logOptions } @@ -997,7 +1006,7 @@ mkRegistryEnv offline = do mkLsEnv :: forall a. Fetch.PackageTransitiveDeps -> Spago (Fetch.FetchEnv a) Ls.LsEnv mkLsEnv dependencies = do - { logOptions, workspace } <- ask + { logOptions, workspace, rootPath } <- ask selected <- case workspace.selected of Just s -> pure s Nothing -> @@ -1013,15 +1022,16 @@ mkLsEnv dependencies = do [ toDoc "No package was selected. Please select (with -p) one of the following packages:" , indent (toDoc $ map _.package.name workspacePackages) ] - pure { logOptions, workspace, dependencies, selected } + pure { logOptions, workspace, dependencies, selected, rootPath } mkDocsEnv :: ∀ a. DocsArgs -> Fetch.PackageTransitiveDeps -> Spago (Fetch.FetchEnv a) (Docs.DocsEnv ()) mkDocsEnv args dependencies = do - { logOptions, workspace } <- ask + { logOptions, rootPath, workspace } <- ask purs <- Purs.getPurs pure { purs , logOptions + , rootPath , workspace , dependencies , depsOnly: args.depsOnly diff --git a/core/src/Config.purs b/core/src/Config.purs index 45eb9a4ea..5e9339243 100644 --- a/core/src/Config.purs +++ b/core/src/Config.purs @@ -100,8 +100,8 @@ type PublishConfig = { version :: Version , license :: License , location :: Maybe Location - , include :: Maybe (Array FilePath) - , exclude :: Maybe (Array FilePath) + , include :: Maybe (Array RawFilePath) + , exclude :: Maybe (Array RawFilePath) , owners :: Maybe (Array Owner) } @@ -210,7 +210,7 @@ packageBuildOptionsCodec = CJ.named "PackageBuildOptionsInput" $ CJS.objectStric type BundleConfig = { minify :: Maybe Boolean , module :: Maybe String - , outfile :: Maybe FilePath + , outfile :: Maybe RawFilePath , platform :: Maybe BundlePlatform , type :: Maybe BundleType , extraArgs :: Maybe (Array String) @@ -347,7 +347,7 @@ workspaceConfigCodec = CJ.named "WorkspaceConfig" $ CJS.objectStrict $ CJS.record type WorkspaceBuildOptionsInput = - { output :: Maybe FilePath + { output :: Maybe RawFilePath , censorLibraryWarnings :: Maybe CensorBuildWarnings , statVerbosity :: Maybe StatVerbosity } @@ -440,7 +440,7 @@ statVerbosityCodec = CJ.Sum.enumSum print parse data SetAddress = SetFromRegistry { registry :: Version } | SetFromUrl { url :: String, hash :: Maybe Sha256 } - | SetFromPath { path :: FilePath } + | SetFromPath { path :: RawFilePath } derive instance Eq SetAddress @@ -474,7 +474,7 @@ extraPackageCodec = Codec.codec' decode encode decode json = map ExtraLocalPackage (Codec.decode localPackageCodec json) <|> map ExtraRemotePackage (Codec.decode remotePackageCodec json) -type LocalPackage = { path :: FilePath } +type LocalPackage = { path :: RawFilePath } localPackageCodec :: CJ.Codec LocalPackage localPackageCodec = CJ.named "LocalPackage" $ CJ.Record.objectStrict { path: CJ.string } @@ -500,7 +500,7 @@ remotePackageCodec = Codec.codec' decode encode type GitPackage = { git :: String , ref :: String - , subdir :: Maybe FilePath + , subdir :: Maybe RawFilePath , dependencies :: Maybe Dependencies } diff --git a/core/src/FS.purs b/core/src/FS.purs index 304f9f2bf..57c434088 100644 --- a/core/src/FS.purs +++ b/core/src/FS.purs @@ -12,6 +12,7 @@ module Spago.FS , moveSync , readJsonFile , readTextFile + , readTextFileSync , readYamlDocFile , readYamlFile , writeFile @@ -33,21 +34,24 @@ import Node.FS.Perms (Perms) import Node.FS.Perms as Perms import Node.FS.Sync as FS.Sync import Spago.Json as Json +import Spago.Path (toRaw) +import Spago.Path as Path import Spago.Yaml as Yaml -mkdirp :: forall m. MonadAff m => FilePath -> m Unit -mkdirp = liftAff <<< flip FS.Aff.mkdir' { recursive: true, mode: Perms.mkPerms Perms.all Perms.all Perms.all } +mkdirp :: forall m path. Path.IsPath path => MonadAff m => path -> m Unit +mkdirp path = liftAff $ + FS.Aff.mkdir' (toRaw path) { recursive: true, mode: Perms.mkPerms Perms.all Perms.all Perms.all } foreign import moveSyncImpl :: String -> String -> Effect Unit -moveSync :: forall m. MonadEffect m => { src :: FilePath, dst :: FilePath } -> m Unit -moveSync { src, dst } = liftEffect $ moveSyncImpl src dst +moveSync :: ∀ m src dst. Path.IsPath src => Path.IsPath dst => MonadEffect m => { src :: src, dst :: dst } -> m Unit +moveSync { src, dst } = liftEffect $ moveSyncImpl (toRaw src) (toRaw dst) -copyFileSync :: forall m. MonadEffect m => { src :: FilePath, dst :: FilePath } -> m Unit -copyFileSync { src, dst } = liftEffect $ FS.Sync.copyFile src dst +copyFileSync :: ∀ m src dst. Path.IsPath src => Path.IsPath dst => MonadEffect m => { src :: src, dst :: dst } -> m Unit +copyFileSync { src, dst } = liftEffect $ FS.Sync.copyFile (toRaw src) (toRaw dst) -copyFile :: forall m. MonadAff m => { src :: FilePath, dst :: FilePath } -> m Unit -copyFile { src, dst } = liftAff $ FS.Aff.copyFile src dst +copyFile :: ∀ m src dst. Path.IsPath src => Path.IsPath dst => MonadAff m => { src :: src, dst :: dst } -> m Unit +copyFile { src, dst } = liftAff $ FS.Aff.copyFile (toRaw src) (toRaw dst) foreign import cpImpl :: String -> String -> Effect Unit @@ -55,64 +59,67 @@ foreign import cpImpl :: String -> String -> Effect Unit -- | Note: if `src` is a directory it will copy everything inside of this directory, -- | not the entire directory itself. -- | Note: if `src` is a file, `dst` cannot be a directory -copyTree :: forall m. MonadEffect m => { src :: FilePath, dst :: FilePath } -> m Unit -copyTree { src, dst } = liftEffect $ cpImpl src dst +copyTree :: ∀ m src dst. Path.IsPath src => Path.IsPath dst => MonadEffect m => { src :: src, dst :: dst } -> m Unit +copyTree { src, dst } = liftEffect $ cpImpl (toRaw src) (toRaw dst) foreign import ensureFileSyncImpl :: String -> Effect Unit -ensureFileSync :: forall m. MonadEffect m => FilePath -> m Unit -ensureFileSync file = liftEffect $ ensureFileSyncImpl file +ensureFileSync :: forall m path. Path.IsPath path => MonadEffect m => path -> m Unit +ensureFileSync file = liftEffect $ ensureFileSyncImpl $ toRaw file -exists :: forall m. MonadEffect m => String -> m Boolean -exists = liftEffect <<< FS.Sync.exists +exists :: forall m path. Path.IsPath path => MonadEffect m => path -> m Boolean +exists = liftEffect <<< FS.Sync.exists <<< toRaw -unlink :: ∀ m. MonadAff m => String -> m Unit -unlink = liftAff <<< FS.Aff.unlink +unlink :: ∀ m path. Path.IsPath path => MonadAff m => path -> m Unit +unlink = liftAff <<< FS.Aff.unlink <<< toRaw -writeTextFile :: forall m. MonadAff m => FilePath -> String -> m Unit -writeTextFile path text = liftAff $ FS.Aff.writeTextFile UTF8 path text +writeTextFile :: forall m path. Path.IsPath path => MonadAff m => path -> String -> m Unit +writeTextFile path text = liftAff $ FS.Aff.writeTextFile UTF8 (toRaw path) text -readTextFile :: forall m. MonadAff m => FilePath -> m String -readTextFile path = liftAff $ FS.Aff.readTextFile UTF8 path +readTextFile :: forall m path. Path.IsPath path => MonadAff m => path -> m String +readTextFile path = liftAff $ FS.Aff.readTextFile UTF8 (toRaw path) -writeFile :: forall m. MonadAff m => FilePath -> Buffer -> m Unit -writeFile path buf = liftAff $ FS.Aff.writeFile path buf +readTextFileSync :: ∀ m path. Path.IsPath path => MonadEffect m => path -> m String +readTextFileSync path = liftEffect $ FS.Sync.readTextFile UTF8 (toRaw path) -ls :: forall m. MonadAff m => FilePath -> m (Array FilePath) -ls = liftAff <<< FS.Aff.readdir +writeFile :: forall m path. Path.IsPath path => MonadAff m => path -> Buffer -> m Unit +writeFile path buf = liftAff $ FS.Aff.writeFile (toRaw path) buf -chmod :: forall m. MonadAff m => FilePath -> Perms -> m Unit -chmod path perms = liftAff $ FS.Aff.chmod path perms +ls :: forall m path. Path.IsPath path => MonadAff m => path -> m (Array RawFilePath) +ls = liftAff <<< FS.Aff.readdir <<< toRaw + +chmod :: forall m path. Path.IsPath path => MonadAff m => path -> Perms -> m Unit +chmod path perms = liftAff $ FS.Aff.chmod (toRaw path) perms -- | Encode data as formatted JSON and write it to the provided filepath -writeJsonFile :: forall a. CJ.Codec a -> FilePath -> a -> Aff Unit -writeJsonFile codec path = FS.Aff.writeTextFile UTF8 path <<< (_ <> "\n") <<< Json.printJson codec +writeJsonFile :: forall a path. Path.IsPath path => CJ.Codec a -> path -> a -> Aff Unit +writeJsonFile codec path = FS.Aff.writeTextFile UTF8 (toRaw path) <<< (_ <> "\n") <<< Json.printJson codec -- | Decode data from a JSON file at the provided filepath -readJsonFile :: forall a. CJ.Codec a -> FilePath -> Aff (Either String a) +readJsonFile :: forall a path. Path.IsPath path => CJ.Codec a -> path -> Aff (Either String a) readJsonFile codec path = do - result <- Aff.attempt $ FS.Aff.readTextFile UTF8 path + result <- Aff.attempt $ FS.Aff.readTextFile UTF8 (toRaw path) pure (lmap Aff.message result >>= Json.parseJson codec >>> lmap CJ.DecodeError.print) -- | Encode data as formatted YAML and write it to the provided filepath -writeYamlFile :: forall a. CJ.Codec a -> FilePath -> a -> Aff Unit -writeYamlFile codec path = FS.Aff.writeTextFile UTF8 path <<< (_ <> "\n") <<< String.trim <<< Yaml.printYaml codec +writeYamlFile :: forall a path. Path.IsPath path => CJ.Codec a -> path -> a -> Aff Unit +writeYamlFile codec path = FS.Aff.writeTextFile UTF8 (toRaw path) <<< (_ <> "\n") <<< String.trim <<< Yaml.printYaml codec -- | Decode data from a YAML file at the provided filepath -readYamlFile :: forall a. CJ.Codec a -> FilePath -> Aff (Either String a) +readYamlFile :: forall a path. Path.IsPath path => CJ.Codec a -> path -> Aff (Either String a) readYamlFile codec path = do - result <- Aff.attempt $ FS.Aff.readTextFile UTF8 path + result <- Aff.attempt $ FS.Aff.readTextFile UTF8 (toRaw path) pure (lmap Aff.message result >>= Yaml.parseYaml codec >>> lmap CJ.DecodeError.print) -writeYamlDocFile :: forall a. FilePath -> Yaml.YamlDoc a -> Aff Unit -writeYamlDocFile path = FS.Aff.writeTextFile UTF8 path <<< (_ <> "\n") <<< String.trim <<< Yaml.toString +writeYamlDocFile :: forall a path. Path.IsPath path => path -> Yaml.YamlDoc a -> Aff Unit +writeYamlDocFile path = FS.Aff.writeTextFile UTF8 (toRaw path) <<< (_ <> "\n") <<< String.trim <<< Yaml.toString -readYamlDocFile :: forall a. CJ.Codec a -> FilePath -> Aff (Either String { doc :: Yaml.YamlDoc a, yaml :: a }) +readYamlDocFile :: forall a path. Path.IsPath path => CJ.Codec a -> path -> Aff (Either String { doc :: Yaml.YamlDoc a, yaml :: a }) readYamlDocFile codec path = do - result <- Aff.attempt $ FS.Aff.readTextFile UTF8 path + result <- Aff.attempt $ FS.Aff.readTextFile UTF8 (toRaw path) pure (lmap Aff.message result >>= Yaml.parseYamlDoc codec >>> lmap CJ.DecodeError.print) -foreign import getInBetweenPathsImpl :: EffectFn2 FilePath FilePath (Array FilePath) +foreign import getInBetweenPathsImpl :: EffectFn2 GlobalPath GlobalPath (Array GlobalPath) -getInBetweenPaths :: forall m. MonadEffect m => FilePath -> FilePath -> m (Array FilePath) +getInBetweenPaths :: forall m. MonadEffect m => GlobalPath -> GlobalPath -> m (Array GlobalPath) getInBetweenPaths a b = liftEffect $ runEffectFn2 getInBetweenPathsImpl a b diff --git a/core/src/Path.purs b/core/src/Path.purs new file mode 100644 index 000000000..627e2a2f6 --- /dev/null +++ b/core/src/Path.purs @@ -0,0 +1,215 @@ +module Spago.Path + ( () + , RawFilePath + , GlobalPath + , LocalPath + , RootPath + , appendPath + , basename + , class AppendPath + , class IsPath + , dirname + , isPrefixOf + , localPart + , localPathCodec + , global + , mkRoot + , printLocalPath + , quote + , relativeTo + , replaceExtension + , rootPart + , toAbsolute + , toGlobal + , toRaw + , withForwardSlashes + , withForwardSlashes' + ) where + +import Prelude + +import Data.Codec.JSON as CJ +import Data.Function (on) +import Data.Maybe (Maybe(..), isJust) +import Data.Profunctor (dimap) +import Data.String as String +import Effect.Class (class MonadEffect, liftEffect) +import Node.Path as Node.Path +import Node.Path as Path +import Node.Platform (Platform(..)) as Node +import Node.Process (platform) as Node + +-- | Normally this represents the root directory of the workspace. All Spago +-- | Workspace-scoped paths are relative to a `RootPath`. +newtype RootPath = RootPath String + +derive newtype instance Show RootPath +derive newtype instance Eq RootPath +derive newtype instance Ord RootPath + +-- | A Spago Workspace-scoped path, consists of two parts: `RootPath` and local +-- | part, relative to the root. This lets us both have the full path for +-- | actually working with files and the local part for user-facing output. +newtype LocalPath = LocalPath { root :: RootPath, local :: RawFilePath } + +instance Show LocalPath where + show (LocalPath p) = p.local + +instance Eq LocalPath where + eq = eq `on` toGlobal + +instance Ord LocalPath where + compare = compare `on` toGlobal + +-- | A part that is logically not part of the Spago Workspace, but points to +-- | something "global", such as registry cache, temp directory, and so on. +newtype GlobalPath = GlobalPath String + +instance Show GlobalPath where + show (GlobalPath p) = p + +derive newtype instance Eq GlobalPath +derive newtype instance Ord GlobalPath + +type RawFilePath = String + +class (Show path, Eq path, Ord path) <= IsPath path where + toGlobal :: path -> GlobalPath + relativeTo :: path -> RootPath -> LocalPath + quote :: path -> String + replaceExtension :: String.Pattern -> String.Replacement -> path -> Maybe path + withForwardSlashes :: path -> path + +instance IsPath LocalPath where + toGlobal (LocalPath { root: RootPath root, local }) = + GlobalPath $ Path.concat [ root, local ] + relativeTo path root + | rootPart path == root = path + | otherwise = toGlobal path `relativeTo` root + quote (LocalPath path) + | path.local == "" = "\".\"" + | otherwise = "\"" <> path.local <> "\"" + replaceExtension p r (LocalPath path) = + LocalPath <<< path { local = _ } <$> replaceExtension_ p r path.local + withForwardSlashes (LocalPath path) = + LocalPath { root: withForwardSlashes path.root, local: withForwardSlashes' path.local } + +instance IsPath GlobalPath where + toGlobal = identity + relativeTo (GlobalPath path) (RootPath root) + | areBothAbsolutePathsOnDifferentDrives path root = + LocalPath { root: RootPath path, local: "" } -- see comments on `areBothAbsolutePathsOnDifferentDrives` + | otherwise = + LocalPath { root: RootPath root, local: Path.relative root path } + quote (GlobalPath path) = + "\"" <> path <> "\"" + replaceExtension p r (GlobalPath path) = + GlobalPath <$> replaceExtension_ p r path + withForwardSlashes (GlobalPath path) = + GlobalPath $ withForwardSlashes' path + +instance IsPath RootPath where + toGlobal (RootPath path) = GlobalPath path + relativeTo path root = toGlobal path `relativeTo` root + quote (RootPath path) = "\"" <> path <> "\"" + replaceExtension p r (RootPath path) = RootPath <$> replaceExtension_ p r path + withForwardSlashes (RootPath path) = RootPath $ withForwardSlashes' path + +class AppendPath base result | base -> result where + appendPath :: base -> RawFilePath -> result + +instance AppendPath RootPath LocalPath where + appendPath root local + | Path.isAbsolute local = global local `relativeTo` root + | otherwise = LocalPath { root, local } + +instance AppendPath LocalPath LocalPath where + appendPath (LocalPath { root, local }) path + | Path.isAbsolute path = global path `relativeTo` root + | otherwise = LocalPath { root, local: Path.concat [ local, path ] } + +instance AppendPath GlobalPath GlobalPath where + appendPath (GlobalPath path) p + | Path.isAbsolute p = GlobalPath p + | otherwise = GlobalPath $ Path.concat [ path, p ] + +infixl 5 appendPath as + +-- | The only publicly available way to create a root path. This function is +-- | intentionally made effectful, even though it doesn't have to be, to make it +-- | difficult to use it accidentally. +mkRoot :: ∀ m path. MonadEffect m => IsPath path => path -> m RootPath +mkRoot path = pure $ RootPath $ toRaw path + +global :: String -> GlobalPath +global = GlobalPath + +rootPart :: LocalPath -> RootPath +rootPart (LocalPath { root }) = root + +localPart :: LocalPath -> RawFilePath +localPart (LocalPath { local }) = local + +dirname :: ∀ path. IsPath path => path -> GlobalPath +dirname path = global $ Node.Path.dirname $ toRaw path + +replaceExtension_ :: String.Pattern -> String.Replacement -> String -> Maybe String +replaceExtension_ p (String.Replacement r) = map (_ <> r) <<< String.stripSuffix p + +basename :: ∀ path. IsPath path => path -> String +basename path = Node.Path.basename $ toRaw path + +isPrefixOf :: ∀ path1 path2. IsPath path1 => IsPath path2 => path1 -> path2 -> Boolean +isPrefixOf prefix whole = isJust $ String.stripPrefix (String.Pattern $ toRaw prefix) (toRaw whole) + +toAbsolute :: ∀ m path. IsPath path => MonadEffect m => path -> m GlobalPath +toAbsolute path = liftEffect $ GlobalPath <$> Node.Path.resolve [] (toRaw path) + +toRaw :: ∀ path. IsPath path => path -> String +toRaw p = let (GlobalPath g) = toGlobal p in g + +withForwardSlashes' :: String -> String +withForwardSlashes' = String.replaceAll (String.Pattern "\\") (String.Replacement "/") + +localPathCodec :: RootPath -> CJ.Codec LocalPath +localPathCodec root = CJ.string # dimap printLocalPath (root _) + +-- | Formats the local part of the path for user-friendly printing. The only +-- | difference (for now) is that the "root" path is represented to the user as +-- | "./" so as not to get them confused by a seeming absence of output. +printLocalPath :: LocalPath -> String +printLocalPath p = + let + l = localPart p + in + if l == "" then "./" else l + +-- This function is special handling for Windows, which may have multiple +-- different file systems with separate roots and no way to build a relative +-- path from one root to another, for example C:\foo\bar and D:\qux\baz. When +-- this happens as we try to build a `LocalPath` as a relative from a given +-- `RootPath`, and the two paths are from different file systems, we cannot use +-- the same `RootPath` as root of the result. +-- +-- For example: +-- +-- -- POSIX case: +-- (GlobalPath "/a/b") `relativeTo` (RootPath "/a/x") == LocalPath { root: RootPath "/a/x", local: "../b" } +-- +-- -- Windows case on the same drive: +-- (GlobalPath "C:\\a\\b") `relativeTo` (RootPath "C:\\a\\x") == LocalPath { root: RootPath "C:\\a\\x", local: "..\\b" } +-- +-- -- But Windows case on different drives: +-- (GlobalPath "C:\\a\\b") `relativeTo` (RootPath "D:\\a\\x") == LocalPath { root: RootPath "C:\\a\\b", local: "" } +-- +-- In the last case the root path `D:\\a\\x` could not be used as the root of +-- the resulting `LocalPath`, because there is no way to build a relative path +-- from it to `C:\\a\\b`. +areBothAbsolutePathsOnDifferentDrives :: String -> String -> Boolean +areBothAbsolutePathsOnDifferentDrives a b + | Node.platform == Just Node.Win32 = + Path.isAbsolute a && Path.isAbsolute b && driveLetter a /= driveLetter b + where + driveLetter s = String.toLower $ String.take 1 s + | otherwise = + false diff --git a/core/src/Prelude.purs b/core/src/Prelude.purs index e41556bdc..585724c71 100644 --- a/core/src/Prelude.purs +++ b/core/src/Prelude.purs @@ -43,12 +43,12 @@ import Effect.Exception.Unsafe (unsafeThrow) as Extra import Effect.Ref (Ref) as Extra import Node.Buffer (Buffer) as Extra import Node.Encoding (Encoding(..)) as Extra -import Node.Path (FilePath) as Extra import Partial.Unsafe (unsafeCrashWith) import Registry.ManifestIndex (ManifestIndex) as Extra import Registry.Types (PackageName, Version, Range, Location, License, Manifest(..), Metadata(..), Sha256) as Extra import Spago.Json (printJson, parseJson) as Extra import Spago.Log (logDebug, logError, logInfo, Docc, logSuccess, logWarn, die, die', justOrDieWith, justOrDieWith', rightOrDie, rightOrDie_, rightOrDieWith, rightOrDieWith', toDoc, indent, indent2, output, LogEnv, LogOptions, OutputFormat(..)) as Extra +import Spago.Path (RawFilePath, GlobalPath, LocalPath, RootPath, class AppendPath, appendPath, ()) as Extra import Spago.Yaml (YamlDoc, printYaml, parseYaml) as Extra newtype Spago env a = Spago (ReaderT env Extra.Aff a) diff --git a/src/Spago/BuildInfo.purs b/src/Spago/BuildInfo.purs index 9f40af61a..706c77442 100644 --- a/src/Spago/BuildInfo.purs +++ b/src/Spago/BuildInfo.purs @@ -4,7 +4,6 @@ import Spago.Prelude import Data.Array.NonEmpty as NEA import Data.String as String -import Node.Path as Path import Registry.PackageName as PackageName import Registry.Version as Version import Spago.Config (Workspace, WorkspacePackage) @@ -30,25 +29,26 @@ type BuildInfo = type BuildInfoEnv a = { workspace :: Workspace , logOptions :: LogOptions + , rootPath :: RootPath , purs :: Purs | a } writeBuildInfo :: forall a. Spago (BuildInfoEnv a) Unit writeBuildInfo = do - { workspace, purs } <- ask + { workspace, purs, rootPath } <- ask let buildInfo = { pursVersion: Version.print purs.version , packages: map mkPackageBuildInfo $ NEA.toUnfoldable $ Config.getWorkspacePackages workspace.packageSet } buildInfoString = mkBuildInfo buildInfo - writeIt = FS.writeTextFile buildInfoPath buildInfoString + writeIt = FS.writeTextFile (buildInfoPath rootPath) buildInfoString -- try to write the new build info only if necessary - FS.exists buildInfoPath >>= case _ of + FS.exists (buildInfoPath rootPath) >>= case _ of false -> writeIt true -> do - currentContent <- FS.readTextFile buildInfoPath + currentContent <- FS.readTextFile (buildInfoPath rootPath) when (currentContent /= buildInfoString) do writeIt @@ -77,8 +77,8 @@ mkBuildInfo { packages, pursVersion } = String.joinWith "\n" renderPackageType p = "\"" <> p.name <> "\" :: String" currentSpagoVersion = BuildInfo.packages."spago-bin" -buildInfoPath ∷ FilePath -buildInfoPath = Path.concat [ Paths.localCachePath, "BuildInfo.purs" ] +buildInfoPath ∷ RootPath -> LocalPath +buildInfoPath root = root Paths.localCachePath "BuildInfo.purs" mkPackageBuildInfo :: WorkspacePackage -> { name :: String, version :: String } mkPackageBuildInfo { package } = diff --git a/src/Spago/Cmd.purs b/src/Spago/Cmd.purs index 5da5f687b..22aeff715 100644 --- a/src/Spago/Cmd.purs +++ b/src/Spago/Cmd.purs @@ -13,6 +13,7 @@ import Node.Library.Execa as Execa import Node.Platform as Platform import Node.Process as Process import Partial.Unsafe (unsafeCrashWith, unsafePartial) +import Spago.Path as Path import Unsafe.Coerce (unsafeCoerce) data StdinConfig @@ -77,7 +78,7 @@ type ExecOptions = { pipeStdin :: StdinConfig , pipeStdout :: Boolean , pipeStderr :: Boolean - , cwd :: Maybe FilePath + , cwd :: Maybe GlobalPath , shell :: Boolean } @@ -97,18 +98,16 @@ spawn cmd args opts = liftAff do StdinPipeParent -> Just inherit StdinWrite _ -> Just pipe StdinNewPipe -> Just pipe - subprocess <- Execa.execa cmd args - ( _ - { cwd = opts.cwd - , stdin = stdinOpt - , stdout = Just pipe - , stderr = Just pipe - , shell = case opts.shell of - -- TODO: execa doesn't support the boolean option yet - true -> Just (unsafeCoerce true) - false -> Nothing - } - ) + subprocess <- Execa.execa cmd args _ + { cwd = Path.toRaw <$> opts.cwd + , stdin = stdinOpt + , stdout = Just pipe + , stderr = Just pipe + , shell = case opts.shell of + -- TODO: execa doesn't support the boolean option yet + true -> Just (unsafeCoerce true) + false -> Nothing + } case opts.pipeStdin of StdinWrite s | Just { writeUtf8End } <- subprocess.stdin -> writeUtf8End s @@ -128,9 +127,9 @@ joinProcess cp = do Normally 0 -> pure $ Right result _ -> pure $ Left result -exec :: forall m. MonadAff m => String -> Array String -> ExecOptions -> m (Either ExecResult ExecResult) +exec :: forall m. MonadAff m => GlobalPath -> Array String -> ExecOptions -> m (Either ExecResult ExecResult) exec cmd args opts = liftAff do - result <- _.getResult =<< spawn cmd args opts + result <- _.getResult =<< spawn (Path.toRaw cmd) args opts case result.exit of Normally 0 -> pure $ Right result _ -> pure $ Left result @@ -197,7 +196,7 @@ findFlag { flags, args } = if argsLen == 0 then Nothing else go 0 isSingleCharFlag = eq (Just 1) <<< map String.length <<< String.stripPrefix (Pattern "-") -getExecutable :: forall a. String -> Spago (LogEnv a) { cmd :: String, output :: String } +getExecutable :: ∀ a. String -> Spago (LogEnv a) { cmd :: GlobalPath, output :: String } getExecutable command = case Process.platform of Just Platform.Win32 -> do @@ -220,7 +219,7 @@ getExecutable command = where askVersion cmd shell = exec cmd [ "--version" ] defaultExecOptions { pipeStdout = false, pipeStderr = false, shell = shell } - mkCmd cmd maybeExtension = cmd <> maybe "" (append ".") maybeExtension + mkCmd cmd maybeExtension = Path.global $ cmd <> maybe "" (append ".") maybeExtension complain err = do logDebug $ printExecResult err diff --git a/src/Spago/Command/Auth.purs b/src/Spago/Command/Auth.purs index 75f0f8b3e..d01e374a4 100644 --- a/src/Spago/Command/Auth.purs +++ b/src/Spago/Command/Auth.purs @@ -6,26 +6,29 @@ import Spago.Prelude import Data.Array as Array import Data.String (Pattern(..)) import Data.String as String -import Node.Path as Path import Registry.SSH as SSH import Spago.Command.Fetch (FetchEnv) import Spago.Config as Config import Spago.FS as FS +import Spago.Path as Path +import Spago.Paths as Paths -type AuthArgs = { keyPath :: FilePath } +type AuthArgs = { keyPath :: RawFilePath } -run :: AuthArgs -> Spago (FetchEnv _) Unit +run :: ∀ r. AuthArgs -> Spago (FetchEnv r) Unit run { keyPath } = do logDebug $ "Authenticating with key at path " <> keyPath + + here <- Paths.cwd let -- we don't want to accidentally read the private key, so we always point to the public - path = case String.stripSuffix (Pattern ".pub") keyPath of + path = here case String.stripSuffix (Pattern ".pub") keyPath of Just _rest -> keyPath Nothing -> keyPath <> ".pub" newOwner <- FS.exists path >>= case _ of false -> do - die $ "Cannot read public key at path " <> show path <> ": file does not exist." + die $ "Cannot read public key at path " <> Path.quote path <> ": file does not exist." true -> do content <- FS.readTextFile path let result = SSH.parsePublicKey content @@ -34,11 +37,14 @@ run { keyPath } = do Right public -> pure $ SSH.publicKeyToOwner public logDebug $ "Parsed owner: " <> show (unwrap newOwner) - { workspace } <- ask + { workspace, rootPath } <- ask { doc, package, configPath } <- case workspace.selected, workspace.rootPackage of - Just { doc, package, path: packagePath }, _ -> pure { doc, package, configPath: Path.concat [ packagePath, "spago.yaml" ] } - Nothing, Just rootPackage -> pure { doc: workspace.doc, package: rootPackage, configPath: "spago.yaml" } - Nothing, Nothing -> die "No package was selected. Please select a package with the -p flag" + Just { doc, package, path: packagePath }, _ -> + pure { doc, package, configPath: packagePath "spago.yaml" } + Nothing, Just rootPackage -> + pure { doc: workspace.doc, package: rootPackage, configPath: rootPath "spago.yaml" } + Nothing, Nothing -> + die "No package was selected. Please select a package with the -p flag" case package.publish of Nothing -> die @@ -51,7 +57,8 @@ run { keyPath } = do case Array.elem newOwner currentOwners of true -> logWarn "Selected key is already present in the config file." false -> do - logInfo $ "Adding selected key to the list of the owners: " <> path - Config.addOwner configPath doc newOwner + logInfo $ "Adding selected key to the list of the owners: " <> Path.quote path + doc' <- justOrDieWith doc Config.configDocMissingErrorMessage + Config.addOwner configPath doc' newOwner logSuccess "The selected key has been added to the list of the owners." logInfo "Once you publish a new version with this configuration you'll be able to transfer and unpublish packages using this key." diff --git a/src/Spago/Command/Build.purs b/src/Spago/Command/Build.purs index 9bdabea50..36fc8f2fe 100644 --- a/src/Spago/Command/Build.purs +++ b/src/Spago/Command/Build.purs @@ -22,6 +22,7 @@ import Spago.Config (Package(..), PackageMap, WithTestGlobs(..), Workspace, Work import Spago.Config as Config import Spago.Git (Git) import Spago.Log (prepareToDie) +import Spago.Path as Path import Spago.Psa as Psa import Spago.Purs (Purs) import Spago.Purs.Graph as Graph @@ -31,6 +32,7 @@ type BuildEnv a = , git :: Git , dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions + , rootPath :: RootPath , workspace :: Workspace , strictWarnings :: Maybe Boolean , pedanticPackages :: Boolean @@ -49,6 +51,7 @@ run opts = do { dependencies , workspace , logOptions + , rootPath , strictWarnings , pedanticPackages } <- ask @@ -64,7 +67,7 @@ run opts = do let addOutputArgs args = case workspace.buildOptions.output of Nothing -> args - Just output -> args <> [ "--output", output ] + Just output -> args <> [ "--output", Path.toRaw output ] -- find the `--json-errors` flag and die if it's there - Spago handles it when (isJust $ Cmd.findFlag { flags: [ "--json-errors" ], args: opts.pursArgs }) do @@ -73,15 +76,6 @@ run opts = do , "Use the --json-errors flag for Spago." ] - {- - TODO: before, then, else - buildAction globs = do - let action = buildBackend globs >> (fromMaybe (pure ()) maybePostBuild) - runCommands "Before" beforeCommands - action `onException` (runCommands "Else" elseCommands) - runCommands "Then" thenCommands - -} - when (isJust $ Cmd.findFlag { flags: [ "-g", "--codegen" ], args: opts.pursArgs }) do die [ "Can't pass the `--codegen` option to purs, Spago already does that for you." @@ -103,7 +97,8 @@ run opts = do Just p -> NEA.singleton p Nothing -> Config.getWorkspacePackages workspace.packageSet globs = getBuildGlobs - { dependencies: case workspace.selected of + { rootPath + , dependencies: case workspace.selected of Just p -> let { core, test } = unsafeFromJust $ Map.lookup p.package.name dependencies @@ -116,7 +111,8 @@ run opts = do , selected: selectedPackages } pathDecisions <- liftEffect $ sequence $ Psa.toPathDecisions - { allDependencies + { rootPath + , allDependencies , selectedPackages: NEA.toArray selectedPackages , psaCliFlags: { strict: strictWarnings, statVerbosity: workspace.buildOptions.statVerbosity } , censorLibWarnings: workspace.buildOptions.censorLibWarnings @@ -129,7 +125,7 @@ run opts = do , statVerbosity: fromMaybe Psa.defaultStatVerbosity workspace.buildOptions.statVerbosity } - built <- Psa.psaCompile globs args psaArgs + built <- Psa.psaCompile rootPath globs args psaArgs backendBuilt <- case workspace.backend of _ | not built -> pure false Nothing -> pure true @@ -140,7 +136,7 @@ run opts = do moreBackendArgs = case backend.args of Just as | Array.length as > 0 -> as _ -> [] - Cmd.exec backend.cmd (addOutputArgs moreBackendArgs) Cmd.defaultExecOptions >>= case _ of + Cmd.exec (Path.global backend.cmd) (addOutputArgs moreBackendArgs) Cmd.defaultExecOptions >>= case _ of Left r -> do logDebug $ Cmd.printExecResult r prepareToDie [ "Failed to build with backend " <> backend.cmd ] $> false @@ -160,7 +156,7 @@ run opts = do pure true else do logInfo "Looking for unused and undeclared transitive dependencies..." - eitherGraph <- Graph.runGraph globs opts.pursArgs + eitherGraph <- Graph.runGraph rootPath globs opts.pursArgs logDebug "Decoded the output of `purs graph` successfully. Analyzing dependencies..." eitherGraph # either (prepareToDie >>> (_ $> false)) \graph -> do env <- ask @@ -178,15 +174,16 @@ run opts = do -- then we could use the graph to remove outdated modules from `output`! type BuildGlobsOptions = - { withTests :: Boolean + { rootPath :: RootPath + , withTests :: Boolean , depsOnly :: Boolean , selected :: NonEmptyArray WorkspacePackage , dependencies :: PackageMap } -getBuildGlobs :: BuildGlobsOptions -> Set FilePath -getBuildGlobs { selected, dependencies, withTests, depsOnly } = - Set.fromFoldable $ projectGlobs <> monorepoPkgGlobs <> dependencyGlobs <> [ BuildInfo.buildInfoPath ] +getBuildGlobs :: BuildGlobsOptions -> Set LocalPath +getBuildGlobs { rootPath, selected, dependencies, withTests, depsOnly } = + Set.fromFoldable $ projectGlobs <> monorepoPkgGlobs <> dependencyGlobs <> [ BuildInfo.buildInfoPath rootPath ] where -- Here we select the right globs for a monorepo setup with a bunch of packages projectGlobs = case depsOnly of @@ -199,15 +196,15 @@ getBuildGlobs { selected, dependencies, withTests, depsOnly } = true -> WithTestGlobs false -> NoTestGlobs - workspacePackageGlob :: WorkspacePackage -> Array String - workspacePackageGlob p = Config.sourceGlob testGlobs p.package.name (WorkspacePackage p) + workspacePackageGlob :: WorkspacePackage -> Array LocalPath + workspacePackageGlob p = Config.sourceGlob rootPath testGlobs p.package.name (WorkspacePackage p) { yes: monorepoPkgs, no: dependencyPkgs } = partition isWorkspacePackage $ Map.toUnfoldable dependencies -- depsOnly means "no packages from the monorepo", so we filter out the workspace packages - dependencyGlobs = (Tuple.uncurry $ Config.sourceGlob NoTestGlobs) =<< dependencyPkgs + dependencyGlobs = (Tuple.uncurry $ Config.sourceGlob rootPath NoTestGlobs) =<< dependencyPkgs monorepoPkgGlobs | depsOnly = [] - | otherwise = (Tuple.uncurry $ Config.sourceGlob NoTestGlobs) =<< monorepoPkgs + | otherwise = (Tuple.uncurry $ Config.sourceGlob rootPath NoTestGlobs) =<< monorepoPkgs isWorkspacePackage :: Tuple PackageName Package -> Boolean isWorkspacePackage = case _ of diff --git a/src/Spago/Command/Bundle.purs b/src/Spago/Command/Bundle.purs index ff8f80f65..b12e9ceda 100644 --- a/src/Spago/Command/Bundle.purs +++ b/src/Spago/Command/Bundle.purs @@ -5,16 +5,17 @@ import Spago.Prelude import Data.Array (all, fold, take) import Data.String as Str import Data.String.Utils (startsWith) -import Node.Path as Path import Spago.Cmd as Cmd import Spago.Config (BundlePlatform(..), BundleType(..), Workspace, WorkspacePackage) import Spago.Esbuild (Esbuild) import Spago.FS as FS import Spago.Generated.BuildInfo as BuildInfo +import Spago.Path as Path type BundleEnv a = { esbuild :: Esbuild , logOptions :: LogOptions + , rootPath :: RootPath , bundleOptions :: BundleOptions , workspace :: Workspace , selected :: WorkspacePackage @@ -25,30 +26,21 @@ type BundleOptions = { minify :: Boolean , sourceMaps :: Boolean , module :: String - , outfile :: FilePath + , outfile :: RawFilePath , force :: Boolean , platform :: BundlePlatform , type :: BundleType , extraArgs :: Array String } -type RawBundleOptions = - { minify :: Boolean - , module :: String - , outfile :: FilePath - , platform :: String - , type :: String - , extraArgs :: Array String - } - run :: ∀ a. Spago (BundleEnv a) Unit run = do - { esbuild, selected, workspace, bundleOptions: opts } <- ask + { rootPath, esbuild, selected, workspace, bundleOptions: opts } <- ask logDebug $ "Bundle options: " <> show opts let minify = if opts.minify then [ "--minify" ] else [] sourceMap = if opts.sourceMaps then [ "--sourcemap" ] else [] - outfile = Path.concat [ selected.path, opts.outfile ] + outfile = selected.path opts.outfile format = case opts.platform, opts.type of BundleBrowser, BundleApp -> "--format=iife" _, _ -> "--format=esm" @@ -57,9 +49,9 @@ run = do BundleNode -> s BundleBrowser -> "" - output = workspace.buildOptions.output # fromMaybe "output" + output = workspace.buildOptions.output # fromMaybe (rootPath "output") -- TODO: we might need to use `Path.relative selected.path output` instead of just output there - mainPath = withForwardSlashes $ Path.concat [ output, opts.module, "index.js" ] + mainPath = Path.localPart $ Path.withForwardSlashes $ output opts.module "index.js" { input, entrypoint } = case opts.type of BundleApp -> @@ -71,7 +63,7 @@ run = do , input: Cmd.StdinNewPipe } - execOptions = Cmd.defaultExecOptions { pipeStdin = input } + execOptions = Cmd.defaultExecOptions { pipeStdin = input, cwd = Just (Path.toGlobal rootPath) } banner = fold [ bundleWatermarkPrefix @@ -82,7 +74,7 @@ run = do args = fold [ [ "--bundle" - , "--outfile=" <> outfile + , "--outfile=" <> (Path.toRaw outfile) , "--platform=" <> show opts.platform , "--banner:js=" <> banner , "--loader:.node=file" -- See https://github.com/evanw/esbuild/issues/1051 @@ -95,10 +87,10 @@ run = do ] -- FIXME: remove this after 2024-12-01 - whenM (FS.exists checkWatermarkMarkerFileName) + whenM (FS.exists $ rootPath checkWatermarkMarkerFileName) $ unless opts.force $ whenM (isNotSpagoGeneratedFile outfile) - $ die [ "Target file " <> opts.outfile <> " was not previously generated by Spago. Use --force to overwrite anyway." ] + $ die [ "Target file " <> Path.quote outfile <> " was not previously generated by Spago. Use --force to overwrite anyway." ] logInfo "Bundling..." logDebug $ "Running esbuild: " <> show args @@ -108,7 +100,7 @@ run = do logDebug $ Cmd.printExecResult r die [ "Failed to bundle." ] -isNotSpagoGeneratedFile :: ∀ a. String -> Spago (BundleEnv a) Boolean +isNotSpagoGeneratedFile :: ∀ a. LocalPath -> Spago (BundleEnv a) Boolean isNotSpagoGeneratedFile path = do exists <- FS.exists path if not exists then diff --git a/src/Spago/Command/Docs.purs b/src/Spago/Command/Docs.purs index 0ea06a5b3..5268fe87f 100644 --- a/src/Spago/Command/Docs.purs +++ b/src/Spago/Command/Docs.purs @@ -23,26 +23,28 @@ type DocsEnv a = , workspace :: Workspace , dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions + , rootPath :: RootPath , docsFormat :: DocsFormat , depsOnly :: Boolean , open :: Boolean | a } -run :: Spago (DocsEnv _) Unit +run :: ∀ a. Spago (DocsEnv a) Unit run = do logDebug "Running `spago docs`" logInfo "Generating documentation for the project. This might take a while..." - { workspace, dependencies, docsFormat, depsOnly, open } <- ask + { rootPath, workspace, dependencies, docsFormat, depsOnly, open } <- ask let globs = Build.getBuildGlobs - { withTests: true + { rootPath + , withTests: true , selected: Config.getWorkspacePackages workspace.packageSet , dependencies: Fetch.toAllDependencies dependencies , depsOnly } - result <- Purs.docs globs docsFormat + result <- Purs.docs rootPath globs docsFormat case result of Left r -> die r.message _ -> pure unit diff --git a/src/Spago/Command/Fetch.purs b/src/Spago/Command/Fetch.purs index 823cb4cc0..bea158293 100644 --- a/src/Spago/Command/Fetch.purs +++ b/src/Spago/Command/Fetch.purs @@ -38,7 +38,6 @@ import Effect.Aff.AVar as AVar import Effect.Ref as Ref import Node.Buffer as Buffer import Node.Encoding as Encoding -import Node.Path as Path import Registry.Internal.Codec as Internal.Codec import Registry.Metadata as Metadata import Registry.PackageName as PackageName @@ -54,6 +53,7 @@ import Spago.FS as FS import Spago.Git as Git import Spago.Lock (LockEntry(..)) import Spago.Lock as Lock +import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs as Purs import Spago.Registry as Registry @@ -66,6 +66,7 @@ type FetchEnvRow a = ( getRegistry :: Spago (Registry.PreRegistryEnv ()) Registry.RegistryFunctions , workspace :: Workspace , logOptions :: LogOptions + , rootPath :: Path.RootPath , offline :: OnlineStatus , purs :: Purs.Purs , git :: Git.Git @@ -86,20 +87,22 @@ run :: forall a. FetchOpts -> Spago (FetchEnv a) PackageTransitiveDeps run { packages: packagesRequestedToInstall, ensureRanges, isTest, isRepl } = do logDebug $ "Requested to install these packages: " <> printJson (CJ.array PackageName.codec) packagesRequestedToInstall - { workspace: currentWorkspace } <- ask + { workspace: currentWorkspace, rootPath } <- ask let getPackageConfigPath errorMessageEnd = do - case currentWorkspace.selected, currentWorkspace.rootPackage of + res <- case currentWorkspace.selected, currentWorkspace.rootPackage of Just { path, doc, package }, _ -> - pure { configPath: Path.concat [ path, "spago.yaml" ], yamlDoc: doc, package } + pure { configPath: path "spago.yaml", yamlDoc: doc, package } _, Just rootPackage -> - pure { configPath: "spago.yaml", yamlDoc: currentWorkspace.doc, package: rootPackage } + pure { configPath: rootPath "spago.yaml", yamlDoc: currentWorkspace.doc, package: rootPackage } Nothing, Nothing -> die [ "No package found in the root configuration." , "Please use the `-p` flag to select a package " <> errorMessageEnd ] + pure res { yamlDoc = res.yamlDoc } + installingPackagesData <- do case packagesRequestedToInstall of [] -> @@ -162,18 +165,20 @@ run { packages: packagesRequestedToInstall, ensureRanges, isTest, isRepl } = do countString = case Array.length actualPackagesToInstall of 1 -> "1 package" n -> show n <> " packages" - logInfo $ "Adding " <> countString <> " to the config in " <> configPath - liftAff $ Config.addPackagesToConfig configPath yamlDoc isTest actualPackagesToInstall + logInfo $ "Adding " <> countString <> " to the config in " <> Path.quote configPath + doc <- justOrDieWith yamlDoc Config.configDocMissingErrorMessage + liftAff $ Config.addPackagesToConfig configPath doc isTest actualPackagesToInstall -- if the flag is selected, we kick off the process of adding ranges to the config when ensureRanges do { configPath, package, yamlDoc } <- getPackageConfigPath "in which to add ranges." - logInfo $ "Adding ranges to core dependencies to the config in " <> configPath + logInfo $ "Adding ranges to core dependencies to the config in " <> Path.quote configPath packageDeps <- (Map.lookup package.name dependencies) `justOrDieWith` "Impossible: package dependencies must be in dependencies map" let rangeMap = map getRangeFromPackage packageDeps.core - liftEffect $ Config.addRangesToConfig yamlDoc rangeMap - liftAff $ FS.writeYamlDocFile configPath yamlDoc + doc <- justOrDieWith yamlDoc Config.configDocMissingErrorMessage + liftEffect $ Config.addRangesToConfig doc rangeMap + liftAff $ FS.writeYamlDocFile configPath doc -- the repl needs a support package, so we add it here as a sidecar supportPackage <- Repl.supportPackage workspace.packageSet @@ -212,7 +217,8 @@ fetchPackagesToLocalCache packages = do GitPackage gitPackage -> (Just <<< Tuple gitPackage.git) <$> AVar.new unit _ -> pure Nothing parallelise $ packages # Map.toUnfoldable <#> \(Tuple name package) -> do - let localPackageLocation = Config.getPackageLocation name package + { rootPath } <- ask + let localPackageLocation = Config.getLocalPackageLocation rootPath name package -- first of all, we check if we have the package in the local cache. If so, we don't even do the work unlessM (FS.exists localPackageLocation) case package of GitPackage gitPackage -> do @@ -234,8 +240,8 @@ fetchPackagesToLocalCache packages = do Right versionMetadata -> do logDebug $ "Metadata read: " <> printJson Metadata.publishedMetadataCodec versionMetadata -- then check if we have a tarball cached. If not, download it - let globalCachePackagePath = Path.concat [ Paths.globalCachePath, "packages", PackageName.print name ] - let archivePath = Path.concat [ globalCachePackagePath, versionString <> ".tar.gz" ] + let globalCachePackagePath = Paths.globalCachePath "packages" PackageName.print name + let archivePath = globalCachePackagePath (versionString <> ".tar.gz") FS.mkdirp globalCachePackagePath -- We need to see if the tarball is there, and if we can decompress it. -- This is because if Spago is killed while it's writing the tar, then it might leave it corrupted. @@ -247,7 +253,7 @@ fetchPackagesToLocalCache packages = do FS.mkdirp tempDir tarIsGood <- if tarExists then do - logDebug $ "Trying to unpack archive to temp folder: " <> tempDir + logDebug $ "Trying to unpack archive to temp folder: " <> Path.quote tempDir map (either (const false) (const true)) $ liftEffect $ Tar.extract { filename: archivePath, cwd: tempDir } else pure false @@ -288,14 +294,14 @@ fetchPackagesToLocalCache packages = do unless (archiveSha == versionMetadata.hash) do die $ "Archive fetched for " <> packageVersion <> " has a different hash (" <> Sha256.print archiveSha <> ") than expected (" <> Sha256.print versionMetadata.hash <> ")" -- if everything's alright we stash the tar in the global cache - logDebug $ "Fetched archive for " <> packageVersion <> ", saving it in the global cache: " <> archivePath + logDebug $ "Fetched archive for " <> packageVersion <> ", saving it in the global cache: " <> Path.quote archivePath FS.writeFile archivePath archiveBuffer - logDebug $ "Unpacking archive to temp folder: " <> tempDir + logDebug $ "Unpacking archive to temp folder: " <> Path.quote tempDir (liftEffect $ Tar.extract { filename: archivePath, cwd: tempDir }) >>= case _ of Right _ -> pure unit Left err -> die [ "Failed to decode downloaded package " <> packageVersion <> ", error:", show err ] - logDebug $ "Moving extracted file to local cache:" <> localPackageLocation - FS.moveSync { src: (Path.concat [ tempDir, tarInnerFolder ]), dst: localPackageLocation } + logDebug $ "Moving extracted file to local cache: " <> Path.quote localPackageLocation + FS.moveSync { src: tempDir tarInnerFolder, dst: Path.toGlobal localPackageLocation } -- Local package, no work to be done LocalPackage _ -> pure unit WorkspacePackage _ -> pure unit @@ -319,7 +325,7 @@ updateCache key value cacheRef = liftEffect $ Ref.modify_ (Map.insert key value) writeNewLockfile :: ∀ a. String -> PackageTransitiveDeps -> Spago (FetchEnv a) PackageTransitiveDeps writeNewLockfile reason allTransitiveDeps = do logInfo $ reason <> ", generating it..." - { workspace } <- ask + { workspace, rootPath } <- ask -- All these Refs are needed to memoise Db and file reads packageDependenciesCache <- liftEffect $ Ref.new Map.empty @@ -372,9 +378,9 @@ writeNewLockfile reason allTransitiveDeps = do WorkspacePackage _ -> Nothing GitPackage gitPackage -> Just do - let packageLocation = Config.getPackageLocation packageName package + let packageLocation = Config.getLocalPackageLocation rootPath packageName package withCache packageLocation gitRefCache do - Git.getRef (Just packageLocation) >>= case _ of + Git.getRef packageLocation >>= case _ of Left err -> die err -- TODO maybe not die here? Right rev -> do @@ -421,7 +427,7 @@ writeNewLockfile reason allTransitiveDeps = do , extra_packages: fromMaybe Map.empty workspace.workspaceConfig.extraPackages } } - liftAff $ FS.writeJsonFile Lock.lockfileCodec "spago.lock" lockfile + liftAff $ FS.writeJsonFile Lock.lockfileCodec (rootPath "spago.lock") lockfile logInfo "Lockfile written to spago.lock. Please commit this file." -- We update the dependencies here with the commit hashes that came from the getRef calls, @@ -446,15 +452,18 @@ toAllDependencies = >>> foldMap (\m -> [ m.core, m.test ]) >>> foldl Map.union Map.empty -getGitPackageInLocalCache :: forall a. PackageName -> GitPackage -> Spago (Git.GitEnv a) Unit +getGitPackageInLocalCache :: forall a. PackageName -> GitPackage -> Spago (FetchEnv a) Unit getGitPackageInLocalCache name package = do - ensureRepoCloned - ensureRefPresent + { rootPath } <- ask + FS.mkdirp $ rootPath Paths.localCachePackagesPath PackageName.print name + let repoCache = rootPath Paths.localCacheGitPath Config.fileSystemCharEscape package.git - let localPackageLocation = Config.getPackageLocation name (GitPackage package) - logDebug $ "Copying repo to " <> localPackageLocation - FS.mkdirp $ Path.concat [ Paths.localCachePackagesPath, PackageName.print name ] - FS.copyTree { src: repoCacheLocation, dst: localPackageLocation } + ensureRepoCloned repoCache + ensureRefPresent repoCache + + let localPackageLocation = Config.getLocalPackageLocation rootPath name (GitPackage package) + logDebug $ "Copying repo to " <> Path.quote localPackageLocation + FS.copyTree { src: repoCache, dst: localPackageLocation } logDebug $ "Checking out ref '" <> package.ref <> "'" Git.checkout { repo: localPackageLocation, ref: package.ref } >>= rightOrDie_ @@ -463,34 +472,31 @@ getGitPackageInLocalCache name package = do -- So we run getRef here and then do a copy if the ref is different than the original one -- (since it might be a commit to start with) logDebug $ "Checking if we need to copy the package to a commit hash location..." - commitHash <- Git.getRef (Just localPackageLocation) >>= rightOrDie + commitHash <- Git.getRef localPackageLocation >>= rightOrDie when (commitHash /= package.ref) do - let commitHashLocation = Config.getPackageLocation name (GitPackage $ package { ref = commitHash }) - logDebug $ "Copying the repo also to " <> commitHashLocation + let commitHashLocation = Config.getLocalPackageLocation rootPath name (GitPackage $ package { ref = commitHash }) + logDebug $ "Copying the repo also to " <> Path.quote commitHashLocation FS.copyTree { src: localPackageLocation, dst: commitHashLocation } where - repoCacheLocation = Path.concat [ Paths.localCacheGitPath, Config.fileSystemCharEscape package.git ] - - ensureRepoCloned = unlessM (FS.exists repoCacheLocation) do + ensureRepoCloned repoCache = unlessM (FS.exists repoCache) do tempDir <- mkTemp' (Just $ printJson Config.gitPackageCodec package) - logDebug $ "Cloning repo in " <> tempDir + logDebug $ "Cloning repo in " <> Path.quote tempDir Git.fetchRepo package tempDir >>= rightOrDie_ - logDebug $ "Repo cloned. Moving to " <> repoCacheLocation - FS.mkdirp $ Path.concat [ Paths.localCachePackagesPath, PackageName.print name ] - FS.moveSync { src: tempDir, dst: repoCacheLocation } + logDebug $ "Repo cloned. Moving to " <> Path.quote repoCache + FS.moveSync { src: tempDir, dst: Path.toGlobal repoCache } - ensureRefPresent = do + ensureRefPresent repoCache = do logDebug $ "Verifying ref " <> package.ref { offline } <- ask - Git.getRefType { repo: repoCacheLocation, ref: package.ref } >>= case _, offline of + Git.getRefType { repo: repoCache, ref: package.ref } >>= case _, offline of Right _, _ -> pure unit Left _, Offline -> die $ "Repo " <> package.git <> " does not have ref " <> package.ref <> " in local cache. Cannot pull from origin in offline mode." Left _, _ -> do logDebug $ "Ref " <> package.ref <> " is not present, trying to pull from origin" - Git.fetch { repo: repoCacheLocation, remote: "origin" } >>= rightOrDie_ + Git.fetch { repo: repoCache, remote: "origin" } >>= rightOrDie_ getPackageDependencies :: forall a. PackageName -> Package -> Spago (FetchEnv a) (Maybe (ByEnv (Map PackageName Range))) getPackageDependencies packageName package = case package of @@ -500,33 +506,32 @@ getPackageDependencies packageName package = case package of GitPackage p -> do -- Note: we get the package in local cache nonetheless, -- so we have guarantees about being able to fetch it - let packageLocation = Config.getPackageLocation packageName package + { rootPath } <- ask + let packageLocation = Config.getLocalPackageLocation rootPath packageName package unlessM (FS.exists packageLocation) do getGitPackageInLocalCache packageName p case p.dependencies of Just (Dependencies dependencies) -> pure $ Just { core: map (fromMaybe Config.widestRange) dependencies, test: Map.empty } Nothing -> do - readLocalDependencies case p.subdir of - Nothing -> packageLocation - Just s -> Path.concat [ packageLocation, s ] + readLocalDependencies $ Path.toGlobal $ maybe packageLocation (packageLocation _) p.subdir LocalPackage p -> do - readLocalDependencies p.path + readLocalDependencies $ Path.global p.path WorkspacePackage p -> pure $ Just $ (map (fromMaybe Config.widestRange) <<< unwrap) `onEachEnv` getWorkspacePackageDeps p where -- try to see if the package has a spago config, and if it's there we read it - readLocalDependencies :: FilePath -> Spago (FetchEnv a) (Maybe (ByEnv (Map PackageName Range))) + readLocalDependencies :: GlobalPath -> Spago (FetchEnv a) (Maybe (ByEnv (Map PackageName Range))) readLocalDependencies configLocation = do -- TODO: make this work with manifests - Config.readConfig (Path.concat [ configLocation, "spago.yaml" ]) >>= case _ of + Config.readConfig (configLocation "spago.yaml") >>= case _ of Right { yaml: { package: Just { dependencies: Dependencies deps, test } } } -> pure $ Just { core: fromMaybe Config.widestRange <$> deps , test: fromMaybe Config.widestRange <$> (test <#> _.dependencies <#> unwrap # fromMaybe Map.empty) } Right _ -> die - [ "Read the configuration at path " <> configLocation + [ "Read the configuration at path " <> Path.quote configLocation , "However, it didn't contain a `package` section." ] Left errLines -> die diff --git a/src/Spago/Command/Graph.purs b/src/Spago/Command/Graph.purs index ba2481788..a7269051f 100644 --- a/src/Spago/Command/Graph.purs +++ b/src/Spago/Command/Graph.purs @@ -22,6 +22,7 @@ import Spago.Purs.Graph as Graph type GraphEnv a = { dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions + , rootPath :: RootPath , workspace :: Workspace , purs :: Purs | a @@ -39,13 +40,13 @@ type GraphPackagesArgs = , topo :: Boolean } -graphModules :: forall a. GraphModulesArgs -> Spago (GraphEnv a) Unit +graphModules :: ∀ a. GraphModulesArgs -> Spago (GraphEnv a) Unit graphModules { dot, json, topo } = do - env@{ dependencies, workspace } <- ask + env@{ dependencies, workspace, rootPath } <- ask let allDependencies = Fetch.toAllDependencies dependencies let selected = Config.getWorkspacePackages workspace.packageSet - let globs = Build.getBuildGlobs { selected, withTests: false, dependencies: allDependencies, depsOnly: false } - eitherGraph <- Graph.runGraph globs [] + let globs = Build.getBuildGlobs { rootPath, selected, withTests: false, dependencies: allDependencies, depsOnly: false } + eitherGraph <- Graph.runGraph rootPath globs [] graph <- either die pure eitherGraph moduleGraph <- runSpago (Record.union { selected } env) (Graph.getModuleGraphWithPackage graph) @@ -67,11 +68,11 @@ graphModules { dot, json, topo } = do graphPackages :: forall a. GraphPackagesArgs -> Spago (GraphEnv a) Unit graphPackages { dot, json, topo } = do - env@{ dependencies, workspace } <- ask + env@{ dependencies, workspace, rootPath } <- ask let allDependencies = Fetch.toAllDependencies dependencies let selected = Config.getWorkspacePackages workspace.packageSet - let globs = Build.getBuildGlobs { selected, withTests: false, dependencies: allDependencies, depsOnly: false } - eitherGraph <- Graph.runGraph globs [] + let globs = Build.getBuildGlobs { rootPath, selected, withTests: false, dependencies: allDependencies, depsOnly: false } + eitherGraph <- Graph.runGraph rootPath globs [] graph <- either die pure eitherGraph packageGraph <- runSpago (Record.union { selected } env) (Graph.getPackageGraph graph) diff --git a/src/Spago/Command/Init.purs b/src/Spago/Command/Init.purs index 886281386..72a97a22a 100644 --- a/src/Spago/Command/Init.purs +++ b/src/Spago/Command/Init.purs @@ -16,17 +16,18 @@ import Spago.Prelude import Data.Map as Map import Data.String as String -import Node.Path as Path import Registry.PackageName as PackageName import Registry.Version as Version import Spago.Config (Dependencies(..), SetAddress(..), Config) import Spago.Config as Config import Spago.FS as FS import Spago.Log as Log -import Spago.Paths as Paths +import Spago.Path as Path import Spago.Registry (RegistryEnv) import Spago.Registry as Registry +type InitEnv a = RegistryEnv (rootPath :: RootPath | a) + data InitMode = InitWorkspace { packageName :: Maybe String } | InitSubpackage { packageName :: String } @@ -40,7 +41,7 @@ type InitOptions = -- TODO run git init? Is that desirable? -run :: ∀ a. InitOptions -> Spago (RegistryEnv a) Config +run :: ∀ a. InitOptions -> Spago (InitEnv a) Config run opts = do -- Use the specified version of the package set (if specified). -- Otherwise, get the latest version of the package set for the given compiler @@ -57,9 +58,9 @@ run opts = do let mainModuleName = "Main" testModuleName = "Test.Main" - srcDir = Path.concat [ projectDir, "src" ] - testDir = Path.concat [ projectDir, "test" ] - configPath = Path.concat [ projectDir, "spago.yaml" ] + srcDir = projectDir "src" + testDir = projectDir "test" + configPath = projectDir "spago.yaml" config = defaultConfig { name: packageName, withWorkspace, testModuleName } -- Write config @@ -71,16 +72,16 @@ run opts = do -- Because you might want to just init a project with your own source files, -- or just migrate a psc-package project whenDirNotExists srcDir do - copyIfNotExists (Path.concat [ srcDir, mainModuleName <> ".purs" ]) (srcMainTemplate mainModuleName) + copyIfNotExists (srcDir (mainModuleName <> ".purs")) (srcMainTemplate mainModuleName) whenDirNotExists testDir $ do - FS.mkdirp (Path.concat [ testDir, "Test" ]) - copyIfNotExists (Path.concat [ testDir, "Test", "Main.purs" ]) (testMainTemplate testModuleName) + FS.mkdirp (testDir "Test") + copyIfNotExists (testDir "Test" "Main.purs") (testMainTemplate testModuleName) case opts.mode of InitWorkspace _ -> do - copyIfNotExists ".gitignore" gitignoreTemplate - copyIfNotExists pursReplFile.name pursReplFile.content + copyIfNotExists (projectDir ".gitignore") gitignoreTemplate + copyIfNotExists (projectDir pursReplFile.name) pursReplFile.content InitSubpackage _ -> pure unit @@ -102,14 +103,15 @@ run opts = do true -> logInfo $ foundExistingFile dest false -> FS.writeTextFile dest srcTemplate - getPackageName :: Spago (RegistryEnv a) PackageName + getPackageName :: Spago (InitEnv a) PackageName getPackageName = do + { rootPath } <- ask let candidateName = case opts.mode of - InitWorkspace { packageName: Nothing } -> String.take 150 $ Path.basename Paths.cwd + InitWorkspace { packageName: Nothing } -> String.take 150 $ Path.basename rootPath InitWorkspace { packageName: Just n } -> n InitSubpackage { packageName: n } -> n - logDebug [ show Paths.cwd, show candidateName ] + logDebug [ Path.quote rootPath, "\"" <> candidateName <> "\"" ] pname <- case PackageName.parse (PackageName.stripPureScriptPrefix candidateName) of Left err -> die [ toDoc "Could not figure out a name for the new package. Error:" @@ -120,7 +122,7 @@ run opts = do logDebug [ "Got packageName and setVersion:", PackageName.print pname, unsafeStringify opts.setVersion ] pure pname - getWithWorkspace :: Version -> Spago (RegistryEnv a) (Maybe { setVersion :: Maybe Version }) + getWithWorkspace :: Version -> Spago (InitEnv a) (Maybe { setVersion :: Maybe Version }) getWithWorkspace setVersion = case opts.mode of InitWorkspace _ -> pure $ Just @@ -133,12 +135,13 @@ run opts = do logWarn "The --package-set and --use-solver flags are ignored when initializing a subpackage" pure Nothing - getProjectDir :: PackageName -> Spago (RegistryEnv a) FilePath + getProjectDir :: PackageName -> Spago (InitEnv a) LocalPath getProjectDir packageName = case opts.mode of InitWorkspace _ -> - pure "." + ask <#> _.rootPath <#> (_ "") InitSubpackage _ -> do - let dirPath = PackageName.print packageName + { rootPath } <- ask + let dirPath = rootPath PackageName.print packageName unlessM (FS.exists dirPath) $ FS.mkdirp dirPath pure dirPath @@ -288,11 +291,11 @@ pursReplFile = { name: ".purs-repl", content: "import Prelude\n" } -- ERROR TEXTS ----------------------------------------------------------------- -foundExistingProject :: FilePath -> String -foundExistingProject path = "Found a \"" <> path <> "\" file, skipping copy." +foundExistingProject :: LocalPath -> String +foundExistingProject path = "Found a " <> Path.quote path <> " file, skipping copy." -foundExistingDirectory :: FilePath -> String -foundExistingDirectory dir = "Found existing directory \"" <> dir <> "\", skipping copy of sample sources" +foundExistingDirectory :: LocalPath -> String +foundExistingDirectory dir = "Found existing directory " <> Path.quote dir <> ", skipping copy of sample sources" -foundExistingFile :: FilePath -> String -foundExistingFile file = "Found existing file \"" <> file <> "\", not overwriting it" +foundExistingFile :: LocalPath -> String +foundExistingFile file = "Found existing file " <> Path.quote file <> ", not overwriting it" diff --git a/src/Spago/Command/Ls.purs b/src/Spago/Command/Ls.purs index c4f645cef..8f0b15056 100644 --- a/src/Spago/Command/Ls.purs +++ b/src/Spago/Command/Ls.purs @@ -25,6 +25,7 @@ import Registry.Version as Version import Spago.Command.Fetch as Fetch import Spago.Config (BuildType(..), Package(..), Workspace, WorkspacePackage) import Spago.Config as Config +import Spago.Path as Path import Spago.Paths as Paths import Type.Proxy (Proxy(..)) @@ -53,6 +54,7 @@ type LsSetEnv = { dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions , workspace :: Workspace + , rootPath :: RootPath } type LsEnv = @@ -60,28 +62,30 @@ type LsEnv = , logOptions :: LogOptions , workspace :: Workspace , selected :: WorkspacePackage + , rootPath :: RootPath } -listPaths :: LsPathsArgs -> Spago { logOptions :: LogOptions } Unit +listPaths :: LsPathsArgs -> Spago { logOptions :: LogOptions, rootPath :: RootPath } Unit listPaths { json } = do logDebug "Running `listPaths`" + { rootPath } <- ask case json of true -> - output $ OutputJson (CJ.Common.map CJ.string CJ.string) $ Map.fromFoldable keyValuePairs + output $ OutputJson (CJ.Common.map CJ.string CJ.string) $ Map.fromFoldable (keyValuePairs rootPath) false -> output $ OutputTable { titles: [ "Name", "Path" ] - , rows: (\(Tuple k v) -> [ k, v ]) <$> keyValuePairs + , rows: (\(Tuple k v) -> [ k, v ]) <$> keyValuePairs rootPath } where - keyValuePairs = + keyValuePairs root = rmap Path.toRaw <$> [ Tuple "Global cache path" Paths.globalCachePath , Tuple "Global registry path" Paths.registryPath , Tuple "Global registry index path" Paths.registryIndexPath , Tuple "Global package sets path" Paths.packageSetsPath , Tuple "Global database path" Paths.databasePath - , Tuple "Local cache path" Paths.localCachePath - , Tuple "Local cache packages path" Paths.localCachePackagesPath + , Tuple "Local cache path" $ Path.toGlobal $ root Paths.localCachePath + , Tuple "Local cache packages path" $ Path.toGlobal $ root Paths.localCachePackagesPath ] -- TODO: add LICENSE field @@ -89,19 +93,19 @@ listPaths { json } = do listPackageSet :: LsPackagesArgs -> Spago LsSetEnv Unit listPackageSet { json } = do logDebug "Running `listPackageSet`" - { workspace } <- ask + { workspace, rootPath } <- ask case workspace.packageSet.buildType of RegistrySolverBuild _extraPackages -> die "Cannot list the packages in the package set, as none is configured for the project." PackageSetBuild _info packageSet -> do let packages = Map.toUnfoldable packageSet case json of - true -> formatPackagesJson packages + true -> formatPackagesJson rootPath packages false -> formatPackagesTable packages listPackages :: LsDepsOpts -> Spago LsEnv Unit listPackages { transitive, json } = do logDebug "Running `listPackages`" - { dependencies, selected } <- ask + { dependencies, selected, rootPath } <- ask let allDependencies = Fetch.toAllDependencies dependencies direct = (Map.keys <<< unwrap <<< _.dependencies <<< _.package) selected @@ -111,11 +115,11 @@ listPackages { transitive, json } = do case packages of [] -> logWarn "There are no dependencies listed in your configuration" _ -> case json of - true -> formatPackagesJson packages + true -> formatPackagesJson rootPath packages false -> formatPackagesTable packages -formatPackagesJson :: forall m. MonadEffect m => Array (Tuple PackageName Package) -> m Unit -formatPackagesJson packages = output $ OutputJson (packageMap packageCodec) (map wrapPackage $ Map.fromFoldable packages) +formatPackagesJson :: forall m. MonadEffect m => RootPath -> Array (Tuple PackageName Package) -> m Unit +formatPackagesJson root packages = output $ OutputJson (packageMap packageCodec) (map wrapPackage $ Map.fromFoldable packages) where wrapPackage value = { value @@ -151,7 +155,7 @@ formatPackagesJson packages = output $ OutputJson (packageMap packageCodec) (map encode = CJ.encode ( CJ.named "WorkspacePackage" $ CJ.Record.object - { path: CJ.string + { path: Path.localPathCodec root , package: Config.packageConfigCodec , hasTests: CJ.boolean } @@ -175,7 +179,7 @@ formatPackagesTable pkgs = output $ OutputTable RegistryVersion _ -> "-" GitPackage { git } -> git LocalPackage { path } -> path - WorkspacePackage { path } -> path + WorkspacePackage { path } -> Path.printLocalPath path showVersion :: Package -> String showVersion = case _ of diff --git a/src/Spago/Command/Publish.purs b/src/Spago/Command/Publish.purs index e50cf3ec8..91ef2a596 100644 --- a/src/Spago/Command/Publish.purs +++ b/src/Spago/Command/Publish.purs @@ -9,7 +9,7 @@ import Data.Map as Map import Data.String as String import Dodo (break, lines) import Effect.Ref as Ref -import Node.Path as Path +import Node.Path as Node.Path import Node.Process as Process import Record as Record import Registry.Internal.Path as Internal.Path @@ -30,6 +30,7 @@ import Spago.Git (Git) import Spago.Git as Git import Spago.Json as Json import Spago.Log as Log +import Spago.Path as Path import Spago.Prelude as Effect import Spago.Purs (Purs) import Spago.Purs.Graph as Graph @@ -48,6 +49,7 @@ type PublishEnv a = { getRegistry :: Spago (PreRegistryEnv ()) Registry.RegistryFunctions , workspace :: Workspace , logOptions :: LogOptions + , rootPath :: RootPath , offline :: OnlineStatus , git :: Git , db :: Db @@ -81,7 +83,7 @@ publish _args = do ) resultRef - env@{ selected: selected', purs, dependencies } <- ask + env@{ selected: selected', purs, dependencies, rootPath } <- ask let (selected :: WorkspacePackage) = selected' { hasTests = false } let name = selected.package.name let strName = PackageName.print name @@ -102,8 +104,8 @@ publish _args = do -- We then need to check that the dependency graph is accurate. If not, queue the errors let allCoreDependencies = Fetch.toAllDependencies $ dependencies <#> _ { test = Map.empty } - let globs = Build.getBuildGlobs { selected: NEA.singleton selected, withTests: false, dependencies: allCoreDependencies, depsOnly: false } - eitherGraph <- Graph.runGraph globs [] + let globs = Build.getBuildGlobs { rootPath, selected: NEA.singleton selected, withTests: false, dependencies: allCoreDependencies, depsOnly: false } + eitherGraph <- Graph.runGraph rootPath globs [] case eitherGraph of Right graph -> do graphCheckErrors <- Graph.toImportErrors selected { reportSrc: true, reportTest: false } @@ -186,7 +188,7 @@ publish _args = do , "submit a transfer operation." ] - locationResult <- locationIsInGitRemotes location + locationResult <- locationIsInGitRemotes rootPath location unless locationResult.result $ addError $ toDoc [ toDoc "The location specified in the manifest file is not one of the remotes in the git repository." , toDoc "Location:" @@ -218,19 +220,30 @@ publish _args = do -- All dependencies come from the registry so we can trust the build plan. -- We can then try to build with the dependencies from there. - Internal.Path.readPursFiles (Path.concat [ selected.path, "src" ]) >>= case _ of + Internal.Path.readPursFiles (Path.toRaw $ selected.path "src") >>= case _ of Nothing -> addError $ toDoc [ "This package has no PureScript files in its `src` directory. " , "All package sources must be in the `src` directory, with any additional " , "sources indicated by the `files` key in your manifest." ] Just files -> do + let + -- `validatePursModules` returns full paths in its response, so + -- we need to strip the workspace root path to print it out in + -- user-friendly way, but we can't use the machinery from + -- `Spago.Paths`, because the paths are embedded in other text, + -- so we have to resort to substring matching. + rootPathPrefix = + Path.toRaw rootPath + # String.stripSuffix (String.Pattern "/") + # fromMaybe (Path.toRaw rootPath) + # (_ <> Node.Path.sep) Operation.Validation.validatePursModules files >>= case _ of Left formattedError -> addError $ toDoc [ "This package has either malformed or disallowed PureScript module names" , "in its `src` directory. All package sources must be in the `src` directory," , "with any additional sources indicated by the `files` key in your manifest." - , formattedError + , formattedError # String.replaceAll (String.Pattern rootPathPrefix) (String.Replacement "") ] Right _ -> pure unit @@ -262,7 +275,7 @@ publish _args = do -- 2) input any login credentials as there are other errors to fix -- before doing that. -- The "hard" git tag checks will occur only if these succeed. - Git.getStatus Nothing >>= case _ of + Git.getStatus rootPath >>= case _ of Left _err -> do die $ toDoc [ toDoc "Could not verify whether the git tree is clean. Error was:" @@ -277,7 +290,7 @@ publish _args = do | otherwise -> do -- TODO: once we ditch `purs publish`, we don't have a requirement for a tag anymore, -- but we can use any ref. We can then use `getRef` here instead of `tagCheckedOut` - maybeCurrentTag <- hush <$> Git.tagCheckedOut Nothing + maybeCurrentTag <- hush <$> Git.tagCheckedOut rootPath case maybeCurrentTag of Just currentTag -> when (currentTag /= expectedTag) $ addError $ toDoc @@ -291,7 +304,7 @@ publish _args = do Nothing -> -- Current commit does not refer to a git tag. -- We should see whether the expected tag was already defined - Git.listTags Nothing >>= case _ of + Git.listTags rootPath >>= case _ of Left err -> die $ toDoc [ toDoc "Cannot check whether publish config's `version` matches any existing git tags due to the below error:" @@ -338,7 +351,7 @@ publish _args = do Right { expectedVersion, publishingData: publishingData@{ resolutions } } -> do logInfo "Passed preliminary checks." -- This requires login credentials. - Git.pushTag Nothing expectedVersion >>= case _ of + Git.pushTag rootPath expectedVersion >>= case _ of Left err -> die $ toDoc [ err , toDoc "You can try to push the tag manually by running:" @@ -381,19 +394,20 @@ publish _args = do , git: env.git , dependencies , logOptions: env.logOptions + , rootPath: env.rootPath , workspace: env.workspace { selected = Just selected } , strictWarnings: Nothing , pedanticPackages: false } action -locationIsInGitRemotes :: ∀ a. Location -> Spago (PublishEnv a) { result :: Boolean, remotes :: Array Git.Remote } -locationIsInGitRemotes location = do - isGitRepo <- FS.exists ".git" +locationIsInGitRemotes :: ∀ a. RootPath -> Location -> Spago (PublishEnv a) { result :: Boolean, remotes :: Array Git.Remote } +locationIsInGitRemotes root location = do + isGitRepo <- FS.exists $ root ".git" if not isGitRepo then pure { result: false, remotes: [] } else - Git.getRemotes Nothing >>= case _ of + Git.getRemotes root >>= case _ of Left err -> die [ toDoc "Couldn't parse Git remotes: ", err ] Right remotes -> do @@ -405,7 +419,8 @@ locationIsInGitRemotes location = do inferLocationAndWriteToConfig :: ∀ a. WorkspacePackage -> Spago (PublishEnv a) Boolean inferLocationAndWriteToConfig selectedPackage = do - Git.getRemotes Nothing >>= case _ of + { rootPath } <- ask + Git.getRemotes rootPath >>= case _ of Left err -> die [ toDoc "Couldn't parse Git remotes: ", err ] Right remotes -> @@ -435,13 +450,9 @@ inferLocationAndWriteToConfig selectedPackage = do , "Cannot publish this package because it is hosted at " <> origin.url ] - let - configPath - | Config.isRootPackage selectedPackage = "spago.yaml" - | otherwise = Path.concat [ selectedPackage.path, "spago.yaml" ] - - liftEffect $ Config.addPublishLocationToConfig selectedPackage.doc location - liftAff $ FS.writeYamlDocFile configPath selectedPackage.doc + doc <- justOrDieWith selectedPackage.doc Config.configDocMissingErrorMessage + liftEffect $ Config.addPublishLocationToConfig doc location + liftAff $ FS.writeYamlDocFile (selectedPackage.path "spago.yaml") doc pure true prettyPrintLocation :: Location -> String diff --git a/src/Spago/Command/Registry.purs b/src/Spago/Command/Registry.purs index 1427a536a..1c5bb8b91 100644 --- a/src/Spago/Command/Registry.purs +++ b/src/Spago/Command/Registry.purs @@ -26,6 +26,8 @@ import Spago.Db as Db import Spago.FS as FS import Spago.Git as Git import Spago.Json as Json +import Spago.Path as Path +import Spago.Paths as Paths import Spago.Registry (RegistryEnv) import Spago.Registry as Registry @@ -35,7 +37,7 @@ type RegistrySearchArgs = } -- TODO: I guess we could also search in (1) the tags and (2) the description -search :: RegistrySearchArgs -> Spago (RegistryEnv _) Unit +search :: ∀ r. RegistrySearchArgs -> Spago (RegistryEnv r) Unit search { package: searchString, json } = do logInfo $ "Searching for " <> show searchString <> " in the Registry package names..." metadataFiles <- Registry.listMetadataFiles @@ -83,7 +85,7 @@ type RegistryInfoArgs = , json :: Boolean } -info :: RegistryInfoArgs -> Spago (RegistryEnv _) Unit +info :: ∀ r. RegistryInfoArgs -> Spago (RegistryEnv r) Unit info { package, json } = do packageName <- case PackageName.parse package of Left err -> die [ toDoc "Could not parse package name, error:", indent (toDoc $ show err) ] @@ -104,7 +106,7 @@ type RegistryPackageSetsArgs = , json :: Boolean } -packageSets :: RegistryPackageSetsArgs -> Spago (RegistryEnv _) Unit +packageSets :: ∀ r. RegistryPackageSetsArgs -> Spago (RegistryEnv r) Unit packageSets { latest, json } = do availableSets <- Registry.listPackageSets @@ -137,12 +139,12 @@ packageSets { latest, json } = do ] } -type RegistryTransferArgs = { privateKeyPath :: String } +type RegistryTransferArgs = { privateKeyPath :: RawFilePath } -transfer :: RegistryTransferArgs -> Spago (FetchEnv _) Unit +transfer :: ∀ r. RegistryTransferArgs -> Spago (FetchEnv r) Unit transfer { privateKeyPath } = do logDebug $ "Running package transfer" - { workspace, offline } <- ask + { workspace, offline, rootPath } <- ask selected <- case workspace.selected of Just s -> pure s @@ -177,7 +179,7 @@ transfer { privateKeyPath } = do -- Check that the git tree is clean - since the transfer will obey the new content -- of the config file, it makes sense to have it commited before transferring - Git.getStatus Nothing >>= case _ of + Git.getStatus rootPath >>= case _ of Left _err -> do die $ toDoc [ toDoc "Could not verify whether the git tree is clean. Error was:" @@ -229,8 +231,11 @@ transfer { privateKeyPath } = do , payload: Operation.Transfer dataToSign } -getPrivateKeyForSigning :: forall e. FilePath -> Spago (LogEnv e) SSH.PrivateKey -getPrivateKeyForSigning privateKeyPath = do +getPrivateKeyForSigning :: ∀ e. RawFilePath -> Spago (LogEnv e) SSH.PrivateKey +getPrivateKeyForSigning privateKeyPath' = do + here <- Paths.cwd + let privateKeyPath = here privateKeyPath' + -- If all is well we read in the private key privateKey <- try (FS.readTextFile privateKeyPath) >>= case _ of Right key -> pure key @@ -250,7 +255,7 @@ getPrivateKeyForSigning privateKeyPath = do Left _ -> do decodeKeyInteractive { requiresPassword: true, attemptsLeft } true -> do - let prompt = "Enter passphrase for " <> privateKeyPath <> ": " + let prompt = "Enter passphrase for " <> Path.quote privateKeyPath <> ": " passphrase <- liftEffect $ runEffectFn1 questionPassword prompt case SSH.parsePrivateKey { key: privateKey, passphrase: Just passphrase } of @@ -266,7 +271,7 @@ getPrivateKeyForSigning privateKeyPath = do type RegistryUnpublishArgs = { version :: Version, reason :: Maybe String } -unpublish :: RegistryUnpublishArgs -> Spago (RegistryEnv _) Unit +unpublish :: ∀ r. RegistryUnpublishArgs -> Spago (RegistryEnv r) Unit unpublish _a = do -- { version, reason } = do logError "Unpublishing packages is not supported yet." die [ "Please contact the maintainers if you need to unpublish a package." ] diff --git a/src/Spago/Command/Repl.purs b/src/Spago/Command/Repl.purs index 8f0a56350..68df68ce5 100644 --- a/src/Spago/Command/Repl.purs +++ b/src/Spago/Command/Repl.purs @@ -20,6 +20,7 @@ type ReplEnv a = , supportPackage :: PackageMap , depsOnly :: Boolean , logOptions :: LogOptions + , rootPath :: RootPath , pursArgs :: Array String , selected :: NonEmptyArray WorkspacePackage | a @@ -27,17 +28,19 @@ type ReplEnv a = run :: ∀ a. Spago (ReplEnv a) Unit run = do - { dependencies, pursArgs, selected, depsOnly, supportPackage } <- ask + { rootPath, dependencies, pursArgs, selected, depsOnly, supportPackage } <- ask - unlessM (FS.exists pursReplFile.name) $ - FS.writeTextFile pursReplFile.name pursReplFile.content + let replFile = rootPath pursReplFile.name + unlessM (FS.exists replFile) $ + FS.writeTextFile replFile pursReplFile.content let allDependencies = Map.unionWith (\l _ -> l) supportPackage $ Fetch.toAllDependencies dependencies globs = Build.getBuildGlobs - { selected + { rootPath + , selected , dependencies: allDependencies , depsOnly , withTests: true } - void $ Purs.repl globs pursArgs + void $ Purs.repl rootPath globs pursArgs diff --git a/src/Spago/Command/Run.purs b/src/Spago/Command/Run.purs index 1d9bd713b..a2a4deded 100644 --- a/src/Spago/Command/Run.purs +++ b/src/Spago/Command/Run.purs @@ -13,19 +13,20 @@ import Data.Array as Array import Data.Array.NonEmpty as NEA import Data.Map as Map import Node.FS.Perms as Perms -import Node.Path as Path import Registry.Version as Version import Spago.Cmd as Cmd import Spago.Command.Build as Build import Spago.Command.Fetch as Fetch import Spago.Config (Workspace, WorkspacePackage) import Spago.FS as FS +import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs (Purs, ModuleGraph(..)) import Spago.Purs as Purs type RunEnv a = { logOptions :: LogOptions + , rootPath :: RootPath , workspace :: Workspace , runOptions :: RunOptions , selected :: WorkspacePackage @@ -38,16 +39,16 @@ type RunEnv a = type RunOptions = { execArgs :: Array String , moduleName :: String - , executeDir :: FilePath + , executeDir :: GlobalPath , successMessage :: Maybe String , failureMessage :: String } -type Node = { cmd :: String, version :: Version } +type Node = { cmd :: GlobalPath, version :: Version } nodeVersion :: forall a. Spago (LogEnv a) Version nodeVersion = - Cmd.exec "node" [ "--version" ] Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } >>= case _ of + Cmd.exec (Path.global "node") [ "--version" ] Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } >>= case _ of Left r -> do logDebug $ Cmd.printExecResult r die [ "Failed to find node. Have you installed it, and is it in your PATH?" ] @@ -62,30 +63,30 @@ nodeVersion = getNode :: forall a. Spago (LogEnv a) Node getNode = do version <- nodeVersion - pure { cmd: "node", version } + pure { cmd: Path.global "node", version } run :: forall a. Spago (RunEnv a) Unit run = do - { workspace, node, runOptions: opts, dependencies, selected } <- ask + { workspace, node, runOptions: opts, dependencies, selected, rootPath } <- ask let execOptions = Cmd.defaultExecOptions { pipeStdin = Cmd.StdinPipeParent } case workspace.backend of Nothing -> do logDebug "Running with backend: nodejs" - let runDir = Path.concat [ Paths.localCachePath, "run" ] + let runDir = rootPath Paths.localCachePath "run" FS.mkdirp runDir - absOutput <- liftEffect $ Path.resolve [] $ fromMaybe "output" workspace.buildOptions.output + absOutput <- liftEffect $ Path.toAbsolute $ fromMaybe (rootPath "output") workspace.buildOptions.output let - runJsPath = Path.concat [ runDir, "run.js" ] - packageJsonPath = Path.concat [ runDir, "package.json" ] + runJsPath = runDir "run.js" + packageJsonPath = runDir "package.json" packageJsonContents = "{\"type\":\"module\" }" - nodeArgs = [ runJsPath ] <> opts.execArgs + nodeArgs = [ Path.toRaw runJsPath ] <> opts.execArgs nodeContents = Array.fold [ "import { main } from 'file://" - , withForwardSlashes absOutput + , Path.toRaw (withForwardSlashes absOutput) , "/" , opts.moduleName , "/" @@ -97,13 +98,14 @@ run = do -- We check that the module we're about to run is included in the build and spit out a nice error if it isn't (see #383) let globs = Build.getBuildGlobs - { dependencies: Fetch.toAllDependencies dependencies + { rootPath + , dependencies: Fetch.toAllDependencies dependencies , depsOnly: false -- Here we include tests as well, because we use this code for `spago run` and `spago test` , withTests: true , selected: NEA.singleton selected } - Purs.graph globs [] >>= case _ of + Purs.graph rootPath globs [] >>= case _ of Left err -> logWarn $ "Could not decode the output of `purs graph`, error: " <> CJ.DecodeError.print err Right (ModuleGraph graph) -> do when (isNothing $ Map.lookup opts.moduleName graph) do @@ -126,7 +128,7 @@ run = do Just backend -> do let args = [ "--run", opts.moduleName <> ".main" ] <> opts.execArgs logDebug $ "Running command `" <> backend.cmd <> " " <> show args <> "`" - Cmd.exec backend.cmd args execOptions >>= case _ of + Cmd.exec (Path.global backend.cmd) args execOptions >>= case _ of Right _ -> case opts.successMessage of Just m -> logSuccess m Nothing -> pure unit diff --git a/src/Spago/Command/Sources.purs b/src/Spago/Command/Sources.purs index 8ad3474f0..3324fd1a3 100644 --- a/src/Spago/Command/Sources.purs +++ b/src/Spago/Command/Sources.purs @@ -10,12 +10,13 @@ import Spago.Command.Fetch (FetchEnv) import Spago.Command.Fetch as Fetch import Spago.Config (Package(..), WithTestGlobs(..)) import Spago.Config as Config +import Spago.Path as Path type SourcesOpts = { json :: Boolean } run :: forall a. SourcesOpts -> Spago (FetchEnv a) Unit run { json } = do - { workspace } <- ask + { workspace, rootPath } <- ask -- lookup the dependencies in the package set, so we get their version numbers let selectedPackages = case workspace.selected of @@ -30,9 +31,9 @@ run { json } = do let globs = Array.foldMap - (\(Tuple packageName package) -> Config.sourceGlob WithTestGlobs packageName package) + (\(Tuple packageName package) -> Config.sourceGlob rootPath WithTestGlobs packageName package) (Map.toUnfoldable transitivePackages :: Array (Tuple PackageName Package)) output case json of - true -> OutputJson (CJ.array CJ.string) globs - false -> OutputLines globs + true -> OutputJson (CJ.array $ Path.localPathCodec rootPath) globs + false -> OutputLines $ Path.localPart <$> globs diff --git a/src/Spago/Command/Test.purs b/src/Spago/Command/Test.purs index 11df0d0ff..a63538077 100644 --- a/src/Spago/Command/Test.purs +++ b/src/Spago/Command/Test.purs @@ -7,11 +7,12 @@ import Spago.Command.Fetch as Fetch import Spago.Command.Run (Node) import Spago.Command.Run as Run import Spago.Config (Workspace, WorkspacePackage) -import Spago.Paths as Paths +import Spago.Path as Path import Spago.Purs (Purs) type TestEnv a = { logOptions :: LogOptions + , rootPath :: RootPath , workspace :: Workspace , selectedPackages :: NonEmptyArray SelectedTest , dependencies :: Fetch.PackageTransitiveDeps @@ -28,7 +29,7 @@ type SelectedTest = run :: forall a. Spago (TestEnv a) Unit run = do - { workspace, logOptions, node, selectedPackages, dependencies, purs } <- ask + { workspace, logOptions, rootPath, node, selectedPackages, dependencies, purs } <- ask void $ for selectedPackages \{ execArgs, moduleName, selected } -> do let @@ -36,12 +37,12 @@ run = do runOptions = { successMessage: Just $ "Test succeeded for package \"" <> PackageName.print name <> "\"." , failureMessage: "Tests failed for package \"" <> PackageName.print name <> "\"." - , executeDir: Paths.cwd + , executeDir: Path.toGlobal rootPath , execArgs , moduleName } - runEnv = { logOptions, workspace, selected, node, runOptions, dependencies, purs } + runEnv = { logOptions, rootPath, workspace, selected, node, runOptions, dependencies, purs } logInfo $ "Running tests for package: " <> PackageName.print name runSpago runEnv Run.run diff --git a/src/Spago/Command/Uninstall.purs b/src/Spago/Command/Uninstall.purs index b80ddc47b..44dc2cb42 100644 --- a/src/Spago/Command/Uninstall.purs +++ b/src/Spago/Command/Uninstall.purs @@ -12,7 +12,6 @@ import Data.Newtype (wrap) import Data.Set as Set import Data.Set.NonEmpty as NonEmptySet import Data.String as String -import Node.Path as Path import Node.Process as Process import Registry.PackageName as PackageName import Spago.Command.Fetch (FetchEnv) @@ -21,21 +20,23 @@ import Spago.Config (BuildType(..), Dependencies, Package(..), PackageConfig) import Spago.Config as Config import Spago.Config as Core import Spago.FS as FS +import Spago.Path as Path type UninstallArgs = { dependenciesToRemove :: Set PackageName , testDeps :: Boolean } -run :: UninstallArgs -> Spago (FetchEnv _) Unit +run :: ∀ a. UninstallArgs -> Spago (FetchEnv a) Unit run args = do logDebug "Running `spago uninstall`" - { workspace } <- ask + { workspace, rootPath } <- ask { sourceOrTestString, deps, configPath, yamlDoc, name } <- case workspace.selected, workspace.rootPackage of - Just p, _ -> toContext (Path.concat [ p.path, "spago.yaml" ]) p.doc p.package - Nothing, Just rootPackage -> toContext "spago.yaml" workspace.doc rootPackage + Just p, _ -> toContext (p.path "spago.yaml") p.doc p.package + Nothing, Just rootPackage -> toContext (rootPath "spago.yaml") workspace.doc rootPackage Nothing, Nothing -> die "No package was selected. Please select a package." + let { warn, removed: removedSet } = separate deps warnAbout = NEA.fromFoldable warn @@ -89,7 +90,7 @@ run args = do $ Config.getWorkspacePackages workspace.packageSet Fetch.writeNewLockfile reason dependencies - toContext :: FilePath -> YamlDoc Core.Config -> PackageConfig -> Spago _ (_ _) + toContext :: LocalPath -> Maybe (YamlDoc Core.Config) -> PackageConfig -> Spago _ (_ _) toContext configPath yamlDoc pkgConfig = case args.testDeps of true -> case pkgConfig.test of Nothing -> do @@ -118,10 +119,11 @@ run args = do true -> acc { removed = Set.insert next acc.removed } false -> acc { warn = Set.insert next acc.warn } - modifyConfig :: FilePath -> YamlDoc Core.Config -> String -> NonEmptyArray PackageName -> Spago (FetchEnv _) Unit + modifyConfig :: LocalPath -> Maybe (YamlDoc Core.Config) -> String -> NonEmptyArray PackageName -> Spago (FetchEnv _) Unit modifyConfig configPath yamlDoc sourceOrTestString = \removedPackages -> do logInfo $ "Removing the following " <> sourceOrTestString <> " dependencies: " <> (String.joinWith ", " $ map PackageName.print $ Array.fromFoldable removedPackages) - logDebug $ "Editing config file at path: " <> configPath - liftEffect $ Config.removePackagesFromConfig yamlDoc args.testDeps $ NonEmptySet.fromFoldable1 removedPackages - liftAff $ FS.writeYamlDocFile configPath yamlDoc + logDebug $ "Editing config file at path: " <> Path.quote configPath + doc <- justOrDieWith yamlDoc Config.configDocMissingErrorMessage + liftEffect $ Config.removePackagesFromConfig doc args.testDeps $ NonEmptySet.fromFoldable1 removedPackages + liftAff $ FS.writeYamlDocFile configPath doc diff --git a/src/Spago/Command/Upgrade.purs b/src/Spago/Command/Upgrade.purs index d5000e148..19631654c 100644 --- a/src/Spago/Command/Upgrade.purs +++ b/src/Spago/Command/Upgrade.purs @@ -14,7 +14,9 @@ type UpgradeArgs = run :: ∀ a. UpgradeArgs -> Spago (FetchEnv a) Unit run args = do - { workspace } <- ask + { workspace, rootPath } <- ask + doc <- justOrDieWith workspace.doc Config.configDocMissingErrorMessage + case workspace.workspaceConfig.packageSet of Just (Core.SetFromRegistry { registry: currentPackageSet }) -> do latestPackageSet <- Registry.findPackageSet args.setVersion @@ -25,7 +27,7 @@ run args = do true -> logSuccess $ "Nothing to upgrade, you already have the " <> whichVersion <> " package set." false -> do logInfo $ "Upgrading the package set to the " <> whichVersion <> " version: " <> Version.print latestPackageSet - Config.setPackageSetVersionInConfig workspace.doc latestPackageSet + Config.setPackageSetVersionInConfig rootPath doc latestPackageSet logSuccess "Upgrade successful!" Just _ -> die "This command is not yet implemented for projects using a custom package set." Nothing -> die "This command is not yet implemented for projects using a solver. See https://github.com/purescript/spago/issues/1001" diff --git a/src/Spago/Config.purs b/src/Spago/Config.purs index 85f1ea83c..5c2885f7b 100644 --- a/src/Spago/Config.purs +++ b/src/Spago/Config.purs @@ -11,8 +11,9 @@ module Spago.Config , addPackagesToConfig , addPublishLocationToConfig , addRangesToConfig + , configDocMissingErrorMessage , fileSystemCharEscape - , getPackageLocation + , getLocalPackageLocation , getTopologicallySortedWorkspacePackages , getWorkspacePackages , isRootPackage @@ -48,27 +49,27 @@ import Data.Profunctor as Profunctor import Data.Set as Set import Data.Set.NonEmpty (NonEmptySet) import Data.Set.NonEmpty as NonEmptySet -import Data.String (CodePoint, Pattern(..)) +import Data.String (CodePoint) import Data.String as String import Dodo as Log import Effect.Aff as Aff import Effect.Uncurried (EffectFn2, EffectFn3, runEffectFn2, runEffectFn3) import Foreign.Object as Foreign import JSON (JSON) -import Node.Path as Path +import Node.Path as Node.Path import Registry.Internal.Codec as Internal.Codec import Registry.Owner (Owner(..)) import Registry.PackageName as PackageName import Registry.PackageSet as Registry.PackageSet import Registry.Range as Range import Registry.Version as Version -import Spago.Core.Config (publishLocationCodec) import Spago.Core.Config as Core import Spago.FS as FS import Spago.Glob as Glob import Spago.Json as Json import Spago.Lock (Lockfile, PackageSetInfo) import Spago.Lock as Lock +import Spago.Path as Path import Spago.Paths as Paths import Spago.Registry as Registry import Spago.Yaml as Yaml @@ -79,13 +80,13 @@ type Workspace = , compatibleCompiler :: Range , backend :: Maybe Core.BackendConfig , buildOptions :: WorkspaceBuildOptions - , doc :: YamlDoc Core.Config + , doc :: Maybe (YamlDoc Core.Config) , workspaceConfig :: Core.WorkspaceConfig , rootPackage :: Maybe Core.PackageConfig } type WorkspaceBuildOptions = - { output :: Maybe FilePath + { output :: Maybe LocalPath , censorLibWarnings :: Maybe Core.CensorBuildWarnings , statVerbosity :: Maybe Core.StatVerbosity } @@ -145,9 +146,9 @@ type PackageSet = } type WorkspacePackage = - { path :: FilePath + { path :: LocalPath , package :: Core.PackageConfig - , doc :: YamlDoc Core.Config + , doc :: Maybe (YamlDoc Core.Config) , hasTests :: Boolean } @@ -160,8 +161,8 @@ data Package type ReadWorkspaceConfigResult = { config :: ReadConfigResult , hasTests :: Boolean - , configPath :: FilePath - , packagePath :: FilePath + , configPath :: LocalPath + , packagePath :: LocalPath } type ReadWorkspaceOptions = @@ -171,30 +172,30 @@ type ReadWorkspaceOptions = } isRootPackage :: WorkspacePackage -> Boolean -isRootPackage p = p.path == rootPackagePath - -rootPackagePath :: String -rootPackagePath = "./" +isRootPackage p = Path.localPart p.path == "" -- | Reads all the configurations in the tree and builds up the Map of local -- | packages to be integrated in the package set -readWorkspace :: ∀ a. ReadWorkspaceOptions -> Spago (Registry.RegistryEnv a) Workspace +readWorkspace :: ∀ a. ReadWorkspaceOptions -> Spago (Registry.RegistryEnv (rootPath :: RootPath | a)) Workspace readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do + { rootPath } <- ask logInfo "Reading Spago workspace configuration..." let - doMigrateConfig :: FilePath -> _ -> Spago (Registry.RegistryEnv _) Unit + doMigrateConfig :: ∀ path. Path.IsPath path => path -> _ -> Spago (Registry.RegistryEnv _) Unit doMigrateConfig path config = do case migrateConfig, config.wasMigrated of true, true -> do - logInfo $ "Migrating your " <> path <> " to the latest version..." + logInfo $ "Migrating your " <> Path.quote path <> " to the latest version..." liftAff $ FS.writeYamlDocFile path config.doc - false, true -> logWarn $ "Your " <> path <> " is using an outdated format. Run Spago with the --migrate flag to update it to the latest version." + false, true -> logWarn $ "Your " <> Path.quote path <> " is using an outdated format. Run Spago with the --migrate flag to update it to the latest version." _, false -> pure unit + rootConfigPath = rootPath "spago.yaml" + -- First try to read the config in the root. It _has_ to contain a workspace -- configuration, or we fail early. - { workspace, package: maybePackage, workspaceDoc } <- readConfig "spago.yaml" >>= case _ of + { workspace, package: maybePackage, workspaceDoc } <- readConfig rootConfigPath >>= case _ of Left errLines -> die [ toDoc "Couldn't parse Spago config, error:" @@ -209,33 +210,38 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do ] Right config@{ yaml: { workspace: Just workspace, package }, doc } -> do logDebug "Read the root config" - doMigrateConfig "spago.yaml" config + doMigrateConfig (rootPath "spago.yaml") config pure { workspace, package, workspaceDoc: doc } logDebug "Gathering all the spago configs in the tree..." - otherConfigPaths <- liftAff $ Glob.gitignoringGlob - { cwd: Paths.cwd + otherConfigPaths <- liftAff $ Array.delete rootConfigPath <$> Glob.gitignoringGlob + { root: rootPath , includePatterns: [ "**/spago.yaml" ] , ignorePatterns: [ "**/node_modules/**", "**/.spago/**" ] } + unless (Array.null otherConfigPaths) do - logDebug $ [ toDoc "Found packages at these paths:", Log.indent $ Log.lines (map toDoc otherConfigPaths) ] + logDebug $ [ toDoc "Found packages at these paths:", Log.indent $ Log.lines (map (toDoc <<< Path.quote) otherConfigPaths) ] -- We read all of them in, and only read the package section, if any. let - readWorkspaceConfig :: FilePath -> Spago (Registry.RegistryEnv _) (Either Docc ReadWorkspaceConfigResult) + readWorkspaceConfig :: LocalPath -> Spago (Registry.RegistryEnv _) (Either Docc ReadWorkspaceConfigResult) readWorkspaceConfig path = do maybeConfig <- readConfig path -- We try to figure out if this package has tests - look for test sources - hasTests <- FS.exists (Path.concat [ Path.dirname path, "test" ]) + hasTests <- FS.exists (Path.dirname path "test") pure $ case maybeConfig of Left eLines -> Left $ toDoc - [ toDoc $ "Could not read config at path " <> path + [ toDoc $ "Could not read config at path " <> Path.quote path , toDoc "Error was: " , indent $ toDoc eLines ] - Right config -> do - Right { config, hasTests, configPath: path, packagePath: Path.dirname path } + Right config -> Right + { config + , hasTests + , configPath: path + , packagePath: Path.dirname path `Path.relativeTo` rootPath + } { right: otherPackages, left: failedPackages } <- partitionMap identity <$> traverse readWorkspaceConfig otherConfigPaths unless (Array.null failedPackages) do @@ -245,32 +251,35 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do -- For reasoning, see https://github.com/purescript/spago/issues/951 let configPathsWithWorkspaces = otherPackages # Array.mapMaybe \readResult -> readResult.packagePath <$ readResult.config.yaml.workspace unless (Array.null configPathsWithWorkspaces) do - logDebug $ "Found these paths with workspaces: " <> show configPathsWithWorkspaces + logDebug $ "Found these paths with workspaces: " <> String.joinWith ", " (Path.quote <$> configPathsWithWorkspaces) { right: configsNoWorkspaces, left: prunedConfigs } <- let fn { left, right } readResult@{ configPath, packagePath, hasTests, config } = do - if Array.any (\p -> isJust $ String.stripPrefix (Pattern p) packagePath) configPathsWithWorkspaces then + if Array.any (_ `Path.isPrefixOf` packagePath) configPathsWithWorkspaces then pure { right, left: Array.cons packagePath left } else case readResult.config.yaml.package of - Nothing -> pure { right, left: Array.cons packagePath left } + Nothing -> + pure { right, left: Array.cons packagePath left } Just package -> do -- Note: we migrate configs only at this point - this is because we read a whole lot of them but we are -- supposed to ignore any subtrees that contain a different workspace, and those we don't want to migrate doMigrateConfig configPath config -- We store the path of the package, so we can treat it basically as a LocalPackage - pure { left, right: Array.cons (Tuple package.name { package, hasTests, path: packagePath, doc: config.doc }) right } + pure { left, right: Array.cons (Tuple package.name { package, hasTests, path: packagePath, doc: Just config.doc }) right } in Array.foldM fn { right: [], left: [] } otherPackages unless (Array.null prunedConfigs) do - logDebug $ [ "Excluding configs that use a different workspace (directly or implicitly via parent directory's config):" ] <> Array.sort prunedConfigs + logDebug + $ [ "Excluding configs that use a different workspace (directly or implicitly via parent directory's config):" ] + <> Array.sort (Path.quote <$> prunedConfigs) rootPackage <- case maybePackage of Nothing -> pure [] Just rootPackage -> do - rootPackage' <- rootPackageToWorkspacePackage { rootPackage, workspaceDoc } + rootPackage' <- rootPackageToWorkspacePackage rootPath { rootPackage, workspaceDoc } pure [ Tuple rootPackage.name rootPackage' ] let workspacePackages = Map.fromFoldable $ configsNoWorkspaces <> rootPackage @@ -281,7 +290,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do Nothing -> die "No valid packages found in the current project, halting." -- If there's only one package and it's not in the root we still select that Just { head: (Tuple packageName package), tail: [] } -> do - logDebug $ "Selecting package " <> PackageName.print packageName <> " from " <> package.path + logDebug $ "Selecting package " <> PackageName.print packageName <> " from " <> Path.quote package.path pure (Just package) -- If no package has been selected and we have many packages, then we build all of them but select none _ -> pure Nothing @@ -299,9 +308,10 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do pure (Just p) logDebug "Parsing the lockfile..." - maybeLockfileContents <- FS.exists "spago.lock" >>= case _ of + let lockFilePath = rootPath "spago.lock" + maybeLockfileContents <- FS.exists lockFilePath >>= case _ of false -> pure (Left "No lockfile found") - true -> liftAff (FS.readJsonFile Lock.lockfileCodec "spago.lock") >>= case _ of + true -> liftAff (FS.readJsonFile Lock.lockfileCodec lockFilePath) >>= case _ of Left error -> do logWarn [ "Your project contains a spago.lock file, but it cannot be decoded. Spago will generate a new one." @@ -351,7 +361,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do Left reason, Just address@(Core.SetFromPath { path }) -> do logDebug reason logDebug $ "Reading the package set from local path: " <> path - liftAff (FS.readJsonFile remotePackageSetCodec path) >>= case _ of + liftAff (FS.readJsonFile remotePackageSetCodec (rootPath path)) >>= case _ of Left err -> die $ "Couldn't read the package set: " <> err Right (RemotePackageSet localPackageSet) -> do logInfo "Read the package set from local path" @@ -432,7 +442,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do case maybeSelected of Just selected -> do logSuccess $ "Selecting package to build: " <> PackageName.print selected.package.name - logDebug $ "Package path: " <> selected.path + logDebug $ "Package path: " <> Path.quote selected.path Nothing -> do logSuccess [ toDoc $ "Selecting " <> show (Map.size workspacePackages) <> " packages to build:" @@ -442,7 +452,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do let buildOptions :: WorkspaceBuildOptions buildOptions = - { output: _.output =<< workspace.buildOpts + { output: workspace.buildOpts >>= _.output <#> \o -> withForwardSlashes $ rootPath o , censorLibWarnings: _.censorLibraryWarnings =<< workspace.buildOpts , statVerbosity: _.statVerbosity =<< workspace.buildOpts } @@ -453,7 +463,7 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do , compatibleCompiler: fromMaybe Core.widestRange $ map _.compiler packageSetInfo , backend: workspace.backend , buildOptions - , doc: workspaceDoc + , doc: Just workspaceDoc , workspaceConfig: workspace , rootPackage: maybePackage } @@ -461,15 +471,18 @@ readWorkspace { maybeSelectedPackage, pureBuild, migrateConfig } = do rootPackageToWorkspacePackage :: forall m . MonadEffect m - => { rootPackage :: Core.PackageConfig, workspaceDoc :: YamlDoc Core.Config } + => RootPath + -> { rootPackage :: Core.PackageConfig, workspaceDoc :: YamlDoc Core.Config } -> m WorkspacePackage -rootPackageToWorkspacePackage { rootPackage, workspaceDoc } = do - hasTests <- liftEffect $ FS.exists "test" - pure { path: rootPackagePath, doc: workspaceDoc, package: rootPackage, hasTests } +rootPackageToWorkspacePackage rootPath { rootPackage, workspaceDoc } = do + hasTests <- liftEffect $ FS.exists (rootPath "test") + pure { path: rootPath "", doc: Just workspaceDoc, package: rootPackage, hasTests } workspacePackageToLockfilePackage :: WorkspacePackage -> Tuple PackageName Lock.WorkspaceLockPackage workspacePackageToLockfilePackage { path, package } = Tuple package.name - { path: withForwardSlashes path + { path: case Path.localPart (withForwardSlashes path) of + "" -> "./" + p -> p , core: { dependencies: package.dependencies, build_plan: mempty } , test: { dependencies: foldMap _.dependencies package.test, build_plan: mempty } } @@ -514,11 +527,11 @@ shouldComputeNewLockfile { workspace, workspacePackages } workspaceLock = Just (Core.SetFromPath _) -> true _ -> false -getPackageLocation :: PackageName -> Package -> FilePath -getPackageLocation name = Paths.mkRelative <<< case _ of - RegistryVersion v -> Path.concat [ Paths.localCachePackagesPath, PackageName.print name <> "-" <> Version.print v ] - GitPackage p -> Path.concat [ Paths.localCachePackagesPath, PackageName.print name, fileSystemCharEscape p.ref ] - LocalPackage p -> p.path +getLocalPackageLocation :: RootPath -> PackageName -> Package -> LocalPath +getLocalPackageLocation root name = case _ of + RegistryVersion v -> root Paths.localCachePackagesPath (PackageName.print name <> "-" <> Version.print v) + GitPackage p -> root Paths.localCachePackagesPath PackageName.print name fileSystemCharEscape p.ref + LocalPackage p -> root p.path WorkspacePackage { path } -> path -- This function must be injective and must always produce valid directory @@ -547,8 +560,8 @@ data WithTestGlobs | NoTestGlobs | OnlyTestGlobs -sourceGlob :: WithTestGlobs -> PackageName -> Package -> Array String -sourceGlob withTestGlobs name package = map (\p -> Path.concat [ getPackageLocation name package, p ]) +sourceGlob :: RootPath -> WithTestGlobs -> PackageName -> Package -> Array LocalPath +sourceGlob root withTestGlobs name package = map (\p -> getLocalPackageLocation root name package p) case package of WorkspacePackage { hasTests } -> case hasTests, withTestGlobs of @@ -557,7 +570,7 @@ sourceGlob withTestGlobs name package = map (\p -> Path.concat [ getPackageLocat true, OnlyTestGlobs -> [ testGlob ] true, NoTestGlobs -> [ srcGlob ] true, WithTestGlobs -> [ srcGlob, testGlob ] - GitPackage { subdir: Just s } -> [ Path.concat [ s, srcGlob ] ] + GitPackage { subdir: Just s } -> [ Node.Path.concat [ s, srcGlob ] ] _ -> [ srcGlob ] srcGlob :: String @@ -595,32 +608,32 @@ getTopologicallySortedWorkspacePackages packageSet = do type ReadConfigResult = { doc :: YamlDoc Core.Config, yaml :: Core.Config, wasMigrated :: Boolean } -readConfig :: forall a. FilePath -> Spago (LogEnv a) (Either (Array String) ReadConfigResult) +readConfig :: ∀ a path. Path.IsPath path => path -> Spago (SpagoBaseEnv a) (Either (Array String) ReadConfigResult) readConfig path = do - logDebug $ "Reading config from " <> path + logDebug $ "Reading config from " <> Path.quote path try (FS.readTextFile path) >>= case _ of Left err -> do - logDebug $ "Could not read file " <> path <> ", error: " <> Aff.message err - let replaceExt = map (_ <> ".yml") <<< String.stripSuffix (String.Pattern ".yaml") - yml <- map join $ for (replaceExt path) \yml -> do + logDebug $ "Could not read file " <> Path.quote path <> ", error: " <> Aff.message err + let altConfigName = Path.replaceExtension (String.Pattern ".yaml") (String.Replacement ".yml") path + yml <- map join $ for altConfigName \yml -> do hasYml <- FS.exists yml pure $ if hasYml then Just yml else Nothing - pure $ Left $ case path, yml of + pure $ Left $ case Path.basename path, yml of "spago.yaml", Nothing -> - [ "Did not find `" <> path <> "`. Run `spago init` to initialize a new project." ] + [ "Did not find " <> Path.quote path <> ". Run `spago init` to initialize a new project." ] "spago.yaml", Just y -> - [ "Did not find `" <> path <> "`. Spago's configuration files must end with `.yaml`, not `.yml`." - , "Try renaming `" <> y <> "` to `" <> path <> "` or run `spago init` to initialize a new project." + [ "Did not find " <> Path.quote path <> ". Spago's configuration files must end with `.yaml`, not `.yml`." + , "Try renaming " <> Path.quote y <> " to " <> Path.quote path <> " or run `spago init` to initialize a new project." ] _, Nothing -> - [ "Did not find `" <> path <> "`." ] + [ "Did not find " <> Path.quote path <> "." ] _, Just y -> - [ "Did not find `" <> path <> "`. Spago's configuration files must end with `.yaml`, not `.yml`." - , "Try renaming `" <> y <> "` to `" <> path <> "`." + [ "Did not find " <> Path.quote path <> ". Spago's configuration files must end with `.yaml`, not `.yml`." + , "Try renaming " <> Path.quote y <> " to " <> Path.quote path <> "." ] Right yamlString -> do case lmap (\err -> CJ.DecodeError.basic ("YAML: " <> err)) (Yaml.parser yamlString) of @@ -642,12 +655,12 @@ readConfig path = do (\yaml -> { doc, yaml, wasMigrated: isJust maybeMigratedDoc }) (CJ.decode Core.configCodec (Yaml.toJson $ fromMaybe doc maybeMigratedDoc)) -setPackageSetVersionInConfig :: forall m. MonadAff m => MonadEffect m => YamlDoc Core.Config -> Version -> m Unit -setPackageSetVersionInConfig doc version = do +setPackageSetVersionInConfig :: forall m. MonadAff m => MonadEffect m => RootPath -> YamlDoc Core.Config -> Version -> m Unit +setPackageSetVersionInConfig root doc version = do liftEffect $ runEffectFn2 setPackageSetVersionInConfigImpl doc (Version.print version) - liftAff $ FS.writeYamlDocFile "spago.yaml" doc + liftAff $ FS.writeYamlDocFile (root "spago.yaml") doc -addPackagesToConfig :: forall m. MonadAff m => FilePath -> YamlDoc Core.Config -> Boolean -> Array PackageName -> m Unit +addPackagesToConfig :: forall m path. Path.IsPath path => MonadAff m => path -> YamlDoc Core.Config -> Boolean -> Array PackageName -> m Unit addPackagesToConfig configPath doc isTest pkgs = do liftEffect $ runEffectFn3 addPackagesToConfigImpl doc isTest (map PackageName.print pkgs) liftAff $ FS.writeYamlDocFile configPath doc @@ -661,13 +674,19 @@ addRangesToConfig doc = runEffectFn2 addRangesToConfigImpl doc <<< map (\(Tuple name range) -> Tuple (PackageName.print name) (Core.printSpagoRange range)) <<< (Map.toUnfoldable :: Map _ _ -> Array _) +configDocMissingErrorMessage :: String +configDocMissingErrorMessage = Array.fold + [ "This operation requires a YAML config document, but none was found in the environment. " + , "This is an internal error. Please open an issue at https://github.com/purescript/spago/issues" + ] + addPublishLocationToConfig :: YamlDoc Core.Config -> Location -> Effect Unit addPublishLocationToConfig doc loc = - runEffectFn2 addPublishLocationToConfigImpl doc (CJ.encode publishLocationCodec loc) + runEffectFn2 addPublishLocationToConfigImpl doc (CJ.encode Core.publishLocationCodec loc) type OwnerJS = { public :: String, keytype :: String, id :: Nullable String } -addOwner :: forall m. MonadAff m => FilePath -> YamlDoc Core.Config -> Owner -> m Unit +addOwner :: forall m. MonadAff m => LocalPath -> YamlDoc Core.Config -> Owner -> m Unit addOwner configPath doc (Owner { id, keytype, public }) = do liftEffect $ runEffectFn2 addOwnerImpl doc { keytype, public, id: Nullable.toNullable id } liftAff $ FS.writeYamlDocFile configPath doc diff --git a/src/Spago/Db.purs b/src/Spago/Db.purs index 874df03ce..979ad6aaa 100644 --- a/src/Spago/Db.purs +++ b/src/Spago/Db.purs @@ -47,7 +47,7 @@ import Registry.Version as Version -- API type ConnectOptions = - { database :: FilePath + { database :: GlobalPath , logger :: String -> Effect Unit } @@ -136,7 +136,7 @@ insertMetadata db packageName metadata@(Metadata { unpublished }) = do -------------------------------------------------------------------------------- -- Table types and conversions --- Note: bump `Paths.databaseVersion` every time we change the database schema in a breaking way +-- Note: bump `Path.databaseVersion` every time we change the database schema in a breaking way data Db type PackageSetJs = @@ -232,7 +232,7 @@ packageSetCodec = CJ.named "PackageSet" $ CJ.Record.object -------------------------------------------------------------------------------- -- FFI -foreign import connectImpl :: EffectFn2 FilePath (EffectFn1 String Unit) Db +foreign import connectImpl :: EffectFn2 GlobalPath (EffectFn1 String Unit) Db foreign import insertPackageSetImpl :: EffectFn2 Db PackageSetJs Unit diff --git a/src/Spago/Esbuild.purs b/src/Spago/Esbuild.purs index e158ce85a..ca0a93244 100644 --- a/src/Spago/Esbuild.purs +++ b/src/Spago/Esbuild.purs @@ -5,7 +5,7 @@ import Spago.Prelude import Spago.Cmd as Cmd type Esbuild = - { cmd :: FilePath + { cmd :: GlobalPath , version :: String } diff --git a/src/Spago/Git.purs b/src/Spago/Git.purs index b40283e88..09406c27e 100644 --- a/src/Spago/Git.purs +++ b/src/Spago/Git.purs @@ -30,25 +30,27 @@ import Partial.Unsafe (unsafePartial) import Registry.Version as Version import Spago.Cmd as Cmd import Spago.FS as FS +import Spago.Path as Path -type Git = { cmd :: String, version :: String } +type Git = { cmd :: GlobalPath, version :: String } type GitEnv a = { git :: Git, logOptions :: LogOptions, offline :: OnlineStatus | a } type Remote = { name :: String, url :: String, owner :: String, repo :: String } -runGit_ :: forall a. Array String -> Maybe FilePath -> ExceptT String (Spago (GitEnv a)) Unit +runGit_ :: ∀ a. Array String -> Maybe GlobalPath -> ExceptT String (Spago (GitEnv a)) Unit runGit_ args cwd = void $ runGit args cwd -runGit :: forall a. Array String -> Maybe FilePath -> ExceptT String (Spago (GitEnv a)) String +runGit :: ∀ a. Array String -> Maybe GlobalPath -> ExceptT String (Spago (GitEnv a)) String runGit args cwd = ExceptT do { git } <- ask - result <- Cmd.exec git.cmd args (Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd }) + result <- Cmd.exec git.cmd args + (Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Path.toGlobal <$> cwd }) pure case result of Right r -> Right r.stdout Left r -> Left r.stderr -fetchRepo :: ∀ a b. { git :: String, ref :: String | a } -> FilePath -> Spago (GitEnv b) (Either (Array String) Unit) +fetchRepo :: ∀ a b path. Path.IsPath path => { git :: String, ref :: String | a } -> path -> Spago (GitEnv b) (Either (Array String) Unit) fetchRepo { git, ref } path = do repoExists <- FS.exists path { offline } <- ask @@ -61,16 +63,16 @@ fetchRepo { git, ref } path = do cloneOrFetchResult <- case repoExists of true -> do logDebug $ "Found " <> git <> " locally, pulling..." - Except.runExceptT $ runGit_ [ "fetch", "origin" ] (Just path) + Except.runExceptT $ runGit_ [ "fetch", "origin" ] (Just $ Path.toGlobal path) false -> do logInfo $ "Cloning " <> git -- For the reasoning on the filter options, see: -- https://github.com/purescript/spago/issues/701#issuecomment-1317192919 - Except.runExceptT $ runGit_ [ "clone", "--filter=tree:0", git, path ] Nothing + Except.runExceptT $ runGit_ [ "clone", "--filter=tree:0", git, Path.toRaw path ] Nothing result <- Except.runExceptT do Except.ExceptT $ pure cloneOrFetchResult logDebug $ "Checking out the requested ref for " <> git <> " : " <> ref - runGit_ [ "checkout", ref ] (Just path) + _ <- runGit [ "checkout", ref ] (Just $ Path.toGlobal path) -- if we are on a branch and not on a detached head, then we need to pull -- the following command will fail if on a detached head, and succeed if on a branch Except.mapExceptT @@ -78,9 +80,9 @@ fetchRepo { git, ref } path = do Left _err -> pure (Right unit) Right _ -> do logDebug "Pulling the latest changes" - Except.runExceptT $ runGit_ [ "pull", "--rebase", "--autostash" ] (Just path) + Except.runExceptT $ runGit_ [ "pull", "--rebase", "--autostash" ] (Just $ Path.toGlobal path) ) - (runGit_ [ "symbolic-ref", "-q", "HEAD" ] (Just path)) + (runGit_ [ "symbolic-ref", "-q", "HEAD" ] (Just $ Path.toGlobal path)) case result of Left err -> pure $ Left @@ -91,30 +93,30 @@ fetchRepo { git, ref } path = do logDebug $ "Successfully fetched the repo '" <> git <> "' at ref '" <> ref <> "'" pure $ Right unit -checkout :: ∀ a. { repo :: String, ref :: String } -> Spago (GitEnv a) (Either String Unit) -checkout { repo, ref } = Except.runExceptT $ void $ runGit [ "checkout", ref ] (Just repo) +checkout :: ∀ a path. Path.IsPath path => { repo :: path, ref :: String } -> Spago (GitEnv a) (Either String Unit) +checkout { repo, ref } = Except.runExceptT $ void $ runGit [ "checkout", ref ] (Just $ Path.toGlobal repo) -fetch :: ∀ a. { repo :: String, remote :: String } -> Spago (GitEnv a) (Either String Unit) +fetch :: ∀ a path. Path.IsPath path => { repo :: path, remote :: String } -> Spago (GitEnv a) (Either String Unit) fetch { repo, remote } = do - remoteUrl <- runGit [ "remote", "get-url", remote ] (Just repo) # Except.runExceptT >>= rightOrDie + remoteUrl <- runGit [ "remote", "get-url", remote ] (Just $ Path.toGlobal repo) # Except.runExceptT >>= rightOrDie logInfo $ "Fetching from " <> remoteUrl - Except.runExceptT $ runGit_ [ "fetch", remote, "--tags" ] (Just repo) + Except.runExceptT $ runGit_ [ "fetch", remote, "--tags" ] (Just $ Path.toGlobal repo) -getRefType :: ∀ a. { repo :: String, ref :: String } -> Spago (GitEnv a) (Either String String) -getRefType { repo, ref } = Except.runExceptT $ runGit [ "cat-file", "-t", ref ] (Just repo) +getRefType :: ∀ a path. Path.IsPath path => { repo :: path, ref :: String } -> Spago (GitEnv a) (Either String String) +getRefType { repo, ref } = Except.runExceptT $ runGit [ "cat-file", "-t", ref ] (Just $ Path.toGlobal repo) -listTags :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc (Array String)) +listTags :: ∀ a path. Path.IsPath path => path -> Spago (GitEnv a) (Either Docc (Array String)) listTags cwd = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git } <- ask Cmd.exec git.cmd [ "tag" ] opts >>= case _ of Left r -> do pure $ Left $ toDoc [ "Could not run `git tag`. Error:", r.message ] Right r -> pure $ Right $ String.split (Pattern "\n") r.stdout -getStatus :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc String) +getStatus :: ∀ a path. Path.IsPath path => path -> Spago (GitEnv a) (Either Docc String) getStatus cwd = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git } <- ask Cmd.exec git.cmd [ "status", "--porcelain" ] opts >>= case _ of Left r -> do @@ -122,9 +124,9 @@ getStatus cwd = do pure $ Left $ toDoc r.stderr Right r -> pure $ Right r.stdout -getRef :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc String) +getRef :: ∀ a path. Path.IsPath path => path -> Spago (GitEnv a) (Either Docc String) getRef cwd = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git } <- ask Cmd.exec git.cmd [ "rev-parse", "HEAD" ] opts >>= case _ of Left r -> pure $ Left $ toDoc @@ -133,9 +135,9 @@ getRef cwd = do ] Right r -> pure $ Right r.stdout -getRemotes :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc (Array Remote)) +getRemotes :: ∀ a @path. Path.IsPath path => path -> Spago (GitEnv a) (Either Docc (Array Remote)) getRemotes = \cwd -> do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git } <- ask Cmd.exec git.cmd [ "remote", "--verbose" ] opts <#> case _ of Left r -> Left $ toDoc @@ -149,17 +151,17 @@ getRemotes = \cwd -> do [] -> Left $ toDoc "Could not parse any remotes from the output of `git remote --verbose`." remotes -> Right $ Array.nub remotes -tagCheckedOut :: forall a. Maybe FilePath -> Spago (GitEnv a) (Either Docc String) +tagCheckedOut :: ∀ a path. Path.IsPath path => path -> Spago (GitEnv a) (Either Docc String) tagCheckedOut cwd = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git } <- ask Cmd.exec git.cmd [ "describe", "--tags", "--exact-match" ] opts >>= case _ of Left _ -> pure $ Left $ toDoc "The git ref currently checked out is not a tag." Right r -> pure $ Right r.stdout -pushTag :: forall a. Maybe FilePath -> Version -> Spago (GitEnv a) (Either Docc Unit) +pushTag :: ∀ a path. Path.IsPath path => path -> Version -> Spago (GitEnv a) (Either Docc Unit) pushTag cwd version = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = cwd } + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } { git, offline } <- ask case offline of Offline -> do @@ -177,12 +179,15 @@ pushTag cwd version = do getGit :: forall a. Spago (LogEnv a) Git getGit = do - Cmd.exec "git" [ "--version" ] Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } >>= case _ of - Right r -> pure { cmd: "git", version: r.stdout } + Cmd.exec cmd [ "--version" ] Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } >>= case _ of + Right r -> pure { cmd, version: r.stdout } Left r -> do logDebug $ Cmd.printExecResult r die [ "Failed to find git. Have you installed it, and is it in your PATH?" ] + where + cmd = Path.global "git" + parseRemote :: String -> Maybe Remote parseRemote = \line -> case Regex.split tabOrSpaceRegex line of diff --git a/src/Spago/Glob.purs b/src/Spago/Glob.purs index 0b46c16f1..eb56c09b0 100644 --- a/src/Spago/Glob.purs +++ b/src/Spago/Glob.purs @@ -17,20 +17,20 @@ import Data.String as String import Data.String as String.CodePoint import Effect.Aff as Aff import Effect.Ref as Ref -import Node.FS.Sync as SyncFS -import Node.Path as Path +import Spago.FS as FS +import Spago.Path as Path type Glob = { ignore :: Array String , include :: Array String } -foreign import testGlob :: Glob -> String -> Boolean +foreign import testGlob :: Glob -> RawFilePath -> Boolean splitGlob :: Glob -> Array Glob splitGlob { ignore, include } = (\a -> { ignore, include: [ a ] }) <$> include -type Entry = { name :: String, path :: String, dirent :: DirEnt } +type Entry = { name :: String, path :: GlobalPath, dirent :: DirEnt } type FsWalkOptions = { entryFilter :: Entry -> Effect Boolean, deepFilter :: Entry -> Effect Boolean } -- https://nodejs.org/api/fs.html#class-fsdirent @@ -58,17 +58,17 @@ foreign import fsWalkImpl -> (forall a b. b -> Either a b) -> (Either Error (Array Entry) -> Effect Unit) -> FsWalkOptions - -> String + -> RootPath -> Effect Unit -gitignoreFileToGlob :: FilePath -> String -> Glob -gitignoreFileToGlob base = +gitignoreFileToGlob :: LocalPath -> String -> Glob +gitignoreFileToGlob root = String.split (String.Pattern "\n") >>> map String.trim - >>> Array.filter (not <<< or [ String.null, isComment ]) + >>> Array.filter (not String.null && not isComment) >>> partitionMap ( \line -> do - let pattern lin = withForwardSlashes $ Path.concat [ base, gitignorePatternToGlobPattern lin ] + let pattern lin = Path.localPart $ withForwardSlashes $ root gitignorePatternToGlobPattern lin case String.stripPrefix (String.Pattern "!") line of Just negated -> Left $ pattern negated Nothing -> Right $ pattern line @@ -90,12 +90,12 @@ gitignoreFileToGlob base = | otherwise = "**/" <> pattern <> "/**" fsWalk :: GlobParams -> Aff (Array Entry) -fsWalk { cwd, ignorePatterns, includePatterns } = Aff.makeAff \cb -> do +fsWalk { root, ignorePatterns, includePatterns } = Aff.makeAff \cb -> do let includeMatcher = testGlob { ignore: [], include: includePatterns } -- Pattern for directories which can be outright ignored. -- This will be updated whenver a .gitignore is found. - ignoreMatcherRef :: Ref (String -> Boolean) <- Ref.new (testGlob { ignore: [], include: ignorePatterns }) + ignoreMatcherRef :: Ref (RawFilePath -> Boolean) <- Ref.new (testGlob { ignore: [], include: ignorePatterns }) -- If this Ref contains `true` because this Aff has been canceled, then deepFilter will always return false. canceled <- Ref.new false @@ -114,11 +114,10 @@ fsWalk { cwd, ignorePatterns, includePatterns } = Aff.makeAff \cb -> do updateIgnoreMatcherWithGitignore :: Entry -> Effect Unit updateIgnoreMatcherWithGitignore entry = do let - gitignorePath = entry.path -- directory of this .gitignore relative to the directory being globbed - base = Path.relative cwd (Path.dirname gitignorePath) + base = Path.dirname entry.path `Path.relativeTo` root - try (SyncFS.readTextFile UTF8 entry.path) >>= traverse_ \gitignore -> do + try (FS.readTextFileSync entry.path) >>= traverse_ \gitignore -> do let gitignored = testGlob <$> (splitGlob $ gitignoreFileToGlob base gitignore) @@ -185,18 +184,21 @@ fsWalk { cwd, ignorePatterns, includePatterns } = Aff.makeAff \cb -> do -- => patternBase is a prefix of relDirPath => the directory matches String.Pattern patternBase `isPrefix` relDirPath + relPath :: Entry -> String + relPath entry = Path.localPart $ withForwardSlashes $ entry.path `Path.relativeTo` root + -- Should `fsWalk` recurse into this directory? deepFilter :: Entry -> Effect Boolean deepFilter entry = fromMaybe false <$> runMaybeT do isCanceled <- lift $ Ref.read canceled guard $ not isCanceled - let relPath = withForwardSlashes $ Path.relative cwd entry.path shouldIgnore <- lift $ Ref.read ignoreMatcherRef - guard $ not $ shouldIgnore relPath + let path = relPath entry + guard $ not $ shouldIgnore path -- Only if the path of this directory matches any of the patterns base path, -- can anything in this directory possibly match the corresponding full pattern. - pure $ matchesAnyPatternBase relPath + pure $ matchesAnyPatternBase path -- Should `fsWalk` retain this entry for the result array? entryFilter :: Entry -> Effect Boolean @@ -204,24 +206,23 @@ fsWalk { cwd, ignorePatterns, includePatterns } = Aff.makeAff \cb -> do when (isFile entry.dirent && entry.name == ".gitignore") do updateIgnoreMatcherWithGitignore entry ignoreMatcher <- Ref.read ignoreMatcherRef - let path = withForwardSlashes $ Path.relative cwd entry.path + let path = relPath entry pure $ includeMatcher path && not (ignoreMatcher path) options = { entryFilter, deepFilter } - fsWalkImpl Left Right cb options cwd + fsWalkImpl Left Right cb options root pure $ Aff.Canceler \_ -> void $ liftEffect $ Ref.write true canceled -type GlobParams = { ignorePatterns :: Array String, includePatterns :: Array String, cwd :: FilePath } +type GlobParams = { ignorePatterns :: Array String, includePatterns :: Array String, root :: RootPath } -gitignoringGlob :: GlobParams -> Aff (Array String) -gitignoringGlob { cwd, ignorePatterns, includePatterns } = map (withForwardSlashes <<< Path.relative cwd <<< _.path) - <$> fsWalk - { cwd - , ignorePatterns: ignorePatterns - -- The ones in the base directory are always ignored - <> [ ".git", "spago.yaml" ] +gitignoringGlob :: GlobParams -> Aff (Array LocalPath) +gitignoringGlob { root, includePatterns, ignorePatterns } = do + entries <- fsWalk + { root + , ignorePatterns: ignorePatterns <> [ ".git", "spago.yaml" ] , includePatterns } + pure $ entries <#> \e -> e.path `Path.relativeTo` root diff --git a/src/Spago/Lock.purs b/src/Spago/Lock.purs index 3d0aa4013..6d61ddee7 100644 --- a/src/Spago/Lock.purs +++ b/src/Spago/Lock.purs @@ -57,7 +57,7 @@ type PackageSetInfo = type WorkspaceLockPackage = { core :: WorkspaceLockPackageEnv , test :: WorkspaceLockPackageEnv - , path :: FilePath + , path :: RawFilePath } type WorkspaceLockPackageEnv = @@ -119,7 +119,7 @@ registryLockType :: String registryLockType = "registry" type PathLock = - { path :: FilePath + { path :: RawFilePath , dependencies :: Array PackageName } @@ -136,7 +136,7 @@ pathLockCodec = Profunctor.dimap toRep fromRep $ CJ.named "PathLock" $ CJS.objec type GitLock = { url :: String , rev :: String - , subdir :: Maybe FilePath + , subdir :: Maybe RawFilePath , dependencies :: Array PackageName } diff --git a/src/Spago/Paths.js b/src/Spago/Paths.js index da219a5fa..3fc526971 100644 --- a/src/Spago/Paths.js +++ b/src/Spago/Paths.js @@ -1,3 +1,3 @@ import envPaths from 'env-paths'; -export const paths = envPaths('spago'); +export const paths_ = envPaths('spago'); diff --git a/src/Spago/Paths.purs b/src/Spago/Paths.purs index e2b3ccc8a..2a3853bd9 100644 --- a/src/Spago/Paths.purs +++ b/src/Spago/Paths.purs @@ -1,61 +1,74 @@ -module Spago.Paths where +module Spago.Paths + ( chdir + , cwd + , databasePath + , databaseVersion + , globalCachePath + , localCacheGitPath + , localCachePackagesPath + , localCachePath + , packageSetsPath + , paths + , registryIndexPath + , registryPath + ) where import Prelude -import Effect.Unsafe (unsafePerformEffect) -import Node.Path (FilePath) -import Node.Path as Path +import Effect.Class (class MonadEffect, liftEffect) +import Node.Path as Node.Path import Node.Process as Process - -type NodePaths = - { config :: FilePath - , data :: FilePath - , cache :: FilePath - , log :: FilePath - , temp :: FilePath +import Spago.Path (class IsPath, RawFilePath, GlobalPath, global, toRaw, ()) + +type NodePaths p = + { config :: p + , data :: p + , cache :: p + , log :: p + , temp :: p } -foreign import paths :: NodePaths - -cwd :: FilePath -cwd = unsafePerformEffect (Process.cwd) - -mkRelative :: FilePath -> FilePath -mkRelative = Path.relative cwd +foreign import paths_ :: NodePaths String -globalCachePath :: FilePath -globalCachePath = paths.cache +paths :: NodePaths GlobalPath +paths = + { config: global paths_.config + , data: global paths_.data + , cache: global paths_.cache + , log: global paths_.log + , temp: global paths_.temp + } -localCachePath :: FilePath -localCachePath = toLocalCachePath cwd +cwd :: ∀ m. MonadEffect m => m GlobalPath +cwd = global <$> liftEffect Process.cwd -localCachePackagesPath :: FilePath -localCachePackagesPath = toLocalCachePackagesPath cwd +chdir :: ∀ m path. MonadEffect m => IsPath path => path -> m Unit +chdir path = liftEffect $ Process.chdir (toRaw path) -localCacheGitPath :: FilePath -localCacheGitPath = toLocalCacheGitPath cwd +globalCachePath :: GlobalPath +globalCachePath = paths.cache -toLocalCachePath :: FilePath -> FilePath -toLocalCachePath rootDir = Path.concat [ rootDir, ".spago" ] +localCachePath :: RawFilePath +localCachePath = ".spago" -toLocalCachePackagesPath :: FilePath -> FilePath -toLocalCachePackagesPath rootDir = Path.concat [ toLocalCachePath rootDir, "p" ] +localCachePackagesPath :: RawFilePath +localCachePackagesPath = Node.Path.concat [ localCachePath, "p" ] -toLocalCacheGitPath :: FilePath -> FilePath -toLocalCacheGitPath rootDir = Path.concat [ toLocalCachePath rootDir, "g" ] +localCacheGitPath :: RawFilePath +localCacheGitPath = Node.Path.concat [ localCachePath, "g" ] -registryPath ∷ FilePath -registryPath = Path.concat [ globalCachePath, "registry" ] +registryPath ∷ GlobalPath +registryPath = globalCachePath "registry" -registryIndexPath ∷ FilePath -registryIndexPath = Path.concat [ globalCachePath, "registry-index" ] +registryIndexPath ∷ GlobalPath +registryIndexPath = globalCachePath "registry-index" -packageSetsPath :: FilePath -packageSetsPath = Path.concat [ registryPath, "package-sets" ] +packageSetsPath :: GlobalPath +packageSetsPath = registryPath "package-sets" -- | We should bump this number every time we change the database schema in a breaking way databaseVersion :: Int databaseVersion = 2 -databasePath :: FilePath -databasePath = Path.concat [ globalCachePath, "spago.v" <> show databaseVersion <> ".sqlite" ] +databasePath :: GlobalPath +databasePath = globalCachePath ("spago.v" <> show databaseVersion <> ".sqlite") diff --git a/src/Spago/Prelude.purs b/src/Spago/Prelude.purs index 78e601f33..04df4e7ed 100644 --- a/src/Spago/Prelude.purs +++ b/src/Spago/Prelude.purs @@ -1,9 +1,11 @@ module Spago.Prelude ( HexString(..) , OnlineStatus(..) + , SpagoBaseEnv , isPrefix , mkTemp , mkTemp' + , module Spago.Path , module Spago.Core.Prelude , parTraverseSpago , parallelise @@ -16,7 +18,6 @@ module Spago.Prelude , unsafeLog , unsafeStringify , withBackoff' - , withForwardSlashes ) where import Spago.Core.Prelude @@ -28,7 +29,6 @@ import Data.Foldable as Foldable import Data.Function.Uncurried (Fn3, runFn3) import Data.Int as Int import Data.Maybe as Maybe -import Data.String (Pattern(..), Replacement(..)) import Data.String as String import Data.String.Extra (levenshtein) import Data.Traversable (class Traversable) @@ -37,11 +37,12 @@ import Effect.Now as Now import JSON (JSON) import JSON as JSON import Node.Buffer as Buffer -import Node.Path as Path import Partial.Unsafe (unsafeCrashWith) import Registry.Sha256 as Registry.Sha256 import Registry.Sha256 as Sha256 import Registry.Version as Version +import Spago.Path (class IsPath, RawFilePath, GlobalPath, LocalPath, RootPath, (), withForwardSlashes) +import Spago.Path as Path import Spago.Paths as Paths import Unsafe.Coerce (unsafeCoerce) @@ -49,6 +50,12 @@ data OnlineStatus = Offline | Online | OnlineBypassCache derive instance Eq OnlineStatus +type SpagoBaseEnv a = + { rootPath :: Path.RootPath + , logOptions :: LogOptions + | a + } + unsafeFromRight :: forall e a. Either e a -> a unsafeFromRight v = Either.fromRight' (\_ -> unsafeCrashWith $ "Unexpected Left: " <> unsafeStringify v) v @@ -153,7 +160,7 @@ withBackoff { delay: Aff.Milliseconds timeout, action, shouldCancel, shouldRetry maybeResult <- runAction 0 action (Int.floor timeout) loop 1 maybeResult -mkTemp' :: forall m. MonadAff m => Maybe String -> m FilePath +mkTemp' :: forall m. MonadAff m => Maybe String -> m Path.GlobalPath mkTemp' maybeSuffix = liftAff do -- Get a random string (HexString random) <- liftEffect do @@ -161,15 +168,12 @@ mkTemp' maybeSuffix = liftAff do sha <- Sha256.hashString $ show now <> fromMaybe "" maybeSuffix shaToHex sha -- Return the dir, but don't make it - that's the responsibility of the client - let tempDirPath = Path.concat [ Paths.paths.temp, String.drop 50 random ] + let tempDirPath = Paths.paths.temp String.drop 50 random pure tempDirPath -mkTemp :: forall m. MonadAff m => m FilePath +mkTemp :: forall m. MonadAff m => m Path.GlobalPath mkTemp = mkTemp' Nothing -withForwardSlashes :: String -> String -withForwardSlashes = String.replaceAll (Pattern "\\") (Replacement "/") - isPrefix :: String.Pattern -> String -> Boolean isPrefix p = isJust <<< String.stripPrefix p diff --git a/src/Spago/Psa.purs b/src/Spago/Psa.purs index ef2b08b52..b51643c1d 100644 --- a/src/Spago/Psa.purs +++ b/src/Spago/Psa.purs @@ -1,6 +1,6 @@ -- A majority of this code was copied from -- - https://github.com/natefaubion/purescript-psa --- +-- -- To fullfil license requirements -- Copyright © Nathan Faubion -- https://opensource.org/license/mit/ @@ -27,24 +27,24 @@ import JSON as JSON import Node.ChildProcess.Types (Exit(..)) import Node.Encoding as Encoding import Node.FS.Aff as FSA -import Node.Path as Path import Spago.Cmd as Cmd import Spago.Config (Package(..), PackageMap, WorkspacePackage) import Spago.Config as Config import Spago.Core.Config (CensorBuildWarnings(..), WarningCensorTest(..)) import Spago.Core.Config as Core import Spago.Log (prepareToDie) +import Spago.Path as Path import Spago.Psa.Output (buildOutput) import Spago.Psa.Printer (printDefaultOutputToErr, printJsonOutputToOut) -import Spago.Psa.Types (ErrorCode, PathDecision, PsaArgs, PsaOutputOptions, PsaPathType(..), psaResultCodec) +import Spago.Psa.Types (ErrorCode, PathDecision, PsaArgs, PsaOutputOptions, PsaPathType(..), PsaEnv, psaResultCodec) import Spago.Purs as Purs defaultStatVerbosity :: Core.StatVerbosity defaultStatVerbosity = Core.CompactStats -psaCompile :: forall a. Set.Set FilePath -> Array String -> PsaArgs -> Spago (Purs.PursEnv a) Boolean -psaCompile globs pursArgs psaArgs = do - purs <- Purs.compile globs (Array.snoc pursArgs "--json-errors") +psaCompile :: ∀ a. RootPath -> Set.Set LocalPath -> Array String -> PsaArgs -> Spago (PsaEnv a) Boolean +psaCompile root globs pursArgs psaArgs = do + purs <- Purs.compile root globs (Array.snoc pursArgs "--json-errors") let resultStdout = Cmd.getStdout purs print' = if psaArgs.jsonErrors then printJsonOutputToOut else printDefaultOutputToErr psaArgs @@ -101,13 +101,14 @@ psaCompile globs pursArgs psaArgs = do either (const (pure Nothing)) pure result toPathDecisions - :: { allDependencies :: PackageMap + :: { rootPath :: RootPath + , allDependencies :: PackageMap , selectedPackages :: Array WorkspacePackage , psaCliFlags :: PsaOutputOptions , censorLibWarnings :: Maybe Core.CensorBuildWarnings } - -> Array (Effect (Array (String -> Maybe PathDecision))) -toPathDecisions { allDependencies, selectedPackages, psaCliFlags, censorLibWarnings } = + -> Array (Effect (Array (LocalPath -> Maybe PathDecision))) +toPathDecisions { rootPath, allDependencies, selectedPackages, psaCliFlags, censorLibWarnings } = projectDecisions <> dependencyDecisions where projectDecisions = selectedPackages <#> \selected -> toWorkspacePackagePathDecision { selected, psaCliFlags } @@ -122,7 +123,7 @@ toPathDecisions { allDependencies, selectedPackages, psaCliFlags, censorLibWarni pkgsInProject :: Set PackageName pkgsInProject = foldMap (\p -> Set.singleton p.package.name) selectedPackages - toDependencyDecision :: Tuple PackageName Package -> Effect (Array (String -> Maybe PathDecision)) + toDependencyDecision :: Tuple PackageName Package -> Effect (Array (LocalPath -> Maybe PathDecision)) toDependencyDecision dep = case snd dep of WorkspacePackage p -> toWorkspacePackagePathDecision @@ -130,10 +131,10 @@ toPathDecisions { allDependencies, selectedPackages, psaCliFlags, censorLibWarni , psaCliFlags } _ -> do - pkgLocation <- Path.resolve [] $ Tuple.uncurry Config.getPackageLocation dep + let pkgLocation = Tuple.uncurry (Config.getLocalPackageLocation rootPath) dep pure [ toPathDecision - { pathIsFromPackage: isJust <<< String.stripPrefix (String.Pattern pkgLocation) + { pathIsFromPackage: (pkgLocation `Path.isPrefixOf` _) , pathType: IsLib , strict: false , censorWarnings: censorLibWarnings @@ -144,20 +145,19 @@ toWorkspacePackagePathDecision :: { selected :: WorkspacePackage , psaCliFlags :: PsaOutputOptions } - -> Effect (Array (String -> Maybe PathDecision)) + -> Effect (Array (LocalPath -> Maybe PathDecision)) toWorkspacePackagePathDecision { selected: { path, package }, psaCliFlags } = do - pkgPath <- Path.resolve [] path - let srcPath = Path.concat [ pkgPath, "src" ] - let testPath = Path.concat [ pkgPath, "test" ] + let srcPath = path "src" + let testPath = path "test" pure [ toPathDecision - { pathIsFromPackage: isJust <<< String.stripPrefix (String.Pattern srcPath) + { pathIsFromPackage: (srcPath `Path.isPrefixOf` _) , pathType: IsSrc , strict: fromMaybe false $ psaCliFlags.strict <|> (package.build >>= _.strict) , censorWarnings: package.build >>= _.censorProjectWarnings } , toPathDecision - { pathIsFromPackage: isJust <<< String.stripPrefix (String.Pattern testPath) + { pathIsFromPackage: (testPath `Path.isPrefixOf` _) , pathType: IsSrc , strict: fromMaybe false $ psaCliFlags.strict <|> (package.test >>= _.strict) , censorWarnings: package.test >>= _.censorTestWarnings @@ -165,12 +165,12 @@ toWorkspacePackagePathDecision { selected: { path, package }, psaCliFlags } = do ] toPathDecision - :: { pathIsFromPackage :: String -> Boolean + :: { pathIsFromPackage :: LocalPath -> Boolean , pathType :: PsaPathType , strict :: Boolean , censorWarnings :: Maybe Config.CensorBuildWarnings } - -> String + -> LocalPath -> Maybe PathDecision toPathDecision options pathToFile = do Alternative.guard $ options.pathIsFromPackage pathToFile @@ -191,6 +191,6 @@ shouldPrintWarning = case _ of ByCode c -> \code _ -> c == code ByMessagePrefix prefix -> \_ msg -> isJust $ String.stripPrefix (String.Pattern $ String.trim prefix) (String.trim msg) -- We return `true` to print the warning. - -- If an element was found (i.e. `Just` is returned), then one of the tests succeeded, + -- If an element was found (i.e. `Just` is returned), then one of the tests succeeded, -- so we should not print the warning and return false here. \code msg -> isNothing $ NonEmptyArray.find (\f -> f code msg) tests diff --git a/src/Spago/Psa/Output.purs b/src/Spago/Psa/Output.purs index f87604cad..ea3f95479 100644 --- a/src/Spago/Psa/Output.purs +++ b/src/Spago/Psa/Output.purs @@ -14,19 +14,13 @@ module Spago.Psa.Output , trimPosition ) where -import Prelude +import Spago.Core.Prelude import Data.Array as Array -import Data.Foldable (foldl) -import Data.Maybe (Maybe(..), fromMaybe) import Data.String as Str -import Data.Tuple (Tuple(..)) import Foreign.Object as FO -import Node.Path as Path -import Spago.Core.Prelude (Spago) -import Spago.Paths as Paths -import Spago.Psa.Types (Filename, Lines, PathDecision, PathInfo, Position, PsaAnnotedError, PsaError, PsaArgs, PsaPath(..), PsaPathType(..), PsaResult, compareByLocation) -import Spago.Purs as Purs +import Spago.Path as Path +import Spago.Psa.Types (Lines, PathDecision, PathInfo, Position, PsaAnnotedError, PsaArgs, PsaEnv, PsaError, PsaPath(..), PsaPathType(..), PsaResult, Filename, compareByLocation) data ErrorTag = Error | Warning @@ -68,15 +62,16 @@ initialStats = -- | position information. buildOutput :: forall a - . (Filename -> Position -> Spago (Purs.PursEnv a) (Maybe Lines)) + . (Filename -> Position -> Spago (PsaEnv a) (Maybe Lines)) -> PsaArgs -> PsaResult - -> Spago (Purs.PursEnv a) Output + -> Spago (PsaEnv a) Output buildOutput loadLines options result = do + { rootPath } <- ask let result' = - { warnings: pathOf <$> result.warnings - , errors: pathOf <$> result.errors + { warnings: pathOf rootPath <$> result.warnings + , errors: pathOf rootPath <$> result.errors } initialState = { warnings: [] @@ -91,15 +86,14 @@ buildOutput loadLines options result = do } where - pathOf :: PsaError -> Tuple PathInfo PsaError - pathOf x = Tuple pathDecision x + pathOf :: Path.RootPath -> PsaError -> Tuple PathInfo PsaError + pathOf root x = Tuple pathDecision x where pathDecision = case x.filename of - Just short | short /= "" -> do + Just filename | filename /= "" -> do let - path - | Path.isAbsolute short = short - | otherwise = Path.concat [ Paths.cwd, short ] + path = root filename + short = Path.localPart path fromMaybe unknownPathInfo $ Array.findMap (\p -> map (toPathInfo short) $ p path) options.decisions _ -> unknownPathInfo @@ -120,7 +114,7 @@ buildOutput loadLines options result = do , shouldShowError: \_ _ -> true } - onError :: ErrorTag -> Output -> Tuple PathInfo PsaError -> Spago (Purs.PursEnv a) Output + onError :: ErrorTag -> Output -> Tuple PathInfo PsaError -> Spago (PsaEnv a) Output onError tag state (Tuple pathInfo error) = if shouldShowError then do source <- fromMaybe (pure Nothing) (loadLines <$> error.filename <*> error.position) @@ -134,7 +128,7 @@ buildOutput loadLines options result = do Error -> true Warning -> pathInfo.shouldShowError error.errorCode error.message - update :: Array PsaAnnotedError -> Spago (Purs.PursEnv a) Output + update :: Array PsaAnnotedError -> Spago (PsaEnv a) Output update log = pure $ onTag (_ { stats = stats, errors = state.errors <> log }) diff --git a/src/Spago/Psa/Types.purs b/src/Spago/Psa/Types.purs index 3ee0b0ae3..ddc04dddc 100644 --- a/src/Spago/Psa/Types.purs +++ b/src/Spago/Psa/Types.purs @@ -1,6 +1,6 @@ -- A majority of this code was copied from -- - https://github.com/natefaubion/purescript-psa-utils --- +-- -- To fullfil license requirements -- Copyright © Nathan Faubion -- https://opensource.org/license/mit/ @@ -18,6 +18,7 @@ module Spago.Psa.Types , PsaOutputOptions , PathInfo , Position + , PsaEnv , Suggestion , Lines , psaResultCodec @@ -33,6 +34,10 @@ import Data.Codec.JSON.Record as CJ.Record import Data.Maybe (Maybe(..)) import Data.Tuple (Tuple(..)) import Spago.Core.Config as Core +import Spago.Path (LocalPath, RootPath) +import Spago.Purs (PursEnv) + +type PsaEnv a = PursEnv (rootPath :: RootPath | a) type ErrorCode = String type ModuleName = String @@ -52,8 +57,8 @@ derive instance Eq PsaPath derive instance Ord PsaPath instance Show PsaPath where show = case _ of - Src s -> "(Src " <> s <> ")" - Lib s -> "(Lib " <> s <> ")" + Src s -> "(Src " <> show s <> ")" + Lib s -> "(Lib " <> show s <> ")" Unknown -> "Unknown" data PsaPathType @@ -82,7 +87,7 @@ type PsaArgs = { jsonErrors :: Boolean , color :: Boolean , statVerbosity :: Core.StatVerbosity - , decisions :: Array (String -> Maybe PathDecision) + , decisions :: Array (LocalPath -> Maybe PathDecision) } type PsaOutputOptions = diff --git a/src/Spago/Purs.purs b/src/Spago/Purs.purs index e28918fe6..29215af30 100644 --- a/src/Spago/Purs.purs +++ b/src/Spago/Purs.purs @@ -12,6 +12,7 @@ import Node.Library.Execa (ExecaResult) import Registry.Internal.Codec as Internal.Codec import Registry.Version as Version import Spago.Cmd as Cmd +import Spago.Path as Path type PursEnv a = { purs :: Purs @@ -20,16 +21,16 @@ type PursEnv a = } type Purs = - { cmd :: FilePath + { cmd :: GlobalPath , version :: Version } -getPurs :: forall a. Spago (LogEnv a) Purs +getPurs :: ∀ a. Spago (LogEnv a) Purs getPurs = Cmd.getExecutable "purs" >>= parseVersionOutput -- Drop the stuff after a space: dev builds look like this: 0.15.6 [development build; commit: 8da7e96005f717f03d6eee3c12b1f1416659a919] -- Drop the stuff after a hyphen: prerelease builds look like this: 0.15.6-2 -parseVersionOutput :: forall a. { cmd :: String, output :: String } -> Spago (LogEnv a) Purs +parseVersionOutput :: ∀ a. { cmd :: GlobalPath, output :: String } -> Spago (LogEnv a) Purs parseVersionOutput { cmd, output: stdout } = case parseLenientVersion (dropStuff "-" $ dropStuff " " stdout) of Left _err -> die $ "Failed to parse purs version. Was: " <> stdout -- Fail if Purs is lower than 0.15.4 @@ -41,10 +42,10 @@ parseVersionOutput { cmd, output: stdout } = case parseLenientVersion (dropStuff where dropStuff pattern = fromMaybe "" <<< Array.head <<< String.split (String.Pattern pattern) -compile :: forall a. Set FilePath -> Array String -> Spago (PursEnv a) (Either ExecaResult ExecaResult) -compile globs pursArgs = do +compile :: ∀ a. RootPath -> Set LocalPath -> Array String -> Spago (PursEnv a) (Either ExecaResult ExecaResult) +compile cwd globs pursArgs = do { purs } <- ask - let args = [ "compile" ] <> pursArgs <> Set.toUnfoldable globs + let args = [ "compile" ] <> pursArgs <> globsToArgs cwd globs logDebug [ "Running command:", "purs " <> String.joinWith " " args ] -- PureScript (as of v0.14.0) outputs the compiler errors/warnings to `stdout` -- and outputs "Compiling..." messages to `stderr` @@ -52,18 +53,24 @@ compile globs pursArgs = do -- However, we do not pipe `stdout` to the parent, so that we don't see the errors reported twice: -- once via `purs` and once via spago's pretty-printing of the same errors/warnings. Cmd.exec purs.cmd args $ Cmd.defaultExecOptions - { pipeStdout = false } + { pipeStdout = false + , cwd = Just $ Path.toGlobal cwd + } -repl :: forall a. Set FilePath -> Array String -> Spago (PursEnv a) (Either ExecaResult ExecaResult) -repl globs pursArgs = do +repl :: ∀ a. RootPath -> Set LocalPath -> Array String -> Spago (PursEnv a) (Either ExecaResult ExecaResult) +repl cwd globs pursArgs = do { purs } <- ask - let args = [ "repl" ] <> pursArgs <> Set.toUnfoldable globs + let args = [ "repl" ] <> pursArgs <> globsToArgs cwd globs Cmd.exec purs.cmd args $ Cmd.defaultExecOptions { pipeStdout = true , pipeStderr = true , pipeStdin = Cmd.StdinPipeParent + , cwd = Just $ Path.toGlobal cwd } +globsToArgs :: RootPath -> Set LocalPath -> Array String +globsToArgs cwd globs = Path.localPart <<< (_ `Path.relativeTo` cwd) <$> Set.toUnfoldable globs + data DocsFormat = Html | Markdown @@ -87,12 +94,13 @@ printDocsFormat = case _ of Ctags -> "ctags" Etags -> "etags" -docs :: forall a. Set FilePath -> DocsFormat -> Spago (PursEnv a) (Either ExecaResult ExecaResult) -docs globs format = do +docs :: ∀ a. RootPath -> Set LocalPath -> DocsFormat -> Spago (PursEnv a) (Either ExecaResult ExecaResult) +docs cwd globs format = do { purs } <- ask - let args = [ "docs", "--format", printDocsFormat format ] <> Set.toUnfoldable globs + let args = [ "docs", "--format", printDocsFormat format ] <> globsToArgs cwd globs Cmd.exec purs.cmd args $ Cmd.defaultExecOptions - { pipeStdout = true + { cwd = Just $ Path.toGlobal cwd + , pipeStdout = true , pipeStderr = true , pipeStdin = Cmd.StdinPipeParent } @@ -120,13 +128,17 @@ moduleGraphNodeCodec = CJ.named "ModuleGraphNode" $ CJ.Record.object , depends: CJ.array CJ.string } -graph :: forall a. Set FilePath -> Array String -> Spago (PursEnv a) (Either CJ.DecodeError ModuleGraph) -graph globs pursArgs = do +graph :: ∀ a. RootPath -> Set LocalPath -> Array String -> Spago (PursEnv a) (Either CJ.DecodeError ModuleGraph) +graph cwd globs pursArgs = do { purs } <- ask - let args = [ "graph" ] <> pursArgs <> Set.toUnfoldable globs + let args = [ "graph" ] <> pursArgs <> globsToArgs cwd globs logDebug [ "Running command:", "purs " <> String.joinWith " " args ] - let execOpts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false } - Cmd.exec purs.cmd args execOpts >>= case _ of + result <- Cmd.exec purs.cmd args $ Cmd.defaultExecOptions + { cwd = Just $ Path.toGlobal cwd + , pipeStdout = false + , pipeStderr = false + } + case result of Right r -> do logDebug "Called `purs graph`, decoding.." pure $ parseJson moduleGraphCodec r.stdout diff --git a/src/Spago/Purs/Graph.purs b/src/Spago/Purs/Graph.purs index 03f99b2dd..6b928b8df 100644 --- a/src/Spago/Purs/Graph.purs +++ b/src/Spago/Purs/Graph.purs @@ -33,7 +33,7 @@ import Spago.Config (Package(..), WithTestGlobs(..), WorkspacePackage) import Spago.Config as Config import Spago.Glob as Glob import Spago.Log as Log -import Spago.Paths as Paths +import Spago.Path as Path import Spago.Purs (ModuleGraph(..), ModuleGraphNode, ModuleName, Purs) import Spago.Purs as Purs import Unsafe.Coerce (unsafeCoerce) @@ -48,8 +48,8 @@ type PreGraphEnv a = | a } -runGraph :: forall a. Set FilePath -> Array String -> Spago (PreGraphEnv a) (Either String Purs.ModuleGraph) -runGraph globs pursArgs = map (lmap toErrorMessage) $ Purs.graph globs pursArgs +runGraph :: ∀ a. RootPath -> Set LocalPath -> Array String -> Spago (PreGraphEnv a) (Either String Purs.ModuleGraph) +runGraph root globs pursArgs = map (lmap toErrorMessage) $ Purs.graph root globs pursArgs where toErrorMessage = append "Could not decode the output of `purs graph`, error: " <<< CJ.DecodeError.print @@ -60,6 +60,7 @@ type PackageGraphEnv a = { selected :: NonEmptyArray WorkspacePackage , dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions + , rootPath :: RootPath | a } @@ -83,7 +84,7 @@ moduleGraphCodec = Internal.Codec.strMap "ModuleGraphWithPackage" Right identity getModuleGraphWithPackage :: forall a. Purs.ModuleGraph -> Spago (PackageGraphEnv a) ModuleGraphWithPackage getModuleGraphWithPackage (ModuleGraph graph) = do - { selected, dependencies } <- ask + { selected, dependencies, rootPath } <- ask -- First compile the globs for each package, we get out a set of the modules that a package contains -- and we can have a map from path to a PackageName @@ -100,13 +101,13 @@ getModuleGraphWithPackage (ModuleGraph graph) = do -- We should memoise them, so that when we get the graph for a monorepo we don't evaluate the globs again. -- Each call is a few milliseconds, but there are potentially hundreds of those, and it adds up. logDebug "Calling pathToPackage..." - pathToPackage :: Map FilePath PackageName <- map (Map.fromFoldable <<< Array.fold) + pathToPackage :: Map LocalPath PackageName <- map (Map.fromFoldable <<< Array.fold) $ for (Map.toUnfoldable allPackages) \(Tuple name package) -> do -- Basically partition the modules of the current package by in src and test packages let withTestGlobs = if (Set.member name (Map.keys testPackages)) then OnlyTestGlobs else NoTestGlobs logDebug $ "Getting globs for package " <> PackageName.print name - globMatches :: Array FilePath <- map Array.fold $ traverse compileGlob (Config.sourceGlob withTestGlobs name package) + globMatches :: Array LocalPath <- map Array.fold $ traverse compileGlob (Config.sourceGlob rootPath withTestGlobs name package) pure $ map (\p -> Tuple p name) globMatches logDebug "Got the pathToPackage map, calling packageGraph" @@ -116,18 +117,24 @@ getModuleGraphWithPackage (ModuleGraph graph) = do addPackageInfo pkgGraph (Tuple moduleName { path, depends }) = let -- Windows paths will need a conversion to forward slashes to be matched to globs - newPath = withForwardSlashes path + newPath = withForwardSlashes $ rootPath path newVal = do package <- Map.lookup newPath pathToPackage - pure { path: newPath, depends, package } + pure { path: Path.localPart newPath, depends, package } in maybe pkgGraph (\v -> Map.insert moduleName v pkgGraph) newVal packageGraph = foldl addPackageInfo Map.empty (Map.toUnfoldable graph :: Array _) pure packageGraph -compileGlob :: forall a. FilePath -> Spago a (Array FilePath) -compileGlob sourcePath = liftAff $ Glob.gitignoringGlob { cwd: Paths.cwd, includePatterns: [ withForwardSlashes sourcePath ], ignorePatterns: [] } +compileGlob :: ∀ a. LocalPath -> Spago { rootPath :: RootPath | a } (Array LocalPath) +compileGlob sourcePath = do + { rootPath } <- ask + liftAff $ Glob.gitignoringGlob + { root: rootPath + , includePatterns: [ Path.localPart $ withForwardSlashes sourcePath ] + , ignorePatterns: [] + } -------------------------------------------------------------------------------- -- Package graph @@ -181,6 +188,7 @@ type ImportsGraphEnv a = , workspacePackages :: NonEmptyArray WorkspacePackage , dependencies :: Fetch.PackageTransitiveDeps , logOptions :: LogOptions + , rootPath :: RootPath | a } @@ -231,12 +239,17 @@ checkImports graph = do | otherwise = OnlyTestGlobs -- Compile the globs for the project, we get the set of source files in the project - glob :: Set FilePath <- map Set.fromFoldable do - map Array.fold $ traverse compileGlob (Config.sourceGlob testGlobOption packageName (WorkspacePackage selected)) + { rootPath } <- ask + projectFiles :: Set String <- + Config.sourceGlob rootPath testGlobOption packageName (WorkspacePackage selected) + # traverse compileGlob + <#> Array.fold + <#> map (Path.localPart <<< withForwardSlashes) + <#> Set.fromFoldable let -- Filter this improved graph to only have the project modules - projectGraph = Map.filterWithKey (\_ { path } -> Set.member path glob) packageGraph + projectGraph = packageGraph # Map.filter \{ path } -> Set.member path projectFiles -- Go through all the modules in the project graph, figure out which packages each module depends on, -- accumulate all of that in a single place @@ -289,10 +302,10 @@ toImportErrors -> Array { errorMessage :: Docc, correction :: Docc } toImportErrors selected opts { unused, unusedTest, transitive, transitiveTest } = do Array.catMaybes - [ if opts.reportSrc && (not $ Set.isEmpty unused) then Just $ unusedError false selected unused else Nothing - , if opts.reportSrc && (not $ Map.isEmpty transitive) then Just $ transitiveError false selected transitive else Nothing - , if opts.reportTest && (not $ Set.isEmpty unusedTest) then Just $ unusedError true selected unusedTest else Nothing - , if opts.reportTest && (not $ Map.isEmpty transitiveTest) then Just $ transitiveError true selected transitiveTest else Nothing + [ if opts.reportSrc && (not Set.isEmpty unused) then Just $ unusedError false selected unused else Nothing + , if opts.reportSrc && (not Map.isEmpty transitive) then Just $ transitiveError false selected transitive else Nothing + , if opts.reportTest && (not Set.isEmpty unusedTest) then Just $ unusedError true selected unusedTest else Nothing + , if opts.reportTest && (not Map.isEmpty transitiveTest) then Just $ transitiveError true selected transitiveTest else Nothing ] formatImportErrors :: Array { errorMessage :: Docc, correction :: Docc } -> Docc diff --git a/src/Spago/Registry.purs b/src/Spago/Registry.purs index 437ae2888..c9ef7ed29 100644 --- a/src/Spago/Registry.purs +++ b/src/Spago/Registry.purs @@ -35,7 +35,6 @@ import Effect.Aff.AVar as AVar import Effect.Exception as Exception import Effect.Now as Now import Fetch as Http -import Node.Path as Path import Node.Process as Process import Registry.API.V1 as V1 import Registry.Constants as Registry.Constants @@ -55,6 +54,7 @@ import Spago.Git (GitEnv) import Spago.Git as Git import Spago.Json as Json import Spago.Log (LogVerbosity(..)) +import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs as Purs @@ -128,7 +128,7 @@ readPackageSet version = do { readPackageSet: fn } <- runSpago { logOptions, db, git, purs, offline } getRegistry runSpago { logOptions } (fn version) -getRegistryFns :: AVar RegistryFunctions -> AVar Unit -> Spago (PreRegistryEnv _) RegistryFunctions +getRegistryFns :: AVar RegistryFunctions -> AVar Unit -> Spago (PreRegistryEnv ()) RegistryFunctions getRegistryFns registryBox registryLock = do -- The Box AVar will be empty until the first time we fetch the Registry, then -- we can just use the value that is cached. @@ -159,7 +159,7 @@ getRegistryFns registryBox registryLock = do { getManifestFromIndex: getManifestFromIndexImpl db , getMetadata: getMetadataImpl db offline , getMetadataForPackages: getMetadataForPackagesImpl db offline - , listMetadataFiles: FS.ls (Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory ]) + , listMetadataFiles: FS.ls $ Paths.registryPath Registry.Constants.metadataDirectory , listPackageSets: listPackageSetsImpl , findPackageSet: findPackageSetImpl , readPackageSet: readPackageSetImpl @@ -191,7 +191,7 @@ getRegistryFns registryBox registryLock = do pure fetchingFreshRegistry -- | Update the database with the latest package sets - updatePackageSetsDb :: Db -> Spago (LogEnv _) Unit + updatePackageSetsDb :: ∀ a. Db -> Spago (LogEnv a) Unit updatePackageSetsDb db = do { logOptions } <- ask setsAvailable <- map Set.fromFoldable getAvailablePackageSets @@ -209,7 +209,7 @@ getRegistryFns registryBox registryLock = do liftEffect $ Db.insertPackageSetEntry db { packageName: name, packageVersion: version, packageSetVersion: set.version } -- | List all the package sets versions available in the Registry repo - getAvailablePackageSets :: Spago (LogEnv _) (Array Version) + getAvailablePackageSets :: ∀ a. Spago (LogEnv a) (Array Version) getAvailablePackageSets = do { success: setVersions, fail: parseFailures } <- map (partitionEithers <<< map parseSetVersion) $ FS.ls Paths.packageSetsPath @@ -225,7 +225,7 @@ getRegistryFns registryBox registryLock = do readPackageSetImpl :: Version -> Spago (LogEnv ()) PackageSet readPackageSetImpl setVersion = do logDebug "Reading the package set from the Registry repo..." - let packageSetPath = Path.concat [ Paths.packageSetsPath, Version.print setVersion <> ".json" ] + let packageSetPath = Paths.packageSetsPath (Version.print setVersion <> ".json") liftAff (FS.readJsonFile PackageSet.codec packageSetPath) >>= case _ of Left err -> die $ "Couldn't read the package set: " <> err Right registryPackageSet -> do @@ -264,8 +264,8 @@ getMetadataForPackagesImpl db onlineStatus names = do where metadataFromFile :: PackageName -> Spago (LogEnv ()) (Either String (Tuple PackageName Metadata)) metadataFromFile pkgName = do - let metadataFilePath = Path.concat [ Paths.registryPath, Registry.Constants.metadataDirectory, PackageName.print pkgName <> ".json" ] - logDebug $ "Reading metadata from file: " <> metadataFilePath + let metadataFilePath = Paths.registryPath Registry.Constants.metadataDirectory (PackageName.print pkgName <> ".json") + logDebug $ "Reading metadata from file: " <> Path.quote metadataFilePath liftAff (FS.readJsonFile Metadata.codec metadataFilePath) >>= case _ of Left e -> pure $ Left e Right m -> do @@ -282,7 +282,7 @@ getManifestFromIndexImpl db name version = do -- if we don't have it we need to read it from file -- (note that we have all the versions of a package in the same file) logDebug $ "Reading package from Index: " <> PackageName.print name - maybeManifests <- liftAff $ ManifestIndex.readEntryFile Paths.registryIndexPath name + maybeManifests <- liftAff $ ManifestIndex.readEntryFile (Path.toRaw Paths.registryIndexPath) name manifests <- map (map (\m@(Manifest m') -> Tuple m'.version m)) case maybeManifests of Right ms -> pure $ NonEmptyArray.toUnfoldable ms Left err -> do @@ -350,7 +350,7 @@ isVersionCompatible installedVersion minVersion = _, _ -> false -- | Check if we have fetched the registry recently enough, so we don't hit the net all the time -shouldFetchRegistryRepos :: forall a. Db -> Spago (LogEnv a) Boolean +shouldFetchRegistryRepos :: ∀ a. Db -> Spago (LogEnv a) Boolean shouldFetchRegistryRepos db = do now <- liftEffect $ Now.nowDateTime let registryKey = "registry" diff --git a/src/Spago/Tar.purs b/src/Spago/Tar.purs index 81ba18347..0b18cef9e 100644 --- a/src/Spago/Tar.purs +++ b/src/Spago/Tar.purs @@ -4,12 +4,12 @@ import Spago.Prelude import Data.Function.Uncurried (Fn4, runFn4) -foreign import extractImpl :: forall r. Fn4 (TarErr -> r) (Unit -> r) String String (Effect r) +foreign import extractImpl :: forall r. Fn4 (TarErr -> r) (Unit -> r) GlobalPath GlobalPath (Effect r) type TarErr = { recoverable :: Boolean - , file :: FilePath - , cwd :: FilePath + , file :: GlobalPath + , cwd :: GlobalPath , code :: String , tarCode :: String } @@ -21,4 +21,4 @@ type TarErr = extract :: ExtractArgs -> Effect (Either TarErr Unit) extract { cwd, filename } = runFn4 extractImpl Left Right cwd filename -type ExtractArgs = { cwd :: String, filename :: String } +type ExtractArgs = { cwd :: GlobalPath, filename :: GlobalPath } diff --git a/test-fixtures/build/migrate-config/migrating-output.txt b/test-fixtures/build/migrate-config/migrating-output.txt index d1f3676ba..a5f03da20 100644 --- a/test-fixtures/build/migrate-config/migrating-output.txt +++ b/test-fixtures/build/migrate-config/migrating-output.txt @@ -1,5 +1,5 @@ Reading Spago workspace configuration... -Migrating your spago.yaml to the latest version... +Migrating your "spago.yaml" to the latest version... ✓ Selecting package to build: migrate-config diff --git a/test-fixtures/build/migrate-config/unmigrated-warning.txt b/test-fixtures/build/migrate-config/unmigrated-warning.txt index 7586bd046..e9e7e0518 100644 --- a/test-fixtures/build/migrate-config/unmigrated-warning.txt +++ b/test-fixtures/build/migrate-config/unmigrated-warning.txt @@ -1,5 +1,5 @@ Reading Spago workspace configuration... -‼ Your spago.yaml is using an outdated format. Run Spago with the --migrate flag to update it to the latest version. +‼ Your "spago.yaml" is using an outdated format. Run Spago with the --migrate flag to update it to the latest version. ✓ Selecting package to build: migrate-config diff --git a/test-fixtures/bundle-refuse-overwrite-output.txt b/test-fixtures/bundle-refuse-overwrite-output.txt index 5ef5140e3..af3f6a80c 100644 --- a/test-fixtures/bundle-refuse-overwrite-output.txt +++ b/test-fixtures/bundle-refuse-overwrite-output.txt @@ -11,4 +11,4 @@ Errors 0 0 0 ✓ Build succeeded. -✘ Target file index.js was not previously generated by Spago. Use --force to overwrite anyway. +✘ Target file "index.js" was not previously generated by Spago. Use --force to overwrite anyway. diff --git a/test-fixtures/pedantic/pedantic-instructions-installation-result.txt b/test-fixtures/pedantic/pedantic-instructions-installation-result.txt index 346c35305..bcb810554 100644 --- a/test-fixtures/pedantic/pedantic-instructions-installation-result.txt +++ b/test-fixtures/pedantic/pedantic-instructions-installation-result.txt @@ -2,7 +2,7 @@ Reading Spago workspace configuration... ✓ Selecting package to build: follow-instructions -Adding 1 package to the config in spago.yaml +Adding 1 package to the config in "spago.yaml" Downloading dependencies... Lockfile is out of date (reason: installing new packages), generating it... Lockfile written to spago.lock. Please commit this file. diff --git a/test-fixtures/spago-yml-check-stderr.txt b/test-fixtures/spago-yml-check-stderr.txt index 23110a41c..ccd6509e4 100644 --- a/test-fixtures/spago-yml-check-stderr.txt +++ b/test-fixtures/spago-yml-check-stderr.txt @@ -3,8 +3,8 @@ Reading Spago workspace configuration... ✘ Couldn't parse Spago config, error: - Did not find `spago.yaml`. Spago's configuration files must end with `.yaml`, not `.yml`. - Try renaming `spago.yml` to `spago.yaml` or run `spago init` to initialize a new project. + Did not find "spago.yaml". Spago's configuration files must end with `.yaml`, not `.yml`. + Try renaming "spago.yml" to "spago.yaml" or run `spago init` to initialize a new project. The configuration file help can be found here https://github.com/purescript/spago#the-configuration-file diff --git a/test/Prelude.purs b/test/Prelude.purs index d43b8f8d8..9df8d5d59 100644 --- a/test/Prelude.purs +++ b/test/Prelude.purs @@ -14,8 +14,6 @@ import Effect.Class.Console (log) import Effect.Class.Console as Console import Node.FS.Aff as FS.Aff import Node.Library.Execa (ExecaResult) -import Node.Path (dirname) -import Node.Path as Path import Node.Platform as Platform import Node.Process as Process import Record (merge) @@ -28,39 +26,44 @@ import Spago.Command.Init as Init import Spago.Core.Config (Dependencies(..), Config) import Spago.Core.Config as Config import Spago.FS as FS +import Spago.Path (toRaw) +import Spago.Path as Path +import Spago.Paths as Paths import Spago.Prelude as X import Test.Spec.Assertions (fail) import Test.Spec.Assertions as Assert +type FixturePath = GlobalPath + type TestDirs = { spago :: Array String -> Aff (Either ExecResult ExecResult) , spago' :: StdinConfig -> Array String -> Aff (Either ExecResult ExecResult) - , fixture :: FilePath -> FilePath - , oldCwd :: FilePath - , testCwd :: FilePath + , fixture :: RawFilePath -> FixturePath + , oldCwd :: GlobalPath + , testCwd :: RootPath } withTempDir :: (TestDirs -> Aff Unit) -> Aff Unit withTempDir = Aff.bracket createTempDir cleanupTempDir where createTempDir = do - oldCwd <- liftEffect $ Process.cwd - temp <- mkTemp' $ Just "spago-test-" + oldCwd <- Paths.cwd + temp <- Path.mkRoot =<< mkTemp' (Just "spago-test-") FS.mkdirp temp - liftEffect $ Process.chdir temp + Paths.chdir temp isDebug <- liftEffect $ map isJust $ Process.lookupEnv "SPAGO_TEST_DEBUG" when isDebug do - log $ "Running test in " <> temp + log $ "Running test in " <> Path.quote temp let - fixturesPath = oldCwd <> Path.sep <> "test-fixtures" + fixturesPath = oldCwd "test-fixtures" - fixture path = Path.concat [ fixturesPath, path ] + fixture path = fixturesPath path spago' :: StdinConfig -> Array String -> Aff (Either ExecResult ExecResult) spago' stdin args = Cmd.exec - "node" - ([ Path.concat [ oldCwd, "bin", "index.dev.js" ] ] <> args) + (Path.global "node") + ([ Path.toRaw $ oldCwd "bin" "index.dev.js" ] <> args) $ Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = stdin } spago = spago' StdinNewPipe @@ -74,10 +77,10 @@ withTempDir = Aff.bracket createTempDir cleanupTempDir } cleanupTempDir { oldCwd } = do - liftEffect $ Process.chdir oldCwd + Paths.chdir oldCwd -rmRf :: ∀ m. MonadAff m => FilePath -> m Unit -rmRf dir = liftAff $ FS.Aff.rm' dir { force: true, recursive: true, maxRetries: 5, retryDelay: 1000 } +rmRf :: ∀ m path. MonadAff m => IsPath path => path -> m Unit +rmRf dir = liftAff $ FS.Aff.rm' (toRaw dir) { force: true, recursive: true, maxRetries: 5, retryDelay: 1000 } shouldEqual :: forall m t @@ -116,16 +119,16 @@ shouldEqualStr v1 v2 = , "" ] -checkFixture :: String -> String -> Aff Unit +checkFixture :: ∀ path. IsPath path => path -> FixturePath -> Aff Unit checkFixture filepath fixturePath = checkFixture' filepath fixturePath (shouldEqualStr `on` String.trim) -checkFixture' :: String -> String -> (String -> String -> Aff Unit) -> Aff Unit +checkFixture' :: ∀ path. IsPath path => path -> FixturePath -> (String -> String -> Aff Unit) -> Aff Unit checkFixture' filepath fixturePath assertEqual = do filecontent <- FS.readTextFile filepath overwriteSpecFile <- liftEffect $ map isJust $ Process.lookupEnv "SPAGO_TEST_ACCEPT" if overwriteSpecFile then do - Console.log $ "Overwriting fixture at path: " <> fixturePath - let parentDir = dirname fixturePath + Console.log $ "Overwriting fixture at path: " <> Path.quote fixturePath + let parentDir = Path.dirname fixturePath unlessM (FS.exists parentDir) $ FS.mkdirp parentDir FS.writeTextFile fixturePath (String.trim filecontent <> "\n") else do @@ -179,8 +182,8 @@ checkOutputsStr checkers = } checkOutputs - :: { stdoutFile :: Maybe FilePath - , stderrFile :: Maybe FilePath + :: { stdoutFile :: Maybe FixturePath + , stderrFile :: Maybe FixturePath , result :: (Either ExecResult ExecResult) -> Boolean } -> Either ExecResult ExecResult @@ -188,8 +191,8 @@ checkOutputs checkOutputs args = checkOutputs' $ args `merge` { sanitize: String.trim } checkOutputs' - :: { stdoutFile :: Maybe FilePath - , stderrFile :: Maybe FilePath + :: { stdoutFile :: Maybe FixturePath + , stderrFile :: Maybe FixturePath , result :: (Either ExecResult ExecResult) -> Boolean , sanitize :: String -> String } @@ -203,8 +206,8 @@ checkOutputs' checkers execResult = do let actual = checkers.sanitize actual' overwriteSpecFile <- liftEffect $ map isJust $ Process.lookupEnv "SPAGO_TEST_ACCEPT" if overwriteSpecFile then do - Console.log $ "Overwriting fixture at path: " <> fixtureFileExpected - let parentDir = dirname fixtureFileExpected + Console.log $ "Overwriting fixture at path: " <> Path.quote fixtureFileExpected + let parentDir = Path.dirname fixtureFileExpected unlessM (FS.exists parentDir) $ FS.mkdirp parentDir FS.writeTextFile fixtureFileExpected (actual <> "\n") else do @@ -220,25 +223,25 @@ checkOutputs' checkers execResult = do shouldBeSuccess :: Either ExecaResult ExecaResult -> Aff Unit shouldBeSuccess = checkOutputs { stdoutFile: Nothing, stderrFile: Nothing, result: isRight } -shouldBeSuccessOutput :: FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeSuccessOutput :: FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeSuccessOutput outFixture = checkOutputs { stdoutFile: Just outFixture, stderrFile: Nothing, result: isRight } -shouldBeSuccessErr :: FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeSuccessErr :: FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeSuccessErr errFixture = checkOutputs { stdoutFile: Nothing, stderrFile: Just errFixture, result: isRight } -shouldBeSuccessOutputWithErr :: FilePath -> FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeSuccessOutputWithErr :: FixturePath -> FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeSuccessOutputWithErr outFixture errFixture = checkOutputs { stdoutFile: Just outFixture, stderrFile: Just errFixture, result: isRight } shouldBeFailure :: Either ExecaResult ExecaResult -> Aff Unit shouldBeFailure = checkOutputs { stdoutFile: Nothing, stderrFile: Nothing, result: isLeft } -shouldBeFailureOutput :: FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeFailureOutput :: FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeFailureOutput outFixture = checkOutputs { stdoutFile: Just outFixture, stderrFile: Nothing, result: isLeft } -shouldBeFailureErr :: FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeFailureErr :: FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeFailureErr errFixture = checkOutputs { stdoutFile: Nothing, stderrFile: Just errFixture, result: isLeft } -shouldBeFailureOutputWithErr :: FilePath -> FilePath -> Either ExecaResult ExecaResult -> Aff Unit +shouldBeFailureOutputWithErr :: FixturePath -> FixturePath -> Either ExecaResult ExecaResult -> Aff Unit shouldBeFailureOutputWithErr outFixture errFixture = checkOutputs { stdoutFile: Just outFixture, stderrFile: Just errFixture, result: isLeft } mkPackageName :: String -> PackageName @@ -260,14 +263,14 @@ writePursFile { moduleName, rest } = modNameLine = "module " <> moduleName <> " where" editSpagoYaml :: (Config -> Config) -> Aff Unit -editSpagoYaml = editSpagoYaml' "spago.yaml" +editSpagoYaml = editSpagoYaml' $ Path.global "spago.yaml" -editSpagoYaml' :: FilePath -> (Config -> Config) -> Aff Unit +editSpagoYaml' :: ∀ path. IsPath path => path -> (Config -> Config) -> Aff Unit editSpagoYaml' configPath f = do content <- liftAff $ FS.readYamlDocFile Config.configCodec configPath case content of Left err -> - Assert.fail $ "Failed to decode spago.yaml file at path " <> configPath <> "\n" <> err + Assert.fail $ "Failed to decode spago.yaml file at path " <> Path.quote configPath <> "\n" <> err Right { yaml: config } -> liftAff $ FS.writeYamlFile Config.configCodec configPath $ f config diff --git a/test/Spago.purs b/test/Spago.purs index a452d7bf1..74b64c393 100644 --- a/test/Spago.purs +++ b/test/Spago.purs @@ -33,7 +33,7 @@ import Test.Spec.Runner.Node.Config as Config testConfig :: Config.TestRunConfig testConfig = Config.defaultConfig - { timeout = Just (Milliseconds 90_000.0) + { timeout = Just (Milliseconds 120_000.0) } main :: Effect Unit diff --git a/test/Spago/Build.purs b/test/Spago/Build.purs index 9d91e68c0..cc780fe87 100644 --- a/test/Spago/Build.purs +++ b/test/Spago/Build.purs @@ -4,8 +4,6 @@ import Test.Prelude import Data.Foldable (fold) import Data.String as String -import Node.FS.Aff as FSA -import Node.Path as Path import Node.Platform as Platform import Node.Process as Process import Spago.Command.Init as Init @@ -53,35 +51,36 @@ spec = Spec.around withTempDir do spago [ "init", "--name", "aaa" ] >>= shouldBeSuccess spago [ "build", "--purs-args", "--json-errors" ] >>= shouldBeFailureErr (fixture "json-errors-err.txt") - Spec.it "can use a different output folder" \{ spago } -> do + Spec.it "can use a different output folder" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "build", "--output", "myOutput" ] >>= shouldBeSuccess - FS.exists "myOutput" `Assert.shouldReturn` true - FS.exists "output" `Assert.shouldReturn` false + FS.exists (testCwd "myOutput") `Assert.shouldReturn` true + FS.exists (testCwd "output") `Assert.shouldReturn` false - Spec.it "can build with a local custom package set" \{ spago, fixture } -> do + Spec.it "can build with a local custom package set" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - FS.unlink "spago.yaml" - FS.copyFileSync { src: fixture "local-package-set-config.yaml", dst: "spago.yaml" } - FS.copyFileSync { src: fixture "local-package-set.json", dst: "local-package-set.json" } + FS.unlink (testCwd "spago.yaml") + FS.copyFile { src: fixture "local-package-set-config.yaml", dst: testCwd "spago.yaml" } + FS.copyFile { src: fixture "local-package-set.json", dst: testCwd "local-package-set.json" } spago [ "build" ] >>= shouldBeSuccess - Spec.it "can build with a local custom package set in a parent directory" \{ spago, fixture } -> do - FS.copyFileSync { src: fixture "local-package-set.json", dst: "local-package-set.json" } - FS.mkdirp "subdir" - liftEffect $ Process.chdir "subdir" + Spec.it "can build with a local custom package set in a parent directory" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "local-package-set.json", dst: testCwd "local-package-set.json" } + let subdir = testCwd "subdir" + FS.mkdirp subdir + Paths.chdir subdir spago [ "init" ] >>= shouldBeSuccess - FS.unlink "spago.yaml" - FS.copyFileSync { src: fixture "local-package-set-config2.yaml", dst: "spago.yaml" } + FS.unlink $ subdir "spago.yaml" + FS.copyFile { src: fixture "local-package-set-config2.yaml", dst: subdir "spago.yaml" } spago [ "build" ] >>= shouldBeSuccess - Spec.it "there's only one output folder in a monorepo" \{ spago } -> do + Spec.it "there's only one output folder in a monorepo" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp (Path.concat [ "subpackage", "src" ]) - FS.mkdirp (Path.concat [ "subpackage", "test" ]) - FS.writeTextFile (Path.concat [ "subpackage", "src", "Main.purs" ]) (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile (Path.concat [ "subpackage", "test", "Main.purs" ]) (Init.testMainTemplate "Subpackage.Test.Main") - FS.writeYamlFile Config.configCodec (Path.concat [ "subpackage", "spago.yaml" ]) + FS.mkdirp $ testCwd "subpackage" "src" + FS.mkdirp $ testCwd "subpackage" "test" + FS.writeTextFile (testCwd "subpackage" "src" "Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (testCwd "subpackage" "test" "Main.purs") (Init.testMainTemplate "Subpackage.Test.Main") + FS.writeYamlFile Config.configCodec (testCwd "subpackage" "spago.yaml") ( Init.defaultConfig { name: mkPackageName "subpackage" , testModuleName: "Subpackage.Test.Main" @@ -90,12 +89,12 @@ spec = Spec.around withTempDir do ) spago [ "build" ] >>= shouldBeSuccess spago [ "build", "-p", "subpackage" ] >>= shouldBeSuccess - FS.exists "output" `Assert.shouldReturn` true - FS.exists (Path.concat [ "subpackage", "output" ]) `Assert.shouldReturn` false + FS.exists (testCwd "output") `Assert.shouldReturn` true + FS.exists (testCwd "subpackage" "output") `Assert.shouldReturn` false - Spec.it "--strict causes build to fail if there are warnings" \{ spago, fixture } -> do + Spec.it "--strict causes build to fail if there are warnings" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - let srcMain = Path.concat [ "src", "Main.purs" ] + let srcMain = testCwd "src" "Main.purs" FS.unlink srcMain FS.copyFile { src: fixture "check-strict.purs" @@ -103,11 +102,11 @@ spec = Spec.around withTempDir do } spago [ "build", "--strict" ] >>= shouldBeFailure - Spec.it "having 'strict: true' in a package config fails the build if there are warnings" \{ spago, fixture } -> do + Spec.it "having 'strict: true' in a package config fails the build if there are warnings" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let - srcMain = Path.concat [ "src", "Main.purs" ] - spagoYaml = "spago.yaml" + srcMain = testCwd "src" "Main.purs" + spagoYaml = testCwd "spago.yaml" FS.unlink srcMain FS.copyFile { src: fixture "check-strict.purs" @@ -125,8 +124,8 @@ spec = Spec.around withTempDir do spago [ "build" ] >>= shouldBeSuccess spago [ "build", "--censor-stats" ] >>= shouldBeSuccessErr (fixture "censor-stats-output.txt") - Spec.it "should censor warnings with given errorcode and prefix messsage" \{ spago, fixture } -> do - FS.copyTree { src: fixture "build/censor-warnings", dst: "." } + Spec.it "should censor warnings with given errorcode and prefix messsage" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "build/censor-warnings", dst: testCwd "." } let remainingWarningPath = [ escapePathInErrMsg [ "src", "Main.purs:5:1" ] ] @@ -145,23 +144,23 @@ spec = Spec.around withTempDir do } Spec.describe "lockfile" do - Spec.it "building with a lockfile doesn't need the Registry repo" \{ spago, fixture } -> do + Spec.it "building with a lockfile doesn't need the Registry repo" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "33.0.0" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess -- Check that we have written the lockfile - checkFixture "spago.lock" (fixture "spago.lock") + checkFixture (testCwd "spago.lock") (fixture "spago.lock") -- Then remove the registry repo - FSA.rm' Paths.registryPath { force: true, recursive: true, retryDelay: 0, maxRetries: 0 } + rmRf Paths.registryPath -- And check that we can still build spago [ "build" ] >>= shouldBeSuccess -- And that we still don't have the registry FS.exists Paths.registryPath `Assert.shouldReturn` false - Spec.it "using the --pure flag does not refresh the lockfile" \{ spago, fixture } -> do + Spec.it "using the --pure flag does not refresh the lockfile" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "33.0.0" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess -- Check that we have written the lockfile - checkFixture "spago.lock" (fixture "spago.lock") + checkFixture (testCwd "spago.lock") (fixture "spago.lock") -- Update the config let conf = Init.defaultConfig @@ -169,22 +168,22 @@ spec = Spec.around withTempDir do , testModuleName: "Test.Main" , withWorkspace: Just { setVersion: Just $ mkVersion "33.0.0" } } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") (conf { package = conf.package # map (\pkg -> pkg { dependencies = pkg.dependencies <> mkDependencies [ "maybe" ] }) }) -- Check that building with --pure does not refresh the lockfile spago [ "build", "--pure" ] >>= shouldBeSuccess - checkFixture "spago.lock" (fixture "spago.lock") + checkFixture (testCwd "spago.lock") (fixture "spago.lock") - Spec.it "lockfile is refreshed when the local package set changes" \{ spago, fixture } -> do - FS.copyTree { src: fixture "build/local-package-set-lockfile", dst: "." } + Spec.it "lockfile is refreshed when the local package set changes" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "build/local-package-set-lockfile", dst: testCwd "." } spago [ "build" ] >>= shouldBeSuccess - checkFixture "spago.lock" (fixture "build/local-package-set-lockfile/spago.lock.old") - FS.moveSync { src: "local-package-set.json", dst: "old-package-set.json" } - FS.moveSync { src: "new-package-set.json", dst: "local-package-set.json" } + checkFixture (testCwd "spago.lock") (fixture "build/local-package-set-lockfile/spago.lock.old") + FS.moveSync { src: testCwd "local-package-set.json", dst: testCwd "old-package-set.json" } + FS.moveSync { src: testCwd "new-package-set.json", dst: testCwd "local-package-set.json" } spago [ "build" ] >>= shouldBeSuccess - checkFixture "spago.lock" (fixture "build/local-package-set-lockfile/spago.lock.new") + checkFixture (testCwd "spago.lock") (fixture "build/local-package-set-lockfile/spago.lock.new") - Spec.it "compiles with the specified backend" \{ spago, fixture } -> do + Spec.it "compiles with the specified backend" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let conf = Init.defaultConfig @@ -194,42 +193,42 @@ spec = Spec.around withTempDir do { setVersion: Just $ mkVersion "0.0.1" } } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") (conf { workspace = conf.workspace # map (_ { backend = Just { cmd: "echo", args: Just [ "hello" ] } }) }) - spago [ "build" ] >>= shouldBeSuccess + spago [ "build", "-v" ] >>= shouldBeSuccess spago [ "run" ] >>= shouldBeSuccessErr (fixture "alternate-backend-output.txt") -- We also make sure that no js files are produced, only corefn - FS.exists "output/Main/index.js" `Assert.shouldReturn` false - FS.exists "output/Main/corefn.json" `Assert.shouldReturn` true + FS.exists (testCwd "output" "Main" "index.js") `Assert.shouldReturn` false + FS.exists (testCwd "output" "Main" "corefn.json") `Assert.shouldReturn` true Spec.it "passing the --codegen flag to purs fails" \{ spago, fixture } -> do spago [ "init", "--name", "7368613235362d68766258694c614d517a3667747a58725778" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "build", "--purs-args", "--codegen", "--purs-args", "corefn" ] >>= shouldBeFailureErr (fixture "codegen-opt.txt") - Spec.it "passing the --ensure-ranges flag without package selection adds ranges to root package when it exists" \{ spago } -> do + Spec.it "passing the --ensure-ranges flag without package selection adds ranges to root package when it exists" \{ spago, testCwd } -> do spago [ "init", "--package-set", "0.0.1" ] >>= shouldBeSuccess spago [ "build", "--ensure-ranges" ] >>= shouldBeSuccess - spagoYaml <- FS.readTextFile "spago.yaml" + spagoYaml <- FS.readTextFile (testCwd "spago.yaml") spagoYaml `shouldContain` "- prelude: \">=6.0.1 <7.0.0\"" - Spec.it "failed build with many warnings and --json-errors does not truncate output" \{ spago, fixture } -> do - FS.copyTree { src: fixture "build/json-truncated-many-warnings", dst: "." } + Spec.it "failed build with many warnings and --json-errors does not truncate output" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "build/json-truncated-many-warnings", dst: testCwd "." } spago [ "build", "--json-errors" ] >>= shouldBeFailureOutput case Process.platform of Just Platform.Win32 -> fixture "build/json-truncated-many-warnings/warnings-windows.json" _ -> fixture "build/json-truncated-many-warnings/warnings.json" - Spec.it "building with old-format config files works, as well as migrating them" \{ spago, fixture } -> do - FS.copyTree { src: fixture "build/migrate-config", dst: "." } + Spec.it "building with old-format config files works, as well as migrating them" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "build/migrate-config", dst: testCwd "." } spago [ "build" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccessErr (fixture "build/migrate-config/unmigrated-warning.txt") spago [ "build", "--migrate" ] >>= shouldBeSuccessErr (fixture "build/migrate-config/migrating-output.txt") spago [ "build" ] >>= shouldBeSuccessErr (fixture "build/migrate-config/migrated-output.txt") - checkFixture "spago.yaml" (fixture "build/migrate-config/migrated-spago.yaml") + checkFixture (testCwd "spago.yaml") (fixture "build/migrate-config/migrated-spago.yaml") - Spec.it "#1148: outputs errors and warnings after build" \{ spago, fixture } -> do + Spec.it "#1148: outputs errors and warnings after build" \{ spago, fixture, testCwd } -> do let shouldBeSuccessErr' = checkOutputsWithPathSeparatorPatchErr isRight shouldBeFailureErr' = checkOutputsWithPathSeparatorPatchErr isLeft @@ -245,13 +244,13 @@ spec = Spec.around withTempDir do >>> String.replaceAll (String.Pattern $ "\r\n") (String.Replacement "\n") } - FS.copyTree { src: fixture "build/1148-warnings-diff-errors", dst: "." } + FS.copyTree { src: fixture "build/1148-warnings-diff-errors", dst: testCwd "." } - liftEffect $ Process.chdir "errors" + Paths.chdir $ testCwd "errors" spago [ "install" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeFailureErr' "build/1148-warnings-diff-errors/errors/expected-stderr.txt" - liftEffect $ Process.chdir "../warnings" + Paths.chdir $ testCwd "warnings" spago [ "install" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccessErr' "build/1148-warnings-diff-errors/warnings/expected-stderr.txt" diff --git a/test/Spago/Build/BuildInfo.purs b/test/Spago/Build/BuildInfo.purs index 4fa0eb863..eb8421cb2 100644 --- a/test/Spago/Build/BuildInfo.purs +++ b/test/Spago/Build/BuildInfo.purs @@ -6,13 +6,13 @@ import Control.Monad.Error.Class as MonadError import Data.Array as Array import Data.DateTime.Instant as Instant import Effect.Exception as Exception -import Node.Path as Path import Registry.Version as Version import Spago.Command.Init (DefaultConfigOptions(..)) import Spago.Command.Init as Init import Spago.Core.Config as Config import Spago.FS as FS import Spago.Log (LogVerbosity(..)) +import Spago.Path as Path import Spago.Purs (getPurs) import Test.Spec (SpecT) import Test.Spec as Spec @@ -61,8 +61,8 @@ spec = setupSinglePackage spago = do spago [ "init", "--name", packageName ] >>= shouldBeSuccess - FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) $ writeMain srcAndTestContent - FS.writeTextFile (Path.concat [ "test", "Test", "Main.purs" ]) $ writeTestMain srcAndTestContent + FS.writeTextFile (Path.global "src/Main.purs") $ writeMain srcAndTestContent + FS.writeTextFile (Path.global "test/Test/Main.purs") $ writeTestMain srcAndTestContent Spec.it ("'spago build' works") \{ spago } -> do setupSinglePackage spago @@ -93,26 +93,27 @@ spec = let packages = [ "foo", "bar", "baz" ] setupPolyrepo = do - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (Path.global "spago.yaml") $ Init.defaultConfig' $ WorkspaceOnly { setVersion: Just $ unsafeFromRight $ Version.parse "0.0.1" } for_ packages \packageName -> do - FS.mkdirp packageName - FS.writeYamlFile Config.configCodec (Path.concat [ packageName, "spago.yaml" ]) + let package = Path.global packageName + FS.mkdirp package + FS.writeYamlFile Config.configCodec (package "spago.yaml") $ mkPackageOnlyConfig { packageName, srcDependencies: [ "prelude", "effect", "console" ] } [ configAddTestMain ] let - src = Path.concat [ packageName, "src" ] - test = Path.concat [ packageName, "test", "Test" ] + src = package "src" + test = package "test" "Test" fileContent = pursModuleUsingBuildInfo packages FS.mkdirp src FS.mkdirp test - FS.writeTextFile (Path.concat [ src, "Main.purs" ]) $ writePursFile + FS.writeTextFile (src "Main.purs") $ writePursFile { moduleName: mkSrcModuleName packageName , rest: fileContent } - FS.writeTextFile (Path.concat [ test, "Main.purs" ]) $ writePursFile + FS.writeTextFile (test "Main.purs") $ writePursFile { moduleName: mkTestModuleName packageName , rest: fileContent } diff --git a/test/Spago/Build/Monorepo.purs b/test/Spago/Build/Monorepo.purs index 01828c011..257d45be0 100644 --- a/test/Spago/Build/Monorepo.purs +++ b/test/Spago/Build/Monorepo.purs @@ -8,10 +8,9 @@ import Data.String as String import Data.String.Regex as Regex import Data.String.Regex.Flags as Regex.Flags import Effect.Aff (bracket) -import Node.Path as Path -import Node.Process as Process import Spago.Cmd as Cmd import Spago.FS as FS +import Spago.Path as Path import Spago.Paths as Paths import Test.Spec (SpecT) import Test.Spec as Spec @@ -35,8 +34,8 @@ spec = Spec.describe "monorepo" do end ``` -} - Spec.it "Case 1: 'independent packages' builds" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/case1-independent-packages", dst: "." } + Spec.it "Case 1: 'independent packages' builds" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/case1-independent-packages", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess {- @@ -51,8 +50,8 @@ spec = Spec.describe "monorepo" do end ``` -} - Spec.it "Case 2: 'shared dependencies packages' builds" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/case2-shared-dependencies1", dst: "." } + Spec.it "Case 2: 'shared dependencies packages' builds" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/case2-shared-dependencies1", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess {- @@ -68,10 +67,10 @@ spec = Spec.describe "monorepo" do end ``` -} - Spec.it "Case 3: 'dependencies: A&B -> C; A -> B' builds" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/case3-shared-dependencies2", dst: "." } + Spec.it "Case 3: 'dependencies: A&B -> C; A -> B' builds" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/case3-shared-dependencies2", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess - checkFixture "spago.lock" (fixture "polyrepo.lock") + checkFixture (testCwd "spago.lock") (fixture "polyrepo.lock") {- ```mermaid @@ -83,8 +82,8 @@ spec = Spec.describe "monorepo" do end ``` -} - Spec.it "declaring 2+ modules with the same name across 2+ packages fails to build" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/unique-module-names", dst: "." } + Spec.it "declaring 2+ modules with the same name across 2+ packages fails to build" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/unique-module-names", dst: Path.toGlobal testCwd } let sameModuleName = "SameModuleName" hasExpectedModules stdErr = do @@ -103,33 +102,33 @@ spec = Spec.describe "monorepo" do end ``` -} - Spec.it "#1161 regression: 'subpackage with disjoint git dependency' builds" \{ spago, fixture } -> do + Spec.it "#1161 regression: 'subpackage with disjoint git dependency' builds" \{ spago, fixture, testCwd } -> do -- This corner case happens only under very specific conditions: -- 1. there must be a root package -- 2. one of the dependencies of the subpackage must be fetched from git. -- This is a problem only when the git dependency is not a dependency of the root package. -- 3. the workspace needs to contain a subpackage that is using the git dependency - FS.copyTree { src: fixture "monorepo/1161-regression", dst: "." } + FS.copyTree { src: fixture "monorepo/1161-regression", dst: Path.toGlobal testCwd } -- Lastly, this broke only when building the root package spago [ "build", "-p", "root" ] >>= shouldBeSuccess -- Or getting its graph spago [ "uninstall", "-p", "root", "console", "effect", "prelude" ] >>= shouldBeSuccess spago [ "build", "-p", "root", "--pedantic-packages" ] >>= shouldBeSuccess - Spec.it "ignore nested workspaces" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/ignore-nested-workspaces", dst: "." } + Spec.it "ignore nested workspaces" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/ignore-nested-workspaces", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccessErr (fixture "monorepo/ignore-nested-workspaces/expected-stderr.txt") - Spec.it "it's possible to reference local packages when using the solver" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/local-packages-work-with-solver", dst: "." } + Spec.it "it's possible to reference local packages when using the solver" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/local-packages-work-with-solver", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccessErr (fixture "monorepo/local-packages-work-with-solver/expected-stderr.txt") Spec.describe "warning censoring and error-promotion" do - Spec.it "build succeeds when 'strict: true' because warnings were censored" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/strict-true-censored-warnings", dst: "." } + Spec.it "build succeeds when 'strict: true' because warnings were censored" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/strict-true-censored-warnings", dst: Path.toGlobal testCwd } let paths = [ escapePathInErrMsg [ "package-a", "src", "Main.purs:6:13" ] @@ -139,8 +138,8 @@ spec = Spec.describe "monorepo" do shouldNotHaveWarning = assertWarning paths false spago [ "build" ] >>= check { stdout: mempty, stderr: shouldNotHaveWarning, result: isRight } - Spec.it "build fails when 'strict: true' and warnings were not censored" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/strict-true-uncensored-warnings", dst: "." } + Spec.it "build fails when 'strict: true' and warnings were not censored" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/strict-true-uncensored-warnings", dst: Path.toGlobal testCwd } let errs = [ "[ERROR 1/2 UnusedName] " <> escapePathInErrMsg [ "package-b", "src", "Main.purs:6:13" ] @@ -151,16 +150,16 @@ spec = Spec.describe "monorepo" do Spec.describe "passing --ensure-ranges flag..." do - Spec.it "when root package exists adds ranges to the root package" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/ensure-ranges-root-package", dst: "." } + Spec.it "when root package exists adds ranges to the root package" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/ensure-ranges-root-package", dst: Path.toGlobal testCwd } spago [ "build", "--ensure-ranges" ] >>= shouldBeSuccess - spagoYaml <- FS.readTextFile "spago.yaml" + spagoYaml <- FS.readTextFile $ testCwd "spago.yaml" spagoYaml `AssertString.shouldContain` "- prelude: \">=6.0.1 <7.0.0\"" - Spec.it "when root package does not exist fails to build" \{ spago, fixture } -> do + Spec.it "when root package does not exist fails to build" \{ spago, fixture, testCwd } -> do -- Note: this needs to contain at least two subpackages, otherwise, spago will -- automatically select the only package available even if it's a non-root package. - FS.copyTree { src: fixture "monorepo/ensure-ranges-no-root-package", dst: "." } + FS.copyTree { src: fixture "monorepo/ensure-ranges-no-root-package", dst: Path.toGlobal testCwd } let hasNoRootPackageError stdErr = do @@ -213,8 +212,8 @@ spec = Spec.describe "monorepo" do , " " <> pkgModName ] - Spec.it "when package config has 'pedantic_packages: true', build fails with expected errors" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/pedantic-config", dst: "." } + Spec.it "when package config has 'pedantic_packages: true', build fails with expected errors" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/pedantic-config", dst: Path.toGlobal testCwd } let errs = @@ -233,8 +232,8 @@ spec = Spec.describe "monorepo" do Assert.fail $ "STDERR did not contain expected texts:\n" <> (Array.intercalate "\n\n" unfoundTexts) <> "\n\nStderr was:\n" <> stdErr spago [ "build" ] >>= check { stdout: mempty, stderr: hasExpectedErrors, result: isLeft } - Spec.it "passing --pedantic-packages overrides package and test configs" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/pedantic-flag", dst: "." } + Spec.it "passing --pedantic-packages overrides package and test configs" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/pedantic-flag", dst: Path.toGlobal testCwd } let errs = do @@ -248,17 +247,17 @@ spec = Spec.describe "monorepo" do Assert.fail $ "STDERR did not contain expected texts:\n" <> (Array.intercalate "\n\n" unfoundTexts) <> "\n\nStderr was:\n" <> stdErr spago [ "build", "--pedantic-packages" ] >>= check { stdout: mempty, stderr: hasExpectedErrors, result: isLeft } - Spec.it "prevents cross-package imports between local packages" \{ spago, fixture } -> do - FS.copyTree { src: fixture "monorepo/pedantic-cross-package-imports", dst: "." } + Spec.it "prevents cross-package imports between local packages" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "monorepo/pedantic-cross-package-imports", dst: Path.toGlobal testCwd } spago [ "build" ] >>= shouldBeSuccess spago [ "build", "--pedantic-packages" ] >>= shouldBeFailureErr (fixture "monorepo/pedantic-cross-package-imports/expected-stderr.txt") - Spec.it "#1208: clones a monorepo only once, even if multiple packages from it are needed" \{ spago, fixture, testCwd } -> do + Spec.it "#1208: clones a monorepo only once, even if multiple packages from it are needed" \testArgs@{ spago, fixture } -> do -- A local file system Git repo to use as a remote for Spago to clone from let createLibraryRepo = do - let libRepo = Path.concat [ Paths.paths.temp, "spago-1208" ] + let libRepo = Paths.paths.temp "spago-1208" whenM (FS.exists libRepo) $ rmRf libRepo FS.copyTree { src: fixture "monorepo/1208-no-double-cloning/library", dst: libRepo } git_ libRepo [ "init" ] @@ -273,28 +272,30 @@ spec = Spec.describe "monorepo" do bracket createLibraryRepo rmRf \libRepo -> do let + consumerDir = testArgs.testCwd "consumer" + libRepoStr = Path.toRaw libRepo recreateConsumerWorkspace = do - liftEffect $ Process.chdir testCwd - whenM (FS.exists "consumer") $ rmRf "consumer" - FS.mkdirp "consumer" - liftEffect $ Process.chdir "consumer" + Paths.chdir testArgs.testCwd + whenM (FS.exists consumerDir) $ rmRf consumerDir + FS.mkdirp consumerDir + Paths.chdir consumerDir copySpagoYaml "spago-two-deps.yaml" copySpagoYaml src = do - whenM (FS.exists "spago.yaml") $ FS.unlink "spago.yaml" - whenM (FS.exists "spago.lock") $ FS.unlink "spago.lock" - content <- FS.readTextFile $ fixture "monorepo/1208-no-double-cloning/" <> src - FS.writeTextFile "spago.yaml" $ String.replaceAll (String.Pattern "") (String.Replacement libRepo) content + whenM (FS.exists $ consumerDir "spago.yaml") $ FS.unlink $ consumerDir "spago.yaml" + whenM (FS.exists $ consumerDir "spago.lock") $ FS.unlink $ consumerDir "spago.lock" + content <- FS.readTextFile $ fixture "monorepo/1208-no-double-cloning" src + FS.writeTextFile (consumerDir "spago.yaml") $ String.replaceAll (String.Pattern "") (String.Replacement libRepoStr) content assertRefCheckedOut package ref = do -- The `.spago/p//` should be a git repo checked out at `ref` - let path = Path.concat [ ".spago", "p", package, ref ] + let path = consumerDir ".spago" "p" package ref commitHash <- git path [ "rev-parse", ref ] git path [ "rev-parse", "HEAD" ] >>= flip shouldEqualStr commitHash -- And there should be a copy of that repo at -- `.spago/p//`, checked out at the same commit. - let commitHashPath = Path.concat [ ".spago", "p", package, commitHash ] + let commitHashPath = consumerDir ".spago" "p" package commitHash git commitHashPath [ "rev-parse", "HEAD" ] >>= flip shouldEqualStr commitHash shouldBeSuccessErr' = checkOutputsWithPatchErr isRight @@ -307,7 +308,7 @@ spec = Spec.describe "monorepo" do , result , sanitize: String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n") - >>> String.replaceAll (String.Pattern libRepo) (String.Replacement "") + >>> String.replaceAll (String.Pattern libRepoStr) (String.Replacement "") >>> Regex.replace (unsafeFromRight $ Regex.regex "^purs compile: .*$" (Regex.Flags.global <> Regex.Flags.multiline)) "purs compile..." >>> String.trim } @@ -316,7 +317,7 @@ spec = Spec.describe "monorepo" do -- otherwise it may or may not appear in Spago's output and then we can't -- reliably compare it to golden output. recreateConsumerWorkspace - spago [ "ls", "packages" ] >>= shouldBeSuccess + spago [ "ls", "packages", "-v" ] >>= shouldBeSuccess -- Nuke the cache after that so Spago can re-clone the repositories and we -- can check that it's happening only once. @@ -358,14 +359,15 @@ spec = Spec.describe "monorepo" do -- Lockfile test: when it's up to date but the cache is not populated (i.e. a fresh clone) -- then there are no double clones. This is a regression test for #1206 spago [ "build" ] >>= shouldBeSuccess - rmRf ".spago" + rmRf $ consumerDir ".spago" spago [ "build" ] >>= shouldBeSuccessErr' "monorepo/1208-no-double-cloning/expected-stderr/lockfile-up-to-date.txt" where git_ cwd = void <<< git cwd + git :: ∀ path. Path.IsPath path => path -> _ git cwd args = do - let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just cwd } - res <- Cmd.exec "git" args opts + let opts = Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, cwd = Just $ Path.toGlobal cwd } + res <- Cmd.exec (Path.global "git") args opts res # shouldBeSuccess pure $ Cmd.getStdout res diff --git a/test/Spago/Build/Pedantic.purs b/test/Spago/Build/Pedantic.purs index 154a5586c..700952015 100644 --- a/test/Spago/Build/Pedantic.purs +++ b/test/Spago/Build/Pedantic.purs @@ -4,9 +4,9 @@ import Test.Prelude import Data.Array as Array import Data.Map as Map -import Node.Path as Path import Spago.Core.Config (Dependencies(..), Config) import Spago.FS as FS +import Spago.Path as Path import Test.Spec (SpecT) import Test.Spec as Spec @@ -18,8 +18,8 @@ spec = -- Here we are importing the `Control.Alt` module, which is in the `control` -- package, which comes through `maybe` but we are not importing directly - Spec.it "appear in the source package" \{ spago, fixture } -> do - setup spago + Spec.it "appear in the source package" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installSourcePackages = [ "maybe" ] , main = Just @@ -41,8 +41,8 @@ spec = -- We are importing `Control.Alt` in the test package, which is in the `control` -- package, which comes through `maybe` but we are not importing directly, so we -- should get a transitivity warning about that - Spec.it "appear in the test package" \{ spago, fixture } -> do - setup spago + Spec.it "appear in the test package" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installTestPackages = [ "maybe" ] , main = Just @@ -76,8 +76,8 @@ spec = -- Here we install `effect` and `console` in the test package, and we don't use them -- in the source, so we should get an "unused" warning about them - Spec.it "in a source package" \{ spago, fixture } -> do - setup spago + Spec.it "in a source package" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installTestPackages = [ "effect", "console" ] , main = Just @@ -93,8 +93,8 @@ spec = -- Here we do not install `effect` and `console` in the test package, and we don't use them -- in the source, so we should get an "unused" warning about them for the source, and a prompt -- to install them in test - Spec.it "in a source package, but they are used in test" \{ spago, fixture } -> do - setup spago + Spec.it "in a source package, but they are used in test" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { main = Just [ "import Prelude" @@ -107,8 +107,8 @@ spec = spago [ "build" ] >>= shouldBeFailureErr (fixture "pedantic/check-unused-dependency.txt") -- Complain about the unused `newtype` dependency in the test package - Spec.it "in a test package" \{ spago, fixture } -> do - setup spago + Spec.it "in a test package" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installTestPackages = [ "newtype" ] , testMain = Just @@ -126,8 +126,8 @@ spec = >>= shouldBeFailureErr (fixture "pedantic/check-unused-test-dependency.txt") -- `console` and `effect` are going to be unused for both source and test packages - Spec.it "in both the source and test packages" \{ spago, fixture } -> do - setup spago + Spec.it "in both the source and test packages" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installSourcePackages = [ "prelude", "effect", "console" ] , installTestPackages = [ "prelude", "effect", "console" ] @@ -157,8 +157,8 @@ spec = -- so consider that fixed -- * `either` is transitively imported, and it's going to be removed from the source -- dependencies, so we get a "transitive" warning to install it in test - Spec.it "fails to build and reports deduplicated src and test unused/transitive dependencies" \{ spago, fixture } -> do - setup spago + Spec.it "fails to build and reports deduplicated src and test unused/transitive dependencies" \{ spago, fixture, testCwd } -> do + setup testCwd spago ( defaultSetupConfig { installSourcePackages = [ "prelude", "control", "either" ] , installTestPackages = [ "tuples" ] @@ -191,8 +191,8 @@ spec = -- So, if we don't have `effect` as a direct dependency, we'll get a pedantic error -- where the fix is to install that missing package. -- Following those instructions shouldn't cause an error. - Spec.it "following installation instructions does not fail with an unrelated pedantic error" \{ spago, fixture } -> do - FS.copyTree { src: fixture "pedantic/follow-instructions", dst: "." } + Spec.it "following installation instructions does not fail with an unrelated pedantic error" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "pedantic/follow-instructions", dst: testCwd } spago [ "uninstall", "effect" ] >>= shouldBeSuccess -- Get rid of "Compiling..." messages spago [ "build" ] >>= shouldBeSuccess @@ -204,10 +204,9 @@ spec = let gitignores = [".spago", "/.spago", ".spago/**"] for_ gitignores \gitignore -> Spec.it - (".gitignore does not affect discovery of transitive deps (" <> gitignore <> ")") - \{ spago, fixture } -> do - FS.copyTree { src: fixture "pedantic/follow-instructions", dst: "." } - FS.writeTextFile ".gitignore" gitignore + (".gitignore does not affect discovery of transitive deps (" <> gitignore <> ")") \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "pedantic/follow-instructions", dst: testCwd } + FS.writeTextFile (Path.global ".gitignore") gitignore spago [ "uninstall", "effect" ] >>= shouldBeSuccess -- Get rid of "Compiling..." messages spago [ "build" ] >>= shouldBeSuccess @@ -255,16 +254,16 @@ defaultSetupConfig = , testMain: Nothing } -setup :: (Array String -> Aff (Either ExecResult ExecResult)) -> SetupConfig -> Aff Unit -setup spago config = do +setup :: RootPath -> (Array String -> Aff (Either ExecResult ExecResult)) -> SetupConfig -> Aff Unit +setup root spago config = do spago [ "init", "--name", "pedantic" ] >>= shouldBeSuccess unless (Array.null config.installSourcePackages) do spago ([ "install" ] <> config.installSourcePackages) >>= shouldBeSuccess unless (Array.null config.installTestPackages) do spago ([ "install", "--test-deps" ] <> config.installTestPackages) >>= shouldBeSuccess for_ config.main \main -> - FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) $ writeMain main + FS.writeTextFile (root "src" "Main.purs") $ writeMain main for_ config.testMain \testMain -> - FS.writeTextFile (Path.concat [ "test", "Test", "Main.purs" ]) $ writeTestMain testMain + FS.writeTextFile (root "test" "Test" "Main.purs") $ writeTestMain testMain -- get rid of "Compiling ..." messages and other compiler warnings spago [ "build" ] >>= shouldBeSuccess diff --git a/test/Spago/Bundle.purs b/test/Spago/Bundle.purs index b3e005927..d11e9051e 100644 --- a/test/Spago/Bundle.purs +++ b/test/Spago/Bundle.purs @@ -15,68 +15,68 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "bundle" do - Spec.it "bundles into an app (browser)" \{ spago, fixture } -> do + Spec.it "bundles into an app (browser)" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "bundle", "-v", "--bundle-type", "app", "--outfile", "bundle-app-browser.js" ] >>= shouldBeSuccess - checkBundle "bundle-app-browser.js" (fixture "bundle-app-browser.js") + checkBundle (testCwd "bundle-app-browser.js") (fixture "bundle-app-browser.js") - Spec.it "bundles into an app (node)" \{ spago, fixture } -> do + Spec.it "bundles into an app (node)" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "bundle", "-v", "--bundle-type", "app", "--outfile", "bundle-app-node.js", "--platform", "node" ] >>= shouldBeSuccess - checkBundle "bundle-app-node.js" (fixture "bundle-app-node.js") + checkBundle (testCwd "bundle-app-node.js") (fixture "bundle-app-node.js") - Spec.it "bundles into a module" \{ spago, fixture } -> do + Spec.it "bundles into a module" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "bundle", "--bundle-type=module", "--outfile", "bundle-module.js" ] >>= shouldBeSuccess - checkBundle "bundle-module.js" (fixture "bundle-module.js") + checkBundle (testCwd "bundle-module.js") (fixture "bundle-module.js") - Spec.it "bundles an app with source map (browser)" \{ spago, fixture } -> do + Spec.it "bundles an app with source map (browser)" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "bundle", "-v", "--outfile", "bundle-app-browser-map.js", "--source-maps", "--bundle-type", "app" ] >>= shouldBeSuccess - checkBundle "bundle-app-browser-map.js" (fixture "bundle-app-browser-map.js") - checkFixture "bundle-app-browser-map.js.map" (fixture "bundle-app-browser-map.js.map") + checkBundle (testCwd "bundle-app-browser-map.js") (fixture "bundle-app-browser-map.js") + checkFixture (testCwd "bundle-app-browser-map.js.map") (fixture "bundle-app-browser-map.js.map") - Spec.it "bundles an app with source map (node)" \{ spago, fixture } -> do + Spec.it "bundles an app with source map (node)" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "bundle", "-v", "--outfile", "bundle-app-node-map.js", "--source-maps", "--bundle-type", "app", "--platform", "node" ] >>= shouldBeSuccess - checkBundle "bundle-app-node-map.js" (fixture "bundle-app-node-map.js") - checkFixture "bundle-app-node-map.js.map" (fixture "bundle-app-node-map.js.map") + checkBundle (testCwd "bundle-app-node-map.js") (fixture "bundle-app-node-map.js") + checkFixture (testCwd "bundle-app-node-map.js.map") (fixture "bundle-app-node-map.js.map") - Spec.it "bundles a module with source map" \{ spago, fixture } -> do + Spec.it "bundles a module with source map" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "bundle", "--bundle-type", "module", "--outfile", "bundle-module-map.js", "--source-maps" ] >>= shouldBeSuccess - checkBundle "bundle-module-map.js" (fixture "bundle-module-map.js") - checkFixture "bundle-module-map.js.map" (fixture "bundle-module-map.js.map") + checkBundle (testCwd "bundle-module-map.js") (fixture "bundle-module-map.js") + checkFixture (testCwd "bundle-module-map.js.map") (fixture "bundle-module-map.js.map") - Spec.it "bundles a module with extra esbuild arguments" \{ spago, fixture } -> do + Spec.it "bundles a module with extra esbuild arguments" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "bundle", "--bundle-type", "module", "--outfile", "bundle-module-map.js", "--source-maps" ] >>= shouldBeSuccess - checkBundle "bundle-module-map.js" (fixture "bundle-module-map.js") - checkFixture "bundle-module-map.js.map" (fixture "bundle-module-map.js.map") + checkBundle (testCwd "bundle-module-map.js") (fixture "bundle-module-map.js") + checkFixture (testCwd "bundle-module-map.js.map") (fixture "bundle-module-map.js.map") - Spec.it "refuses to overwrite existing bundle that is not Spago-generated" \{ spago, fixture } -> do - FS.writeTextFile checkWatermarkMarkerFileName "." + Spec.it "refuses to overwrite existing bundle that is not Spago-generated" \{ spago, fixture, testCwd } -> do + FS.writeTextFile (testCwd checkWatermarkMarkerFileName) "." spago [ "init", "--name", "project" ] >>= shouldBeSuccess spago [ "bundle" ] >>= shouldBeSuccess spago [ "bundle" ] >>= shouldBeSuccess - FS.readTextFile "index.js" >>= \content -> content `shouldStartWith` "/* Generated by Spago" - FS.writeTextFile "index.js" "Bogus" + FS.readTextFile (testCwd "index.js") >>= \content -> content `shouldStartWith` "/* Generated by Spago" + FS.writeTextFile (testCwd "index.js") "Bogus" spago [ "bundle" ] >>= shouldBeFailureErr (fixture "bundle-refuse-overwrite-output.txt") - FS.readTextFile "index.js" >>= shouldEqual "Bogus" + FS.readTextFile (testCwd "index.js") >>= shouldEqual "Bogus" spago [ "bundle", "--force" ] >>= shouldBeSuccess - FS.readTextFile "index.js" >>= shouldNotEqual "Bogus" + FS.readTextFile (testCwd "index.js") >>= shouldNotEqual "Bogus" - Spec.it "overwrites non-Spago-generated bundle when there is no magic marker file" \{ spago, fixture } -> do + Spec.it "overwrites non-Spago-generated bundle when there is no magic marker file" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "project" ] >>= shouldBeSuccess spago [ "bundle" ] >>= shouldBeSuccess - FS.writeTextFile "index.js" "Bogus" + FS.writeTextFile (testCwd "index.js") "Bogus" spago [ "bundle" ] >>= shouldBeSuccess - checkBundle "index.js" (fixture "bundle-default.js") + checkBundle (testCwd "index.js") (fixture "bundle-default.js") where -- This is a version of `checkFixture`, but it replaces the "v0" placeholder diff --git a/test/Spago/Docs.purs b/test/Spago/Docs.purs index bd0be93a3..faa7d5134 100644 --- a/test/Spago/Docs.purs +++ b/test/Spago/Docs.purs @@ -11,17 +11,17 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "docs" do - Spec.it "documents successfully with no flags" \{ spago } -> do + Spec.it "documents successfully with no flags" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "docs" ] >>= shouldBeSuccess - FS.exists "generated-docs/html/index.html" `Assert.shouldReturn` true + FS.exists (testCwd "generated-docs" "html" "index.html") `Assert.shouldReturn` true - Spec.it "builds successfully a solver-only package" \{ spago } -> do + Spec.it "builds successfully a solver-only package" \{ spago, testCwd } -> do spago [ "init", "--name", "aaa", "--use-solver" ] >>= shouldBeSuccess spago [ "docs" ] >>= shouldBeSuccess - FS.exists "generated-docs/html/index.html" `Assert.shouldReturn` true + FS.exists (testCwd "generated-docs" "html" "index.html") `Assert.shouldReturn` true - Spec.it "can output ctags instead of html" \{ spago } -> do + Spec.it "can output ctags instead of html" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "docs", "--format", "ctags" ] >>= shouldBeSuccess - FS.exists "tags" `Assert.shouldReturn` true + FS.exists (testCwd "tags") `Assert.shouldReturn` true diff --git a/test/Spago/Errors.purs b/test/Spago/Errors.purs index 2830c95e8..6bf412413 100644 --- a/test/Spago/Errors.purs +++ b/test/Spago/Errors.purs @@ -13,21 +13,21 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "errors" do - Spec.it "fails with a spago.yml" \{ spago, fixture } -> do + Spec.it "fails with a spago.yml" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa" ] >>= shouldBeSuccess - FS.moveSync { src: "spago.yaml", dst: "spago.yml" } + FS.moveSync { src: testCwd "spago.yaml", dst: testCwd "spago.yml" } spago [ "build" ] >>= shouldBeFailureErr (fixture "spago-yml-check-stderr.txt") Spec.it "fails for package names that are too long" \{ spago, fixture } -> do let name = String.joinWith "" $ Array.replicate 256 "a" spago [ "init", "--name", name ] >>= shouldBeFailureErr (fixture "package-name-too-long-stderr.txt") - Spec.it "prints suggested package names when package is not found" \{ spago, fixture } -> do + Spec.it "prints suggested package names when package is not found" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "root" ] >>= shouldBeSuccess ["finder", "binder", "founder"] # traverse_ \name -> do - FS.mkdirp name - FS.writeTextFile (name <> "/spago.yaml") $ + FS.mkdirp $ testCwd name + FS.writeTextFile (testCwd name "spago.yaml") $ "{ package: { name: \"" <> name <> "\", dependencies: [] } }" spago [ "build", "-p", "inder" ] >>= shouldBeFailureErr (fixture "package-typo-suggestions/1.txt") diff --git a/test/Spago/Glob.purs b/test/Spago/Glob.purs index c5c86a86a..97e1b3c63 100644 --- a/test/Spago/Glob.purs +++ b/test/Spago/Glob.purs @@ -5,23 +5,23 @@ import Test.Prelude import Data.Array as Array import Data.Foldable (intercalate) import Effect.Aff as Aff -import Node.Path as Path import Spago.FS as FS import Spago.Glob as Glob +import Spago.Path as Path import Test.Spec (Spec) import Test.Spec as Spec import Test.Spec.Assertions as Assert -globTmpDir :: (String -> Aff Unit) -> Aff Unit +globTmpDir :: (RootPath -> Aff Unit) -> Aff Unit globTmpDir m = Aff.bracket make cleanup m where - touch name base = FS.writeTextFile (Path.concat [ base, name ]) "" + touch name base = FS.writeTextFile (base name) "" dir name contents base = do - FS.mkdirp $ Path.concat [ base, name ] - for_ contents \f -> f $ Path.concat [ base, name ] + FS.mkdirp $ base name + for_ contents \f -> f =<< Path.mkRoot (base name) cleanup _ = pure unit make = do - base <- mkTemp' $ Just "spago-test-" + base <- Path.mkRoot =<< mkTemp' (Just "spago-test-") dir ".git" [ dir "fruits" [ touch "apple" ] ] @@ -46,50 +46,52 @@ globTmpDir m = Aff.bracket make cleanup m spec :: Spec Unit spec = Spec.around globTmpDir do - let glob cwd includePatterns = Glob.gitignoringGlob { cwd, includePatterns, ignorePatterns: [] } + let glob root includePatterns = Glob.gitignoringGlob { root, includePatterns, ignorePatterns: [] } Spec.describe "glob" do Spec.describe "glob behavior" do Spec.it "'**/..' matches 0 or more directories" \p -> do - a <- glob (Path.concat [ p, "fruits/left" ]) [ "**/apple" ] - b <- glob (Path.concat [ p, "fruits" ]) [ "**/apple" ] - Array.sort a `Assert.shouldEqual` [ "apple" ] - Array.sort b `Assert.shouldEqual` [ "left/apple", "right/apple" ] + aRoot <- Path.mkRoot (p "fruits" "left") + bRoot <- Path.mkRoot (p "fruits") + a <- glob aRoot [ "**/apple" ] + b <- glob bRoot [ "**/apple" ] + sortedPaths a `Assert.shouldEqual` [ "apple" ] + sortedPaths b `Assert.shouldEqual` [ "left/apple", "right/apple" ] Spec.it "'../**/..' matches 0 or more directories" \p -> do a <- glob p [ "fruits/**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] Spec.it "'../**' matches 0 or more directories" \p -> do a <- glob p [ "fruits/left/**" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left", "fruits/left/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left", "fruits/left/apple" ] Spec.describe "gitignoringGlob" do Spec.it "when no .gitignore, yields all matches" \p -> do a <- glob p [ "**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple", "src/fruits/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple", "src/fruits/apple" ] Spec.it "respects a .gitignore pattern that doesn't conflict with search" \p -> do - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) "fruits/right" + FS.writeTextFile (p ".gitignore") "fruits/right" a <- glob p [ "fruits/**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple" ] Spec.it "respects some .gitignore patterns" \p -> do - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) "fruits\nfruits/right" + FS.writeTextFile (p ".gitignore") "fruits\nfruits/right" a <- glob p [ "fruits/**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple" ] Spec.it "respects a negated .gitignore pattern" \p -> do - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) "!/fruits/left/apple\n/fruits/**/apple" + FS.writeTextFile (p ".gitignore") "!/fruits/left/apple\n/fruits/**/apple" a <- glob p [ "**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple", "src/fruits/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple", "src/fruits/apple" ] for_ [ "/fruits", "fruits", "fruits/", "**/fruits", "fruits/**", "**/fruits/**" ] \gitignore -> do Spec.it ("does not respect a .gitignore pattern that conflicts with search: " <> gitignore) \p -> do - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) gitignore + FS.writeTextFile (p ".gitignore") gitignore a <- glob p [ "fruits/**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] Spec.it "is stacksafe" \p -> do let @@ -98,14 +100,17 @@ spec = Spec.around globTmpDir do words = [ \a b c d -> a <> b <> c <> d ] <*> chars <*> chars <*> chars <*> chars hugeGitignore = intercalate "\n" words -- Write it in a few places - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) hugeGitignore - FS.writeTextFile (Path.concat [ p, "fruits", ".gitignore" ]) hugeGitignore - FS.writeTextFile (Path.concat [ p, "fruits", "left", ".gitignore" ]) hugeGitignore - FS.writeTextFile (Path.concat [ p, "fruits", "right", ".gitignore" ]) hugeGitignore + FS.writeTextFile (p ".gitignore") hugeGitignore + FS.writeTextFile (p "fruits" ".gitignore") hugeGitignore + FS.writeTextFile (p "fruits" "left" ".gitignore") hugeGitignore + FS.writeTextFile (p "fruits" "right" ".gitignore") hugeGitignore a <- glob p [ "fruits/**/apple" ] - Array.sort a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] + sortedPaths a `Assert.shouldEqual` [ "fruits/left/apple", "fruits/right/apple" ] Spec.it "does respect .gitignore even though it might conflict with a search path without base" $ \p -> do - FS.writeTextFile (Path.concat [ p, ".gitignore" ]) "fruits" + FS.writeTextFile (p ".gitignore") "fruits" a <- glob p [ "**/apple" ] - Array.sort a `Assert.shouldEqual` [] + sortedPaths a `Assert.shouldEqual` [] + + where + sortedPaths = map (Path.localPart <<< Path.withForwardSlashes) >>> Array.sort diff --git a/test/Spago/Init.purs b/test/Spago/Init.purs index 977ff68a1..f9aa267e3 100644 --- a/test/Spago/Init.purs +++ b/test/Spago/Init.purs @@ -2,7 +2,6 @@ module Test.Spago.Init where import Test.Prelude -import Node.Path as Path import Spago.FS as FS import Test.Spago.InitSubpackage as Subpackage import Test.Spec (Spec) @@ -16,15 +15,15 @@ spec = Spec.around withTempDir do Spec.it "sets up a project" \{ spago } -> do spago [ "init" ] >>= shouldBeSuccess - Spec.it "does not overwrite files when initing a project" \{ spago } -> do - FS.mkdirp "src" - FS.writeTextFile (Path.concat [ "src", "Main.purs" ]) "Something" + Spec.it "does not overwrite files when initing a project" \{ spago, testCwd } -> do + FS.mkdirp (testCwd "src") + FS.writeTextFile (testCwd "src" "Main.purs") "Something" spago [ "init" ] >>= shouldBeSuccess - fileContent <- FS.readTextFile (Path.concat [ "src", "Main.purs" ]) + fileContent <- FS.readTextFile (testCwd "src" "Main.purs") fileContent `Assert.shouldEqual` "Something" - Spec.it "should use user-specified tag if it exists instead of latest release" \({ spago, fixture } :: TestDirs) -> do + Spec.it "should use user-specified tag if it exists instead of latest release" \{ spago, fixture, testCwd } -> do spago [ "init", "--package-set", "9.0.0", "--name", "7368613235362d47665357393342584955783641314b70674c" ] >>= shouldBeSuccess - checkFixture "spago.yaml" (fixture "older-package-set-tag.yaml") + checkFixture (testCwd "spago.yaml") (fixture "older-package-set-tag.yaml") Subpackage.spec diff --git a/test/Spago/InitSubpackage.purs b/test/Spago/InitSubpackage.purs index 567b4dae1..1cf7befd3 100644 --- a/test/Spago/InitSubpackage.purs +++ b/test/Spago/InitSubpackage.purs @@ -4,8 +4,8 @@ import Test.Prelude import Data.String.Regex as Regex import Data.String.Regex.Flags as RF -import Node.Path as Path import Spago.FS as FS +import Spago.Path (withForwardSlashes') import Test.Spago.Cli (sanitizeCliHelpOutput) import Test.Spec (SpecT) import Test.Spec as Spec @@ -13,32 +13,32 @@ import Test.Spec as Spec spec :: SpecT Aff TestDirs Identity Unit spec = Spec.describe "subpackage" do - Spec.it "sets up a sub-project in a subdirectory" \{ spago, fixture } -> do + Spec.it "sets up a sub-project in a subdirectory" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "init", "--subpackage", "subdir" ] >>= shouldBeSuccess - checkFixture "subdir/spago.yaml" (fixture "init/subpackage/subdir-spago.yaml") + checkFixture (testCwd "subdir" "spago.yaml") (fixture "init/subpackage/subdir-spago.yaml") spago [ "init", "--subpackage", "subdir2" ] >>= shouldBeSuccess - checkFixture "subdir2/spago.yaml" (fixture "init/subpackage/subdir2-spago.yaml") + checkFixture (testCwd "subdir2" "spago.yaml") (fixture "init/subpackage/subdir2-spago.yaml") - Spec.it "does not overwrite existing files" \{ spago, fixture } -> do + Spec.it "does not overwrite existing files" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subdir/src" - FS.writeTextFile (Path.concat [ "subdir", "src", "Main.purs" ]) "Something" + FS.mkdirp $ testCwd "subdir" "src" + FS.writeTextFile (testCwd "subdir" "src" "Main.purs") "Something" spago [ "init", "--subpackage", "subdir" ] >>= shouldBeSuccessErr' (fixture "init/subpackage/existing-src-file.txt") - fileContent <- FS.readTextFile (Path.concat [ "subdir", "src", "Main.purs" ]) + fileContent <- FS.readTextFile (testCwd "subdir" "src" "Main.purs") fileContent `shouldEqualStr` "Something" - Spec.it "warns when --package-set or --use-solver flags are used" \{ spago, fixture } -> do + Spec.it "warns when --package-set or --use-solver flags are used" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "init", "--package-set", "9.0.0", "--subpackage", "subdir" ] >>= shouldBeSuccessErr' (fixture "init/subpackage/package-set-solver-warning.txt") - checkFixture "subdir/spago.yaml" (fixture "init/subpackage/subdir-spago.yaml") + checkFixture (testCwd "subdir" "spago.yaml") (fixture "init/subpackage/subdir-spago.yaml") spago [ "init", "--use-solver", "--subpackage", "subdir" ] >>= shouldBeSuccessErr' (fixture "init/subpackage/package-set-solver-warning-existing-files.txt") - checkFixture "subdir/spago.yaml" (fixture "init/subpackage/subdir-spago.yaml") + checkFixture (testCwd "subdir" "spago.yaml") (fixture "init/subpackage/subdir-spago.yaml") Spec.it "does not allow both --name and --subpackage flags" \{ spago, fixture } -> do spago [ "init", "--name", "foo", "--subpackage", "bar" ] @@ -55,7 +55,7 @@ spec = , result , sanitize: sanitizeCliHelpOutput - >>> withForwardSlashes + >>> withForwardSlashes' >>> Regex.replace versionsRegex "Found PureScript a.b.c, will use package set x.y.z" } diff --git a/test/Spago/Install.purs b/test/Spago/Install.purs index 4e310c631..c911d44da 100644 --- a/test/Spago/Install.purs +++ b/test/Spago/Install.purs @@ -5,13 +5,12 @@ import Test.Prelude import Data.Array as Array import Data.Map as Map import Effect.Now as Now -import Node.FS.Aff as FSA -import Node.Path as Path import Registry.Version as Version import Spago.Command.Init as Init import Spago.Core.Config as Config import Spago.FS as FS import Spago.Log (LogVerbosity(..)) +import Spago.Path as Path import Spago.Paths as Paths import Spago.Purs as Purs import Test.Spec (Spec) @@ -36,19 +35,19 @@ spec = Spec.around withTempDir do -- -- dep added without "purescript-" prefix -- checkFixture "spago.yaml" (fixture "spago-strips-purescript.yaml") - Spec.it "adds dependencies to the config file" \{ spago, fixture } -> do + Spec.it "adds dependencies to the config file" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "29.3.0" ] >>= shouldBeSuccess spago [ "install", "foreign" ] >>= shouldBeSuccess - checkFixture "spago.yaml" (fixture "spago-install-success.yaml") + checkFixture (testCwd "spago.yaml") (fixture "spago-install-success.yaml") - Spec.it "adds test dependencies to the config file" \{ spago, fixture } -> do + Spec.it "adds test dependencies to the config file" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "29.3.0" ] >>= shouldBeSuccess spago [ "install", "--test-deps", "foreign" ] >>= shouldBeSuccess - checkFixture "spago.yaml" (fixture "spago-install-test-deps-success.yaml") + checkFixture (testCwd "spago.yaml") (fixture "spago-install-test-deps-success.yaml") - Spec.it "adds test dependencies to the config file when the test section does not exist" \{ spago, fixture } -> do + Spec.it "adds test dependencies to the config file when the test section does not exist" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "29.3.0" ] >>= shouldBeSuccess - let spagoYaml = "spago.yaml" + let spagoYaml = testCwd "spago.yaml" FS.unlink spagoYaml FS.copyFile { src: fixture "no-test-section.yaml" @@ -57,12 +56,12 @@ spec = Spec.around withTempDir do spago [ "install", "--test-deps", "foreign" ] >>= shouldBeSuccess checkFixture spagoYaml (fixture "spago-install-test-deps-success.yaml") - Spec.it "can't add dependencies that are not in the package set" \{ spago, fixture } -> do + Spec.it "can't add dependencies that are not in the package set" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaaa", "--package-set", "29.3.0" ] >>= shouldBeSuccess spago [ "install", "foo-foo-foo", "bar-bar-bar", "effcet", "arrys" ] >>= shouldBeFailureErr (fixture "missing-dependencies.txt") - checkFixture "spago.yaml" (fixture "spago-install-failure.yaml") + checkFixture (testCwd "spago.yaml") (fixture "spago-install-failure.yaml") - Spec.it "does not allow circular dependencies" \{ spago, fixture } -> do + Spec.it "does not allow circular dependencies" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let conf = Init.defaultConfig @@ -72,7 +71,7 @@ spec = Spec.around withTempDir do } , testModuleName: "Test.Main" } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") ( conf { workspace = conf.workspace # map ( _ @@ -96,14 +95,14 @@ spec = Spec.around withTempDir do ) spago [ "install", "a", "b" ] >>= shouldBeFailureErr (fixture "circular-dependencies.txt") - Spec.it "installs a package in the set from a commit hash" \{ spago } -> do + Spec.it "installs a package in the set from a commit hash" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - writeConfigWithEither + writeConfigWithEither testCwd spago [ "install", "either" ] >>= shouldBeSuccess - Spec.it "can't install (uncached) dependencies if offline" \{ spago, fixture } -> do + Spec.it "can't install (uncached) dependencies if offline" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - writeConfigWithEither + writeConfigWithEither testCwd spago [ "install", "--offline", "either" ] >>= shouldBeFailureErr (fixture "offline.txt") Spec.it "installs a package version by branch name with / in it" \{ spago, testCwd } -> do @@ -116,7 +115,7 @@ spec = Spec.around withTempDir do } , testModuleName: "Test.Main" } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") ( conf { workspace = conf.workspace # map ( _ @@ -133,14 +132,14 @@ spec = Spec.around withTempDir do } ) spago [ "install", "nonexistent-package" ] >>= shouldBeSuccess - let slashyPath = Path.concat [ Paths.toLocalCachePackagesPath testCwd, "nonexistent-package", "spago-test%2fbranch-with-slash" ] + let slashyPath = testCwd Paths.localCachePackagesPath "nonexistent-package" "spago-test%2fbranch-with-slash" unlessM (FS.exists slashyPath) do - Assertions.fail $ "Expected path to exist: " <> slashyPath - kids <- FSA.readdir slashyPath + Assertions.fail $ "Expected path to exist: " <> Path.quote slashyPath + kids <- FS.ls slashyPath when (Array.length kids == 0) do - Assertions.fail $ "Expected path exists but contains nothing: " <> slashyPath + Assertions.fail $ "Expected path exists but contains nothing: " <> Path.quote slashyPath - Spec.it "installs a package not in the set from a commit hash" \{ spago } -> do + Spec.it "installs a package not in the set from a commit hash" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let conf = Init.defaultConfig @@ -150,7 +149,7 @@ spec = Spec.around withTempDir do } , testModuleName: "Test.Main" } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") ( conf { workspace = conf.workspace # map ( _ @@ -168,7 +167,7 @@ spec = Spec.around withTempDir do ) spago [ "install", "spago" ] >>= shouldBeSuccess - Spec.it "can't install a package from a not-existing commit hash" \{ spago } -> do + Spec.it "can't install a package from a not-existing commit hash" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess let conf = Init.defaultConfig @@ -178,7 +177,7 @@ spec = Spec.around withTempDir do } , testModuleName: "Test.Main" } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") ( conf { workspace = conf.workspace # map ( _ @@ -196,13 +195,14 @@ spec = Spec.around withTempDir do ) spago [ "install", "spago" ] >>= shouldBeFailure - Spec.it "can update dependencies in a sub-package" \{ spago, fixture } -> do + Spec.it "can update dependencies in a sub-package" \{ spago, fixture, testCwd } -> do + let subpackage = testCwd "subpackage" spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile "subpackage/test/Main.purs" (Init.testMainTemplate "Subpackage.Test.Main") - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src" "Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (subpackage "test" "Main.purs") (Init.testMainTemplate "Subpackage.Test.Main") + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( Init.defaultConfig { name: mkPackageName "subpackage" , withWorkspace: Nothing @@ -210,7 +210,7 @@ spec = Spec.around withTempDir do } ) spago [ "install", "-p", "subpackage", "either" ] >>= shouldBeSuccess - checkFixture "subpackage/spago.yaml" (fixture "spago-subpackage-install-success.yaml") + checkFixture (subpackage "spago.yaml") (fixture "spago-subpackage-install-success.yaml") Spec.it "can build with a newer (but still compatible) compiler than the one in the package set" \{ spago } -> do spago [ "init", "--package-set", "10.0.0" ] >>= shouldBeSuccess @@ -222,20 +222,20 @@ spec = Spec.around withTempDir do false -> Assert.fail $ "Expected purs version to be newer than 0.15.4, but it was " <> Version.print purs.version spago [ "install" ] >>= shouldBeSuccess - Spec.it "can refresh the lockfile, and uninstall restores it" \{ spago, fixture } -> do + Spec.it "can refresh the lockfile, and uninstall restores it" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "33.0.0" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess -- Check that we have written the lockfile - checkFixture "spago.lock" (fixture "spago.lock") + checkFixture (testCwd "spago.lock") (fixture "spago.lock") spago [ "install", "maybe" ] >>= shouldBeSuccess -- Check that the new lockfile includes maybe - checkFixture "spago.lock" (fixture "spago-with-maybe.lock") + checkFixture (testCwd "spago.lock") (fixture "spago-with-maybe.lock") spago [ "uninstall", "maybe" ] >>= shouldBeSuccess -- Check that the lockfile is back to the original - checkFixture "spago.lock" (fixture "spago.lock") + checkFixture (testCwd "spago.lock") (fixture "spago.lock") -writeConfigWithEither :: Aff Unit -writeConfigWithEither = do +writeConfigWithEither :: RootPath -> Aff Unit +writeConfigWithEither root = do -- The commit for `either` is for the `v6.1.0` release let conf = Init.defaultConfig @@ -245,7 +245,7 @@ writeConfigWithEither = do } , testModuleName: "Test.Main" } - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (root "spago.yaml") ( conf { workspace = conf.workspace # map ( _ diff --git a/test/Spago/Lock.purs b/test/Spago/Lock.purs index de09ed934..98a594ca6 100644 --- a/test/Spago/Lock.purs +++ b/test/Spago/Lock.purs @@ -29,10 +29,10 @@ spec = Spec.around withTempDir do Right _ -> pure unit - Spec.it "#1158: always uses forward slash separator for doubly nested projects' paths" \{ spago, fixture } -> do - FS.copyTree { src: fixture "lock/1158-doubly-nested-projects", dst: "." } + Spec.it "#1158: always uses forward slash separator for doubly nested projects' paths" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "lock/1158-doubly-nested-projects", dst: testCwd "." } spago [ "install" ] >>= shouldBeSuccess - checkFixture "spago.lock" (fixture "lock/1158-doubly-nested-projects/expected-lockfile.txt") + checkFixture (testCwd "spago.lock") (fixture "lock/1158-doubly-nested-projects/expected-lockfile.txt") validLockfile :: Lockfile validLockfile = diff --git a/test/Spago/Ls.purs b/test/Spago/Ls.purs index 4133f83b1..d7e8595f8 100644 --- a/test/Spago/Ls.purs +++ b/test/Spago/Ls.purs @@ -16,9 +16,9 @@ spec = Spec.around withTempDir do spago [ "init" ] >>= shouldBeSuccess spago [ "ls", "deps" ] >>= shouldBeSuccessOutput (fixture "list-dependencies.txt") - Spec.it "direct dependencies in JSON, and requires selecting a package when many are present" \{ spago, fixture } -> do + Spec.it "direct dependencies in JSON, and requires selecting a package when many are present" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa" ] >>= shouldBeSuccess - makeSubpackage + makeSubpackage testCwd spago [ "install", "-p", "aaa", "aaa2" ] >>= shouldBeSuccess spago [ "ls", "deps", "-p", "aaa", "--json" ] >>= shouldBeSuccessOutput (fixture "list-dependencies.json") @@ -26,9 +26,9 @@ spec = Spec.around withTempDir do spago [ "init", "--name", "aaa", "--package-set", "41.2.0" ] >>= shouldBeSuccess spago [ "ls", "packages" ] >>= shouldBeSuccessOutput (fixture "list-packages.txt") - Spec.it "package set in JSON" \{ spago, fixture } -> do + Spec.it "package set in JSON" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "41.2.0" ] >>= shouldBeSuccess - makeSubpackage + makeSubpackage testCwd spago [ "install", "-p", "aaa", "aaa2" ] >>= shouldBeSuccess spago [ "ls", "packages", "--json" ] >>= shouldBeSuccessOutput (fixture "list-packages.json") @@ -36,13 +36,14 @@ spec = Spec.around withTempDir do spago [ "init", "--name", "aaa", "--use-solver" ] >>= shouldBeSuccess spago [ "ls", "packages" ] >>= shouldBeFailureErr (fixture "list-packages-registry.txt") -makeSubpackage :: Aff Unit -makeSubpackage = do - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile "subpackage/test/Main.purs" (Init.testMainTemplate "Subpackage.Test.Main") - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" +makeSubpackage :: RootPath -> Aff Unit +makeSubpackage root = do + let subpackage = root "subpackage" + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src" "Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (subpackage "test" "Main.purs") (Init.testMainTemplate "Subpackage.Test.Main") + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( Init.defaultConfig { name: mkPackageName "aaa2" , withWorkspace: Nothing diff --git a/test/Spago/Publish.purs b/test/Spago/Publish.purs index 1fb5b27e7..b6f15cf00 100644 --- a/test/Spago/Publish.purs +++ b/test/Spago/Publish.purs @@ -9,6 +9,7 @@ import Node.Platform as Platform import Node.Process as Process import Spago.Cmd as Cmd import Spago.FS as FS +import Spago.Path as Path import Test.Spec (Spec) import Test.Spec as Spec @@ -27,51 +28,51 @@ spec = Spec.around withTempDir do spago [ "fetch", "--ensure-ranges" ] >>= shouldBeSuccess spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/no-config.txt") - Spec.it "fails if the git tree is not clean" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } - FS.mkdirp "src" - FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } + Spec.it "fails if the git tree is not clean" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } + FS.mkdirp $ testCwd "src" + FS.copyFile { src: fixture "publish/basic.purs", dst: testCwd "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/no-git.txt") - Spec.it "fails if the module is called Main" \{ spago, fixture } -> do + Spec.it "fails if the module is called Main" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess - FS.unlink "spago.yaml" - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } + FS.unlink $ testCwd "spago.yaml" + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "publish", "--offline" ] >>= shouldBeFailureErr case Process.platform of Just Platform.Win32 -> fixture "publish/main-win.txt" _ -> fixture "publish/main.txt" - Spec.it "fails if the publish repo location is not among git remotes" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } - FS.mkdirp "src" - FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } + Spec.it "fails if the publish repo location is not among git remotes" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } + FS.mkdirp $ testCwd "src" + FS.copyFile { src: fixture "publish/basic.purs", dst: testCwd "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing git [ "remote", "set-url", "origin", "git@github.com:purescript/bbb.git" ] -- TODO check this is a Right? spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/invalid-location.txt") - Spec.it "fails if a core dependency is not in the registry" \{ spago, fixture } -> do - FS.copyTree { src: fixture "publish/extra-package-core", dst: "." } + Spec.it "fails if a core dependency is not in the registry" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "publish/extra-package-core", dst: testCwd } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "fetch" ] >>= shouldBeSuccess spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/extra-package-core-dependency.txt") - Spec.it "can get a package ready to publish" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } - FS.mkdirp "src" - FS.copyFile { src: fixture "publish/basic.purs", dst: "src/Main.purs" } + Spec.it "can get a package ready to publish" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } + FS.mkdirp $ testCwd "src" + FS.copyFile { src: fixture "publish/basic.purs", dst: testCwd "src/Main.purs" } spago [ "build" ] >>= shouldBeSuccess doTheGitThing -- It will fail because it can't hit the registry, but the fixture will check that everything else is ready spago [ "fetch" ] >>= shouldBeSuccess spago [ "publish", "--offline" ] >>= shouldBeFailureErr (fixture "publish/ready.txt") - Spec.it "allows to publish with a test dependency not in the registry" \{ spago, fixture } -> do - FS.copyTree { src: fixture "publish/extra-package-test", dst: "." } + Spec.it "allows to publish with a test dependency not in the registry" \{ spago, fixture, testCwd } -> do + FS.copyTree { src: fixture "publish/extra-package-test", dst: testCwd } spago [ "build" ] >>= shouldBeSuccess doTheGitThing spago [ "fetch" ] >>= shouldBeSuccess @@ -81,129 +82,129 @@ spec = Spec.around withTempDir do Spec.it "fails if the publish config is not specified" \{ spago, fixture } -> do spago [ "init", "--name", "aaaa" ] >>= shouldBeSuccess - spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-publish-config.txt") + spago [ "registry", "transfer", "--offline", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-publish-config.txt") - Spec.it "fails if the config does not specify an owner" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } + Spec.it "fails if the config does not specify an owner" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } spago [ "build" ] >>= shouldBeSuccess - spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-owner.txt") + spago [ "registry", "transfer", "--offline", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-owner.txt") - Spec.it "fails if the git tree is not clean" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } - spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess - spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-git.txt") + Spec.it "fails if the git tree is not clean" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } + spago [ "auth", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeSuccess + spago [ "registry", "transfer", "--offline", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-git.txt") - Spec.it "fails if the package has never been published before" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/basic.yaml", dst: "spago.yaml" } - spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + Spec.it "fails if the package has never been published before" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/basic.yaml", dst: testCwd "spago.yaml" } + spago [ "auth", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeSuccess doTheGitThing - spago [ "registry", "transfer", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/never-published.txt") + spago [ "registry", "transfer", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/never-published.txt") - Spec.it "fails if the new repo location is the same as the current one in the registry" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/transfer/aff.yaml", dst: "spago.yaml" } - spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + Spec.it "fails if the new repo location is the same as the current one in the registry" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/transfer/aff.yaml", dst: testCwd "spago.yaml" } + spago [ "auth", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeSuccess doTheGitThing - spago [ "registry", "transfer", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/same-location.txt") + spago [ "registry", "transfer", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/same-location.txt") - Spec.it "fails if can't find the private key" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: "spago.yaml" } - spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + Spec.it "fails if can't find the private key" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: testCwd "spago.yaml" } + spago [ "auth", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeSuccess doTheGitThing - spago [ "registry", "transfer", "-i", (fixture "publish/no-key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-key.txt") + spago [ "registry", "transfer", "-i", (Path.toRaw $ fixture "publish/no-key") ] >>= shouldBeFailureErr (fixture "publish/transfer/no-key.txt") - Spec.it "fails if running with --offline" \{ spago, fixture } -> do - FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: "spago.yaml" } - spago [ "auth", "-i", (fixture "publish/key") ] >>= shouldBeSuccess + Spec.it "fails if running with --offline" \{ spago, fixture, testCwd } -> do + FS.copyFile { src: fixture "publish/transfer/aff-new-location.yaml", dst: testCwd "spago.yaml" } + spago [ "auth", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeSuccess doTheGitThing - spago [ "registry", "transfer", "--offline", "-i", (fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/offline.txt") - - Spec.it "#1110 installs versions of packages that are returned by the registry solver, but not present in cache" \{ spago, fixture } -> do - let - shouldBeFailureErr' file = checkOutputs' - { stdoutFile: Nothing - , stderrFile: Just file - , result: isLeft - , sanitize: - String.trim - >>> withForwardSlashes - >>> String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n") - >>> Regex.replace buildOrderRegex "[x of 3] Compiling module-name" - } - - -- We have to ignore lines like "[1 of 3] Compiling Effect.Console" when - -- comparing output, because the compiler will always compile in - -- different order, depending on how the system resources happened to - -- align at the moment of the test run. - buildOrderRegex = unsafeFromRight $ Regex.regex - "\\[\\d of 3\\] Compiling (Effect\\.Console|Effect\\.Class\\.Console|Lib)" - RF.global - - FS.copyTree { src: fixture "publish/1110-solver-different-version", dst: "." } - spago [ "build" ] >>= shouldBeSuccess - doTheGitThing - spago [ "fetch" ] >>= shouldBeSuccess - - -- The local `spago.yaml` specifies `console: 6.0.0` in `extraPackages`, - -- so that's what should be in local cache after running `fetch`. - -- Importantly, `console-6.1.0` should not be there yet. - FS.exists ".spago/p/console-6.0.0" >>= (_ `shouldEqual` true) - FS.exists ".spago/p/console-6.1.0" >>= (_ `shouldEqual` false) - - spago [ "publish", "--offline" ] >>= shouldBeFailureErr' (fixture "publish/1110-solver-different-version/expected-stderr.txt") - - -- When `publish` runs, it uses the registry solver, which returns - -- `console-6.1.0` version, so `publish` should fetch that into local - -- cache and build with it. - FS.exists ".spago/p/console-6.1.0" >>= (_ `shouldEqual` true) - - -- Now screw up the `console-6.1.0` package in the local cache, so that it - -- doesn't compile anymore, and check that the relevant compile error - -- happens on publish. - FS.unlink ".spago/p/console-6.1.0/src/Effect/Console.js" - rmRf ".spago/p/console-6.1.0/output" - spago [ "publish", "--offline" ] >>= shouldBeFailureErr' (fixture "publish/1110-solver-different-version/failure-stderr.txt") - - Spec.describe "#1060 auto-filling the `publish.location` field" do - let - prepareProject spago fixture = do - FS.copyTree { src: fixture "publish/1060-autofill-location/project", dst: "." } - spago [ "build" ] >>= shouldBeSuccess - doTheGitThing - spago [ "fetch" ] >>= shouldBeSuccess - - Spec.it "happens for root package" \{ fixture, spago } -> do - prepareProject spago fixture - spago [ "publish", "-p", "aaa", "--offline" ] >>= - shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-root/expected-stderr.txt") - checkFixture "spago.yaml" - (fixture "publish/1060-autofill-location/scenario-root/expected-spago.yaml") - - Spec.it "errors out for non-root package" \{ fixture, spago } -> do - prepareProject spago fixture - spago [ "publish", "-p", "bbb", "--offline" ] >>= - shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-subdir/expected-stderr.txt") - - Spec.it "errors out for nested non-root package" \{ fixture, spago } -> do - prepareProject spago fixture - spago [ "publish", "-p", "ccc", "--offline" ] >>= - shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-nested-subdir/expected-stderr.txt") - - Spec.it "errors out when not a GitHub remote" \{ fixture, spago } -> do - prepareProject spago fixture - git [ "remote", "set-url", "origin", "https://not.git-hub.net/foo/bar.git" ] - spago [ "publish", "-p", "aaa", "--offline" ] >>= - shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-non-github/expected-stderr.txt") - checkFixture "spago.yaml" - (fixture "publish/1060-autofill-location/scenario-non-github/expected-spago.yaml") - - Spec.it "prints error when no origin remote" \{ fixture, spago } -> do - prepareProject spago fixture - git [ "remote", "remove", "origin" ] - git [ "remote", "add", "upstream", "git@github.com:foo/bar.git" ] - spago [ "publish", "-p", "aaa", "--offline" ] >>= - shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-no-origin/expected-stderr.txt") - checkFixture "spago.yaml" - (fixture "publish/1060-autofill-location/project/spago.yaml") + spago [ "registry", "transfer", "--offline", "-i", (Path.toRaw $ fixture "publish/key") ] >>= shouldBeFailureErr (fixture "publish/transfer/offline.txt") + + Spec.it "#1110 installs versions of packages that are returned by the registry solver, but not present in cache" \{ spago, fixture, testCwd } -> do + let + shouldBeFailureErr' file = checkOutputs' + { stdoutFile: Nothing + , stderrFile: Just file + , result: isLeft + , sanitize: + String.trim + >>> String.replaceAll (String.Pattern "\\") (String.Replacement "/") + >>> String.replaceAll (String.Pattern "\r\n") (String.Replacement "\n") + >>> Regex.replace buildOrderRegex "[x of 3] Compiling module-name" + } + + -- We have to ignore lines like "[1 of 3] Compiling Effect.Console" when + -- comparing output, because the compiler will always compile in + -- different order, depending on how the system resources happened to + -- align at the moment of the test run. + buildOrderRegex = unsafeFromRight $ Regex.regex + "\\[\\d of 3\\] Compiling (Effect\\.Console|Effect\\.Class\\.Console|Lib)" + RF.global + + FS.copyTree { src: fixture "publish/1110-solver-different-version", dst: testCwd } + spago [ "build" ] >>= shouldBeSuccess + doTheGitThing + spago [ "fetch" ] >>= shouldBeSuccess + + -- The local `spago.yaml` specifies `console: 6.0.0` in `extraPackages`, + -- so that's what should be in local cache after running `fetch`. + -- Importantly, `console-6.1.0` should not be there yet. + FS.exists (testCwd ".spago/p/console-6.0.0") >>= (_ `shouldEqual` true) + FS.exists (testCwd ".spago/p/console-6.1.0") >>= (_ `shouldEqual` false) + + spago [ "publish", "--offline" ] >>= shouldBeFailureErr' (fixture "publish/1110-solver-different-version/expected-stderr.txt") + + -- When `publish` runs, it uses the registry solver, which returns + -- `console-6.1.0` version, so `publish` should fetch that into local + -- cache and build with it. + FS.exists (testCwd ".spago/p/console-6.1.0") >>= (_ `shouldEqual` true) + + -- Now screw up the `console-6.1.0` package in the local cache, so that it + -- doesn't compile anymore, and check that the relevant compile error + -- happens on publish. + FS.unlink $ testCwd ".spago/p/console-6.1.0/src/Effect/Console.js" + rmRf $ testCwd ".spago/p/console-6.1.0/output" + spago [ "publish", "--offline" ] >>= shouldBeFailureErr' (fixture "publish/1110-solver-different-version/failure-stderr.txt") + + Spec.describe "#1060 auto-filling the `publish.location` field" do + let + prepareProject spago fixture testCwd = do + FS.copyTree { src: fixture "publish/1060-autofill-location/project", dst: testCwd } + spago [ "build" ] >>= shouldBeSuccess + doTheGitThing + spago [ "fetch" ] >>= shouldBeSuccess + + Spec.it "happens for root package" \{ fixture, spago, testCwd } -> do + prepareProject spago fixture testCwd + spago [ "publish", "-p", "aaa", "--offline" ] >>= + shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-root/expected-stderr.txt") + checkFixture (testCwd "spago.yaml") + (fixture "publish/1060-autofill-location/scenario-root/expected-spago.yaml") + + Spec.it "errors out for non-root package" \{ fixture, spago, testCwd } -> do + prepareProject spago fixture testCwd + spago [ "publish", "-p", "bbb", "--offline" ] >>= + shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-subdir/expected-stderr.txt") + + Spec.it "errors out for nested non-root package" \{ fixture, spago, testCwd } -> do + prepareProject spago fixture testCwd + spago [ "publish", "-p", "ccc", "--offline" ] >>= + shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-nested-subdir/expected-stderr.txt") + + Spec.it "errors out when not a GitHub remote" \{ fixture, spago, testCwd } -> do + prepareProject spago fixture testCwd + git [ "remote", "set-url", "origin", "https://not.git-hub.net/foo/bar.git" ] + spago [ "publish", "-p", "aaa", "--offline" ] >>= + shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-non-github/expected-stderr.txt") + checkFixture (testCwd "spago.yaml") + (fixture "publish/1060-autofill-location/scenario-non-github/expected-spago.yaml") + + Spec.it "prints error when no origin remote" \{ fixture, spago, testCwd } -> do + prepareProject spago fixture testCwd + git [ "remote", "remove", "origin" ] + git [ "remote", "add", "upstream", "git@github.com:foo/bar.git" ] + spago [ "publish", "-p", "aaa", "--offline" ] >>= + shouldBeFailureErr (fixture "publish/1060-autofill-location/scenario-no-origin/expected-stderr.txt") + checkFixture (testCwd "spago.yaml") + (fixture "publish/1060-autofill-location/project/spago.yaml") doTheGitThing :: Aff Unit doTheGitThing = do @@ -220,8 +221,8 @@ doTheGitThing = do git :: Array String -> Aff Unit git = git' Nothing -git' :: Maybe FilePath -> Array String -> Aff Unit +git' :: Maybe GlobalPath -> Array String -> Aff Unit git' cwd args = - Cmd.exec "git" args + Cmd.exec (Path.global "git") args (Cmd.defaultExecOptions { pipeStdout = false, pipeStderr = false, pipeStdin = StdinNewPipe, cwd = cwd }) >>= shouldBeSuccess diff --git a/test/Spago/Repl.purs b/test/Spago/Repl.purs index e621ceedf..dbe1cfba3 100644 --- a/test/Spago/Repl.purs +++ b/test/Spago/Repl.purs @@ -10,16 +10,16 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "repl" do - Spec.it "writes .purs-repl if not there" \{ spago, spago' } -> do + Spec.it "writes .purs-repl if not there" \{ spago, spago', testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - FS.readTextFile ".purs-repl" >>= shouldEqual "import Prelude\n" + FS.readTextFile (testCwd ".purs-repl") >>= shouldEqual "import Prelude\n" - FS.unlink ".purs-repl" + FS.unlink $ testCwd ".purs-repl" spago' (StdinWrite ":q") [ "repl" ] >>= shouldBeSuccess - FS.readTextFile ".purs-repl" >>= shouldEqual "import Prelude\n" + FS.readTextFile (testCwd ".purs-repl") >>= shouldEqual "import Prelude\n" - Spec.it "does not write .purs-repl if already there" \{ spago, spago' } -> do + Spec.it "does not write .purs-repl if already there" \{ spago, spago', testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - FS.writeTextFile ".purs-repl" "import Data.Maybe\n" + FS.writeTextFile (testCwd ".purs-repl") "import Data.Maybe\n" spago' (StdinWrite ":q") [ "repl" ] >>= shouldBeSuccess - FS.readTextFile ".purs-repl" >>= shouldEqual "import Data.Maybe\n" + FS.readTextFile (testCwd ".purs-repl") >>= shouldEqual "import Data.Maybe\n" diff --git a/test/Spago/Run.purs b/test/Spago/Run.purs index 663fa2ff0..65c55ad07 100644 --- a/test/Spago/Run.purs +++ b/test/Spago/Run.purs @@ -6,11 +6,6 @@ import Spago.FS as FS import Test.Spec (Spec) import Test.Spec as Spec -cp :: forall m. MonadAff m => String -> String -> m Unit -cp from to = do - str <- FS.readTextFile from - FS.writeTextFile to str - spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "run" do @@ -20,32 +15,32 @@ spec = Spec.around withTempDir do spago [ "build" ] >>= shouldBeSuccess spago [ "run" ] >>= shouldBeSuccessOutput (fixture "run-output.txt") - Spec.it "can pass stdin to the application" \{ spago, spago', fixture } -> do + Spec.it "can pass stdin to the application" \{ spago, spago', fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - cp (fixture "spago-run-stdin.purs") "src/Main.purs" + FS.copyTree { src: fixture "spago-run-stdin.purs", dst: testCwd "src" "Main.purs" } spago [ "install", "node-buffer", "node-streams", "node-process", "node-event-emitter" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago' (StdinWrite "wut") [ "run" ] >>= shouldBeSuccessOutput (fixture "run-passthrough.txt") - Spec.it "can pass arguments to the application" \{ spago, fixture } -> do + Spec.it "can pass arguments to the application" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - cp (fixture "spago-run-args.purs") "src/Main.purs" + FS.copyTree { src: fixture "spago-run-args.purs", dst: testCwd "src" "Main.purs" } spago [ "install", "node-process", "arrays" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "run", "hello" , "world" ] >>= shouldBeSuccessOutput (fixture "run-args-output.txt") - Spec.it "args in spago.yaml should be used as the fallback args" \{ spago, fixture } -> do + Spec.it "args in spago.yaml should be used as the fallback args" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - cp (fixture "spago-run-args.purs") "src/Main.purs" - cp (fixture "spago-args.yaml") "spago.yaml" + FS.copyTree { src: fixture "spago-run-args.purs", dst: testCwd "src" "Main.purs" } + FS.copyTree { src: fixture "spago-args.yaml", dst: testCwd "spago.yaml" } spago [ "install", "node-process", "arrays" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "run" ] >>= shouldBeSuccessOutput (fixture "run-args-output.txt") - Spec.it "explicit args has more priority than args in spago.yaml" \{ spago, fixture } -> do + Spec.it "explicit args has more priority than args in spago.yaml" \{ spago, fixture, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess - cp (fixture "spago-args.yaml") "spago.yaml" - cp (fixture "spago-run-args.purs") "src/Main.purs" + FS.copyTree { src: fixture "spago-args.yaml", dst: testCwd "spago.yaml" } + FS.copyTree { src: fixture "spago-run-args.purs", dst: testCwd "src" "Main.purs" } spago [ "install", "node-process", "arrays" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess spago [ "run", "bye" , "world" ] >>= shouldBeSuccessOutput (fixture "run-args-output2.txt") diff --git a/test/Spago/Sources.purs b/test/Spago/Sources.purs index 9b0b0c9c4..0acb85c6c 100644 --- a/test/Spago/Sources.purs +++ b/test/Spago/Sources.purs @@ -20,13 +20,14 @@ spec = Spec.around withTempDir do Just Platform.Win32 -> fixture "sources-output.win.txt" _ -> fixture "sources-output.txt" - Spec.it "contains subproject sources when selecting a subproject" \{ spago, fixture } -> do + Spec.it "contains subproject sources when selecting a subproject" \{ spago, fixture, testCwd } -> do + let subpackage = testCwd "subpackage" spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile "subpackage/test/Main.purs" (Init.testMainTemplate "Subpackage.Test.Main") - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src/Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (subpackage "test/Main.purs") (Init.testMainTemplate "Subpackage.Test.Main") + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( Init.defaultConfig { name: mkPackageName "subpackage" , withWorkspace: Nothing diff --git a/test/Spago/Test.purs b/test/Spago/Test.purs index 37f4ce3f3..f518cb367 100644 --- a/test/Spago/Test.purs +++ b/test/Spago/Test.purs @@ -4,7 +4,6 @@ import Test.Prelude import Data.Array as Array import Data.String as String -import Node.Path as Path import Node.Platform as Platform import Node.Process as Process import Registry.Version as Version @@ -12,7 +11,8 @@ import Spago.Command.Init (DefaultConfigOptions(..)) import Spago.Command.Init as Init import Spago.Core.Config as Config import Spago.FS as FS -import Spago.Paths (paths) +import Spago.Path as Path +import Spago.Paths as Paths import Test.Spec (Spec) import Test.Spec as Spec import Test.Spec.Assertions as Assert @@ -29,23 +29,24 @@ spec = Spec.around withTempDir do Spec.it "tests successfully when using a different output dir" \{ spago, fixture } -> do spago [ "init", "--name", "7368613235362d6a336156536c675a7033334e7659556c6d38" ] >>= shouldBeSuccess - let tempDir = Path.concat [ paths.temp, "output" ] + let tempDir = Path.toRaw $ Paths.paths.temp "output" spago [ "build", "--output", tempDir ] >>= shouldBeSuccess spago [ "test", "--output", tempDir ] >>= shouldBeSuccessOutputWithErr (fixture "test-output-stdout.txt") (fixture "test-output-stderr.txt") - Spec.it "fails nicely when the test module is not found" \{ spago, fixture } -> do + Spec.it "fails nicely when the test module is not found" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "7368613235362d6a336156536c675a7033334e7659556c6d38" ] >>= shouldBeSuccess spago [ "build" ] >>= shouldBeSuccess - FS.moveSync { dst: "test2", src: "test" } + FS.moveSync { dst: testCwd "test2", src: testCwd "test" } spago [ "test" ] >>= shouldBeFailureErr (fixture "test-missing-module.txt") - Spec.it "runs tests from a sub-package" \{ spago } -> do + Spec.it "runs tests from a sub-package" \{ spago, testCwd } -> do + let subpackage = testCwd "subpackage" spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile "subpackage/test/Main.purs" (Init.testMainTemplate "Subpackage.Test.Main") - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src/Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (subpackage "test/Main.purs") (Init.testMainTemplate "Subpackage.Test.Main") + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( Init.defaultConfig { name: mkPackageName "subpackage" , withWorkspace: Nothing @@ -54,23 +55,24 @@ spec = Spec.around withTempDir do ) spago [ "test", "-p", "subpackage" ] >>= shouldBeSuccess - Spec.it "runs tests from a sub-package in the current working directory, not the sub-package's directory" \{ spago, fixture } -> do + Spec.it "runs tests from a sub-package in the current working directory, not the sub-package's directory" \{ spago, fixture, testCwd } -> do + let subpackage = testCwd "subpackage" spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src" "Main.purs") (Init.srcMainTemplate "Subpackage.Main") -- We write a file into the current working directory. -- The subpackage test will read the given file without changing its directory -- and log its content as its output. - let textFilePath = "foo.txt" + let textFilePath = testCwd "foo.txt" let fileContent = "foo" FS.writeTextFile textFilePath fileContent FS.copyFile { src: fixture "spago-subpackage-test-cwd.purs" - , dst: "subpackage/test/Main.purs" + , dst: subpackage "test" "Main.purs" } - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( ( Init.defaultConfig { name: mkPackageName "subpackage" , withWorkspace: Nothing @@ -80,13 +82,14 @@ spec = Spec.around withTempDir do ) spago [ "test", "-p", "subpackage" ] >>= checkOutputsStr { stdoutStr: Just fileContent, stderrStr: Nothing, result: isRight } - Spec.it "fails when running tests from a sub-package, where the module does not exist" \{ spago } -> do + Spec.it "fails when running tests from a sub-package, where the module does not exist" \{ spago, testCwd } -> do + let subpackage = testCwd "subpackage" spago [ "init" ] >>= shouldBeSuccess - FS.mkdirp "subpackage/src" - FS.mkdirp "subpackage/test" - FS.writeTextFile "subpackage/src/Main.purs" (Init.srcMainTemplate "Subpackage.Main") - FS.writeTextFile "subpackage/test/Main.purs" (Init.testMainTemplate "Subpackage.Test.Main2") - FS.writeYamlFile Config.configCodec "subpackage/spago.yaml" + FS.mkdirp (subpackage "src") + FS.mkdirp (subpackage "test") + FS.writeTextFile (subpackage "src/Main.purs") (Init.srcMainTemplate "Subpackage.Main") + FS.writeTextFile (subpackage "test/Main.purs") (Init.testMainTemplate "Subpackage.Test.Main2") + FS.writeYamlFile Config.configCodec (subpackage "spago.yaml") ( Init.defaultConfig { name: mkPackageName "subpackage" , withWorkspace: Nothing @@ -95,15 +98,15 @@ spec = Spec.around withTempDir do ) spago [ "test", "-p", "subpackage" ] >>= shouldBeFailure - Spec.it "can use a custom output folder" \{ spago } -> do + Spec.it "can use a custom output folder" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess spago [ "test", "--output", "myOutput" ] >>= shouldBeSuccess - FS.exists "myOutput" `Assert.shouldReturn` true + FS.exists (testCwd "myOutput") `Assert.shouldReturn` true - Spec.it "'strict: true' on package src does not cause test code containing warnings to fail to build" \{ spago } -> do + Spec.it "'strict: true' on package src does not cause test code containing warnings to fail to build" \{ spago, testCwd } -> do spago [ "init" ] >>= shouldBeSuccess -- add --strict - FS.writeYamlFile Config.configCodec "spago.yaml" $ Init.defaultConfig' $ PackageAndWorkspace + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") $ Init.defaultConfig' $ PackageAndWorkspace { name: mkPackageName "package-a" , dependencies: [ "prelude", "effect", "console" ] , test: Just { moduleMain: "Test.Main", strict: Nothing, censorTestWarnings: Nothing, pedanticPackages: Nothing, dependencies: Nothing } @@ -112,7 +115,7 @@ spec = Spec.around withTempDir do { setVersion: Just $ unsafeFromRight $ Version.parse "0.0.1" } -- add version where test file has warning - FS.writeTextFile (Path.concat [ "test", "Test", "Main.purs" ]) $ Array.intercalate "\n" + FS.writeTextFile (testCwd "test" "Test" "Main.purs") $ Array.intercalate "\n" [ "module Test.Main where" , "" , "import Prelude" diff --git a/test/Spago/Uninstall.purs b/test/Spago/Uninstall.purs index 8c4170d78..f363e5ac1 100644 --- a/test/Spago/Uninstall.purs +++ b/test/Spago/Uninstall.purs @@ -3,7 +3,6 @@ module Test.Spago.Uninstall where import Test.Prelude import Data.String as String -import Node.Path as Path import Spago.Command.Init (DefaultConfigOptions(..)) import Spago.Command.Init as Init import Spago.Core.Config as Config @@ -16,18 +15,18 @@ spec ∷ Spec Unit spec = Spec.around withTempDir do Spec.describe "uninstall" do - Spec.it "fails when no package was selected" \{ spago, fixture } -> do + Spec.it "fails when no package was selected" \{ spago, fixture, testCwd } -> do let setupSubpackage packageName = do - let subdir = Path.concat [ packageName, "src" ] + let subdir = testCwd packageName "src" FS.mkdirp subdir - FS.writeTextFile (Path.concat [ subdir, "Main.purs" ]) $ "module " <> String.toUpper packageName <> " where" - FS.writeYamlFile Config.configCodec (Path.concat [ packageName, "spago.yaml" ]) $ mkPackageOnlyConfig + FS.writeTextFile (subdir "Main.purs") $ "module " <> String.toUpper packageName <> " where" + FS.writeYamlFile Config.configCodec (testCwd packageName "spago.yaml") $ mkPackageOnlyConfig { packageName: packageName , srcDependencies: [] } [] - FS.writeYamlFile Config.configCodec "spago.yaml" + FS.writeYamlFile Config.configCodec (testCwd "spago.yaml") $ Init.defaultConfig' $ WorkspaceOnly { setVersion: Just $ mkVersion "0.0.1" } setupSubpackage "foo" @@ -35,9 +34,9 @@ spec = Spec.around withTempDir do spago [ "build" ] >>= shouldBeSuccess spago [ "uninstall" ] >>= shouldBeFailureErr (fixture "uninstall-no-package-selection.txt") - Spec.it "warns when test config does not exist and uninstalling test deps" \{ spago, fixture } -> do + Spec.it "warns when test config does not exist and uninstalling test deps" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "uninstall-tests" ] >>= shouldBeSuccess - editSpagoYaml' "spago.yaml" \config -> + editSpagoYaml' (testCwd "spago.yaml") \config -> config { package = config.package <#> \p -> p { test = Nothing } } spago [ "uninstall", "--test-deps", "either" ] >>= shouldBeSuccessErr (fixture "uninstall-no-test-config.txt") @@ -49,26 +48,26 @@ spec = Spec.around withTempDir do spago [ "init", "--name", "uninstall-tests" ] >>= shouldBeSuccess spago [ "uninstall", "--test-deps", "either" ] >>= shouldBeSuccessErr (fixture "uninstall-deps-undeclared-test-deps.txt") - Spec.it "removes declared packages in source config" \{ spago, fixture } -> do + Spec.it "removes declared packages in source config" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "uninstall-tests" ] >>= shouldBeSuccess - originalConfig <- FS.readTextFile "spago.yaml" + originalConfig <- FS.readTextFile (testCwd "spago.yaml") spago [ "install", "either" ] >>= shouldBeSuccess - postInstallConfig <- FS.readTextFile "spago.yaml" + postInstallConfig <- FS.readTextFile (testCwd "spago.yaml") originalConfig `Assert.shouldNotEqual` postInstallConfig spago [ "uninstall", "either" ] >>= shouldBeSuccessErr (fixture "uninstall-remove-src-deps.txt") - postUninstallConfig <- FS.readTextFile "spago.yaml" + postUninstallConfig <- FS.readTextFile (testCwd "spago.yaml") originalConfig `Assert.shouldEqual` postUninstallConfig - Spec.it "removes declared packages in test config" \{ spago, fixture } -> do + Spec.it "removes declared packages in test config" \{ spago, fixture, testCwd } -> do spago [ "init", "--name", "uninstall-tests" ] >>= shouldBeSuccess - originalConfig <- FS.readTextFile "spago.yaml" + originalConfig <- FS.readTextFile (testCwd "spago.yaml") spago [ "install", "--test-deps", "either" ] >>= shouldBeSuccess - postInstallConfig <- FS.readTextFile "spago.yaml" + postInstallConfig <- FS.readTextFile (testCwd "spago.yaml") originalConfig `Assert.shouldNotEqual` postInstallConfig spago [ "uninstall", "--test-deps", "either" ] >>= shouldBeSuccessErr (fixture "uninstall-remove-test-deps.txt") - postUninstallConfig <- FS.readTextFile "spago.yaml" + postUninstallConfig <- FS.readTextFile (testCwd "spago.yaml") originalConfig `Assert.shouldEqual` postUninstallConfig diff --git a/test/Spago/Unit.purs b/test/Spago/Unit.purs index 96648229c..065c5dd23 100644 --- a/test/Spago/Unit.purs +++ b/test/Spago/Unit.purs @@ -5,6 +5,7 @@ import Prelude import Test.Spago.Unit.CheckInjectivity as CheckInjectivity import Test.Spago.Unit.FindFlags as FindFlags import Test.Spago.Unit.Git as Git +import Test.Spago.Unit.Path as Path import Test.Spago.Unit.Printer as Printer import Test.Spec (Spec) import Test.Spec as Spec @@ -15,3 +16,4 @@ spec = Spec.describe "unit" do CheckInjectivity.spec Printer.spec Git.spec + Path.spec diff --git a/test/Spago/Unit/Path.purs b/test/Spago/Unit/Path.purs new file mode 100644 index 000000000..6e3deb3c3 --- /dev/null +++ b/test/Spago/Unit/Path.purs @@ -0,0 +1,66 @@ +module Test.Spago.Unit.Path where + +import Test.Prelude + +import Effect.Unsafe (unsafePerformEffect) +import Node.Platform (Platform(..)) as Node +import Node.Process (platform) as Node +import Spago.Path as Path +import Test.Spec (Spec) +import Test.Spec as Spec + +spec :: Spec Unit +spec = Spec.describe "Paths" do + + Spec.describe "RootPath" do + Spec.it "can append strings" do + (root "/foo" "bar") `shouldPointAt` "/foo/bar" + (root "/foo" "bar" "baz") `shouldPointAt` "/foo/bar/baz" + (root "/foo/x/y" "/bar" "baz") `shouldPointAt` "/bar/baz" + (root "/foo/x/y" "/foo/x/y/z") `shouldPointAt` "/foo/x/y/z" + + Spec.describe "LocalPath" do + Spec.it "can append strings" do + let p = Path.withForwardSlashes $ root "/foo" "bar" "baz" + Path.localPart p `shouldEqual` "bar/baz" + (p "x") `shouldPointAt` "/foo/bar/baz/x" + (p "../x") `shouldPointAt` "/foo/bar/x" + (p "../.." "x") `shouldPointAt` "/foo/x" + (p "x" "y" "z") `shouldPointAt` "/foo/bar/baz/x/y/z" + + Spec.it "always keeps the original root" do + let p1 = Path.withForwardSlashes $ root "/foo/x/y" "/bar" "baz" + Path.localPart p1 `shouldEqual` "../../../bar/baz" + Path.rootPart p1 `shouldPointAt` "/foo/x/y" + + let p2 = Path.withForwardSlashes $ root "/foo/x/y" "bar" "baz" + Path.localPart p2 `shouldEqual` "bar/baz" + Path.rootPart p2 `shouldPointAt` "/foo/x/y" + + let p3 = Path.withForwardSlashes $ root "/foo/x/y" "../../bar" "baz" + p3 `shouldPointAt` "/foo/bar/baz" + Path.localPart p3 `shouldEqual` "../../bar/baz" + Path.rootPart p3 `shouldPointAt` "/foo/x/y" + + Spec.describe "GlobalPath" do + Spec.it "can append strings" do + (Path.global "/foo" "bar") `shouldPointAt` "/foo/bar" + (Path.global "/foo" "bar" "baz") `shouldPointAt` "/foo/bar/baz" + (Path.global "/foo/x/y" "/bar" "baz") `shouldPointAt` "/bar/baz" + (Path.global "/foo/x/y" "/foo/x/y/z") `shouldPointAt` "/foo/x/y/z" + (Path.global "/foo/x/y" ".." ".." "bar") `shouldPointAt` "/foo/bar" + + when (Node.platform == Just Node.Win32) do + Spec.describe "On different drives under Windows" do + Spec.it "LocalPath appends correctly" do + (root "C:\\foo" "bar") `shouldPointAt` "C:/foo/bar" + (root "C:\\foo" "bar" "baz") `shouldPointAt` "C:/foo/bar/baz" + (root "C:\\foo\\x\\y" "D:\\bar" "baz") `shouldPointAt` "D:/bar/baz" + (root "C:\\foo\\x\\y" "D:\\foo\\x\\y\\z") `shouldPointAt` "D:/foo/x/y/z" + (root "C:\\foo\\x\\y" ".." ".." "bar") `shouldPointAt` "C:/foo/bar" + + where + root = unsafePerformEffect <<< Path.mkRoot <<< Path.global + + shouldPointAt :: ∀ path. Path.IsPath path => path -> String -> _ + shouldPointAt path raw = Path.toRaw (Path.withForwardSlashes $ Path.toGlobal path) `shouldEqual` raw diff --git a/test/Spago/Upgrade.purs b/test/Spago/Upgrade.purs index 6c6722159..3a797e68b 100644 --- a/test/Spago/Upgrade.purs +++ b/test/Spago/Upgrade.purs @@ -14,33 +14,35 @@ spec :: Spec Unit spec = Spec.around withTempDir do Spec.describe "upgrade" do - Spec.it "can upgrade a package set version" \{ spago } -> do + Spec.it "can upgrade a package set version" \{ spago, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "20.0.1" ] >>= shouldBeSuccess spago [ "upgrade" ] >>= shouldBeSuccess -- we can't just check a fixture here, as there are new package set versions all the time. -- so we read the config file, and check that the package set version is more recent than the one we started with - assertExpectedVersion + assertExpectedVersion testCwd { check: (_ > mkVersion "20.0.1") , error: "Could not upgrade the package set." } - Spec.it "allows to specify package set version" \{ spago } -> do + Spec.it "allows to specify package set version" \{ spago, testCwd } -> do spago [ "init", "--name", "aaa", "--package-set", "20.0.0" ] >>= shouldBeSuccess - assertExpectedVersion + assertExpectedVersion testCwd { check: (_ == mkVersion "20.0.0") , error: "Could not init with package set 20.0.0." } spago [ "upgrade", "--package-set", "20.0.1" ] >>= shouldBeSuccess - assertExpectedVersion + assertExpectedVersion testCwd { check: (_ == mkVersion "20.0.1") , error: "Could not upgrade the package set to 20.0.1." } where - assertExpectedVersion { check, error } = do + assertExpectedVersion root { check, error } = do startingTime <- liftEffect $ Now.now - maybeConfig <- runSpago { logOptions: { color: false, verbosity: LogQuiet, startingTime } } (Config.readConfig "spago.yaml") + maybeConfig <- runSpago + { logOptions: { color: false, verbosity: LogQuiet, startingTime }, rootPath: root } + (Config.readConfig $ root "spago.yaml") case maybeConfig of Right { yaml: { workspace: Just { packageSet: Just (SetFromRegistry { registry }) } } } | check registry -> pure unit Right { yaml: c } -> Assert.fail $ error <> " Config: " <> printJson Config.configCodec c