Skip to content

Commit c723832

Browse files
authored
Stricter path types for stricter path-related logic (purescript#1296)
1 parent 8b951f4 commit c723832

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+1629
-1165
lines changed

.github/workflows/build.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ jobs:
4545
env:
4646
cache-name: cache-node-modules
4747
with:
48-
path: ~/.npm
48+
path: |
49+
~/.npm
50+
$APPDATA/npm
51+
node_modules
4952
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
5053
restore-keys: |
5154
${{ runner.os }}-build-${{ env.cache-name }}-

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Other improvements:
4242
help catch typos in field names.
4343
- When the `publish.location` field is missing, `spago publish` will attempt to
4444
figure out the location from Git remotes and write it back to `spago.yaml`.
45+
- Internally Spago uses stricter-typed file paths.
4546

4647
## [0.21.0] - 2023-05-04
4748

CONTRIBUTING.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,50 @@ Learn by doing and get your hands dirty!
111111
[f-f]: https://github.com/f-f
112112
[discord]: https://purescript.org/chat
113113
[spago-issues]: https://github.com/purescript/spago/issues
114+
115+
## Working with file paths
116+
117+
File paths are very important in Spago. A very big chunk of Spago does is
118+
shuffling files around and manipulating their paths. Representing them as plain
119+
strings is not enough.
120+
121+
Spago has three different kinds of paths, represented as distinct types:
122+
123+
- `RootPath` can generally be the root of anything, but in practice it usually
124+
points to the root directory of the current workspace. It is constructed in
125+
`Main.purs` close to the entry point and is available in all `ReaderT`
126+
environments as `rootPath :: RootPath`
127+
- `LocalPath` is path of a particular file or directory within the workspace. It
128+
doesn't have to be literally _within_ the workspace directory - e.g. a custom
129+
dependency that lives somewhere on the local file system, - but it's still
130+
_relative_ to the workspace. A `LocalPath` is explicitly broken into two
131+
parts: a `RootPath` and the "local" part relative to the root. This is useful
132+
for printing out workspace-relative paths in user-facing output, while still
133+
retaining the full path for actual file operations. A `LocalPath` can be
134+
constructed by appending to a `RootPath`. Once so constructed, the `LocalPath`
135+
always retains the same root, no matter what subsequent manipulations are done
136+
to it. Therefore, if you have a `LocalPath` value, its root is probably
137+
pointing to the current workspace directory.
138+
- `GlobalPath` is for things that are not related to the current workspace.
139+
Examples include paths to executables, such as `node` and `purs`, and global
140+
directories, such as `registryPath` and `globalCachePath`.
141+
142+
Paths can be appended by using the `</>` operator. It is overloaded for all
143+
three path types and allows to append string segments to them. When appending to
144+
a `RootPath`, the result comes out as `LocalPath`. You cannot produce a new
145+
`RootPath` by appending.
146+
147+
Most code that deals with the workspace operates in `LocalPath` values. Most
148+
code that deals with external and global things operates in `GlobalPath` values.
149+
Lower-level primitives, such as in the `Spago.FS` module, are polymorphic and
150+
can take all three path types as parameters.
151+
152+
For example:
153+
154+
```haskell
155+
rootPath <- Path.mkRootPath =<< Paths.cwd
156+
config <- readConfig (rootPath </> "spago.yaml")
157+
let srcDir = rootPath </> "src"
158+
compileResult <- callCompiler [ srcDir </> "Main.purs", srcDir </> "Lib.purs" ]
159+
FS.writeFile (rootPath </> "result.json") (serialize compileResult)
160+
```

bin/src/Flags.purs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,15 +336,15 @@ depsOnly =
336336
<> O.help "Build depedencies only"
337337
)
338338

339-
publicKeyPath :: Parser FilePath
339+
publicKeyPath :: Parser RawFilePath
340340
publicKeyPath =
341341
O.strOption
342342
( O.short 'i'
343343
<> O.metavar "PUBLIC_KEY_PATH"
344344
<> O.help "Select the path of the public key to use for authenticating operations of the package"
345345
)
346346

347-
privateKeyPath :: Parser FilePath
347+
privateKeyPath :: Parser RawFilePath
348348
privateKeyPath =
349349
O.strOption
350350
( O.short 'i'

bin/src/Main.purs

Lines changed: 53 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import Data.Set as Set
1515
import Effect.Aff as Aff
1616
import Effect.Aff.AVar as AVar
1717
import Effect.Now as Now
18-
import Node.Process as Process
1918
import Options.Applicative (CommandFields, Mod, Parser, ParserPrefs(..))
2019
import Options.Applicative as O
2120
import Options.Applicative.Types (Backtracking(..))
@@ -52,6 +51,7 @@ import Spago.Generated.BuildInfo as BuildInfo
5251
import Spago.Git as Git
5352
import Spago.Json as Json
5453
import Spago.Log (LogVerbosity(..))
54+
import Spago.Path as Path
5555
import Spago.Paths as Paths
5656
import Spago.Purs as Purs
5757
import Spago.Registry as Registry
@@ -163,7 +163,7 @@ type BundleArgs =
163163
{ minify :: Boolean
164164
, sourceMaps :: Boolean
165165
, module :: Maybe String
166-
, outfile :: Maybe FilePath
166+
, outfile :: Maybe String
167167
, platform :: Maybe String
168168
, selectedPackage :: Maybe String
169169
, pursArgs :: List String
@@ -536,7 +536,8 @@ main = do
536536
\c -> Aff.launchAff_ case c of
537537
Cmd'SpagoCmd (SpagoCmd globalArgs@{ offline, migrateConfig } command) -> do
538538
logOptions <- mkLogOptions startingTime globalArgs
539-
runSpago { logOptions } case command of
539+
rootPath <- Path.mkRoot =<< Paths.cwd
540+
runSpago { logOptions, rootPath } case command of
540541
Sources args -> do
541542
{ env } <- mkFetchEnv
542543
{ packages: mempty
@@ -551,11 +552,9 @@ main = do
551552
void $ runSpago env (Sources.run { json: args.json })
552553
Init args@{ useSolver } -> do
553554
-- Fetch the registry here so we can select the right package set later
554-
env <- mkRegistryEnv offline
555-
555+
env <- mkRegistryEnv offline <#> Record.union { rootPath }
556556
setVersion <- parseSetVersion args.setVersion
557557
void $ runSpago env $ Init.run { mode: args.mode, setVersion, useSolver }
558-
559558
Fetch args -> do
560559
{ env, fetchOpts } <- mkFetchEnv (Record.merge { isRepl: false, migrateConfig, offline } args)
561560
void $ runSpago env (Fetch.run fetchOpts)
@@ -600,7 +599,7 @@ main = do
600599
void $ runSpago publishEnv (Publish.publish {})
601600

602601
Repl args@{ selectedPackage } -> do
603-
packages <- FS.exists "spago.yaml" >>= case _ of
602+
packages <- FS.exists (rootPath </> "spago.yaml") >>= case _ of
604603
true -> do
605604
-- if we have a config then we assume it's a workspace, and we can run a repl in the project
606605
pure mempty -- TODO newPackages
@@ -609,9 +608,10 @@ main = do
609608
logWarn "No configuration found, creating a temporary project to run a repl in..."
610609
tmpDir <- mkTemp
611610
FS.mkdirp tmpDir
612-
logDebug $ "Creating repl project in temp dir: " <> tmpDir
613-
liftEffect $ Process.chdir tmpDir
614-
env <- mkRegistryEnv offline
611+
logDebug $ "Creating repl project in temp dir: " <> Path.quote tmpDir
612+
Paths.chdir tmpDir
613+
tmpRootPath <- Path.mkRoot tmpDir
614+
env <- mkRegistryEnv offline <#> Record.union { rootPath: tmpRootPath }
615615
void $ runSpago env $ Init.run
616616
{ setVersion: Nothing
617617
, mode: Init.InitWorkspace { packageName: Just "repl" }
@@ -661,12 +661,12 @@ main = do
661661
testEnv <- runSpago env (mkTestEnv args buildEnv)
662662
runSpago testEnv Test.run
663663
LsPaths args -> do
664-
runSpago { logOptions } $ Ls.listPaths args
664+
runSpago { logOptions, rootPath } $ Ls.listPaths args
665665
LsPackages args@{ pure } -> do
666666
let fetchArgs = { packages: mempty, selectedPackage: Nothing, pure, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline }
667667
{ env: env@{ workspace }, fetchOpts } <- mkFetchEnv fetchArgs
668668
dependencies <- runSpago env (Fetch.run fetchOpts)
669-
let lsEnv = { workspace, dependencies, logOptions }
669+
let lsEnv = { workspace, dependencies, logOptions, rootPath }
670670
runSpago lsEnv (Ls.listPackageSet args)
671671
LsDeps { selectedPackage, json, transitive, pure } -> do
672672
let fetchArgs = { packages: mempty, selectedPackage, pure, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline }
@@ -691,12 +691,12 @@ main = do
691691
{ env, fetchOpts } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline }
692692
dependencies <- runSpago env (Fetch.run fetchOpts)
693693
purs <- Purs.getPurs
694-
runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphModules args)
694+
runSpago { dependencies, logOptions, rootPath, purs, workspace: env.workspace } (Graph.graphModules args)
695695
GraphPackages args -> do
696696
{ env, fetchOpts } <- mkFetchEnv { packages: mempty, selectedPackage: Nothing, pure: false, ensureRanges: false, testDeps: false, isRepl: false, migrateConfig, offline }
697697
dependencies <- runSpago env (Fetch.run fetchOpts)
698698
purs <- Purs.getPurs
699-
runSpago { dependencies, logOptions, purs, workspace: env.workspace } (Graph.graphPackages args)
699+
runSpago { dependencies, logOptions, rootPath, purs, workspace: env.workspace } (Graph.graphPackages args)
700700

701701
Cmd'VersionCmd v -> when v do
702702
output (OutputLines [ BuildInfo.packages."spago-bin" ])
@@ -721,7 +721,7 @@ main = do
721721

722722
mkBundleEnv :: forall a. BundleArgs -> Spago (Fetch.FetchEnv a) (Bundle.BundleEnv ())
723723
mkBundleEnv bundleArgs = do
724-
{ workspace, logOptions } <- ask
724+
{ workspace, logOptions, rootPath } <- ask
725725
logDebug $ "Bundle args: " <> show bundleArgs
726726

727727
selected <- case workspace.selected of
@@ -770,18 +770,19 @@ mkBundleEnv bundleArgs = do
770770
, sourceMaps: bundleArgs.sourceMaps
771771
, extraArgs
772772
}
773+
argsOutput = bundleArgs.output <#> (rootPath </> _)
773774
newWorkspace = workspace
774775
{ buildOptions
775-
{ output = bundleArgs.output <|> workspace.buildOptions.output
776+
{ output = argsOutput <|> workspace.buildOptions.output
776777
}
777778
}
778779
esbuild <- Esbuild.getEsbuild
779-
let bundleEnv = { esbuild, logOptions, workspace: newWorkspace, selected, bundleOptions }
780+
let bundleEnv = { esbuild, logOptions, rootPath, workspace: newWorkspace, selected, bundleOptions }
780781
pure bundleEnv
781782

782783
mkRunEnv :: forall a b. RunArgs -> Build.BuildEnv b -> Spago (Fetch.FetchEnv a) (Run.RunEnv ())
783784
mkRunEnv runArgs { dependencies, purs } = do
784-
{ workspace, logOptions } <- ask
785+
{ workspace, logOptions, rootPath } <- ask
785786
logDebug $ "Run args: " <> show runArgs
786787

787788
node <- Run.getNode
@@ -816,17 +817,18 @@ mkRunEnv runArgs { dependencies, purs } = do
816817
runOptions =
817818
{ moduleName
818819
, execArgs
819-
, executeDir: Paths.cwd
820+
, executeDir: Path.toGlobal rootPath
820821
, successMessage: Nothing
821822
, failureMessage: "Running failed."
822823
}
823-
let newWorkspace = workspace { buildOptions { output = runArgs.output <|> workspace.buildOptions.output } }
824-
let runEnv = { logOptions, workspace: newWorkspace, selected, node, runOptions, dependencies, purs }
824+
let argsOutput = runArgs.output <#> (rootPath </> _)
825+
let newWorkspace = workspace { buildOptions { output = argsOutput <|> workspace.buildOptions.output } }
826+
let runEnv = { logOptions, rootPath, workspace: newWorkspace, selected, node, runOptions, dependencies, purs }
825827
pure runEnv
826828

827829
mkTestEnv :: forall a b. TestArgs -> Build.BuildEnv b -> Spago (Fetch.FetchEnv a) (Test.TestEnv ())
828830
mkTestEnv testArgs { dependencies, purs } = do
829-
{ workspace, logOptions } <- ask
831+
{ workspace, logOptions, rootPath } <- ask
830832
logDebug $ "Test args: " <> show testArgs
831833

832834
node <- Run.getNode
@@ -860,8 +862,9 @@ mkTestEnv testArgs { dependencies, purs } = do
860862

861863
logDebug $ "Selected packages to test: " <> Json.stringifyJson (CJ.Common.nonEmptyArray PackageName.codec) (map _.selected.package.name selectedPackages)
862864

863-
let newWorkspace = workspace { buildOptions { output = testArgs.output <|> workspace.buildOptions.output } }
864-
let testEnv = { logOptions, workspace: newWorkspace, selectedPackages, node, dependencies, purs }
865+
let argsOutput = testArgs.output <#> (rootPath </> _)
866+
let newWorkspace = workspace { buildOptions { output = argsOutput <|> workspace.buildOptions.output } }
867+
let testEnv = { logOptions, rootPath, workspace: newWorkspace, selectedPackages, node, dependencies, purs }
865868
pure testEnv
866869

867870
mkBuildEnv
@@ -876,12 +879,13 @@ mkBuildEnv
876879
-> Fetch.PackageTransitiveDeps
877880
-> Spago (Fetch.FetchEnv ()) (Build.BuildEnv ())
878881
mkBuildEnv buildArgs dependencies = do
879-
{ logOptions, workspace, git } <- ask
882+
{ logOptions, rootPath, workspace, git } <- ask
880883
purs <- Purs.getPurs
881884
let
885+
argsOutput = buildArgs.output <#> (rootPath </> _)
882886
newWorkspace = workspace
883887
{ buildOptions
884-
{ output = buildArgs.output <|> workspace.buildOptions.output
888+
{ output = argsOutput <|> workspace.buildOptions.output
885889
, statVerbosity = buildArgs.statVerbosity <|> workspace.buildOptions.statVerbosity
886890
}
887891
-- Override the backend args from the config if they are passed in through a flag
@@ -895,6 +899,7 @@ mkBuildEnv buildArgs dependencies = do
895899

896900
pure
897901
{ logOptions
902+
, rootPath
898903
, purs
899904
, git
900905
, dependencies
@@ -925,7 +930,7 @@ mkPublishEnv dependencies = do
925930

926931
mkReplEnv :: forall a. ReplArgs -> Fetch.PackageTransitiveDeps -> PackageMap -> Spago (Fetch.FetchEnv a) (Repl.ReplEnv ())
927932
mkReplEnv replArgs dependencies supportPackage = do
928-
{ workspace, logOptions } <- ask
933+
{ workspace, logOptions, rootPath } <- ask
929934
logDebug $ "Repl args: " <> show replArgs
930935

931936
purs <- Purs.getPurs
@@ -941,16 +946,17 @@ mkReplEnv replArgs dependencies supportPackage = do
941946
, supportPackage
942947
, depsOnly: false
943948
, logOptions
949+
, rootPath
944950
, pursArgs: Array.fromFoldable replArgs.pursArgs
945951
, selected
946952
}
947953

948-
mkFetchEnv :: forall a b. { offline :: OnlineStatus, migrateConfig :: Boolean, isRepl :: Boolean | FetchArgsRow b } -> Spago (LogEnv a) { env :: Fetch.FetchEnv (), fetchOpts :: Fetch.FetchOpts }
954+
mkFetchEnv :: forall a b. { offline :: OnlineStatus, migrateConfig :: Boolean, isRepl :: Boolean | FetchArgsRow b } -> Spago (SpagoBaseEnv a) { env :: Fetch.FetchEnv (), fetchOpts :: Fetch.FetchOpts }
949955
mkFetchEnv args@{ migrateConfig, offline } = do
950956
let
951-
parsePackageName p = case PackageName.parse p of
952-
Right pkg -> Right pkg
953-
Left err -> Left ("- Could not parse package " <> show p <> ": " <> err)
957+
parsePackageName p =
958+
PackageName.parse p
959+
# lmap \err -> "- Could not parse package " <> show p <> ": " <> err
954960
let { right: packageNames, left: failedPackageNames } = partitionMap parsePackageName (Array.fromFoldable args.packages)
955961
unless (Array.null failedPackageNames) do
956962
die $ [ toDoc "Failed to parse some package name: " ] <> map (indent <<< toDoc) failedPackageNames
@@ -960,25 +966,28 @@ mkFetchEnv args@{ migrateConfig, offline } = do
960966
Left _err -> die $ "Failed to parse selected package name, was: " <> show args.selectedPackage
961967

962968
env <- mkRegistryEnv offline
963-
workspace <- runSpago env (Config.readWorkspace { maybeSelectedPackage, pureBuild: args.pure, migrateConfig })
969+
{ rootPath } <- ask
970+
workspace <-
971+
runSpago (Record.union env { rootPath })
972+
(Config.readWorkspace { maybeSelectedPackage, pureBuild: args.pure, migrateConfig })
964973
let fetchOpts = { packages: packageNames, ensureRanges: args.ensureRanges, isTest: args.testDeps, isRepl: args.isRepl }
965-
pure { fetchOpts, env: Record.union { workspace } env }
974+
pure { fetchOpts, env: Record.union { workspace, rootPath } env }
966975

967-
mkRegistryEnv :: forall a. OnlineStatus -> Spago (LogEnv a) (Registry.RegistryEnv ())
976+
mkRegistryEnv :: forall a. OnlineStatus -> Spago (SpagoBaseEnv a) (Registry.RegistryEnv ())
968977
mkRegistryEnv offline = do
969-
logDebug $ "CWD: " <> Paths.cwd
978+
{ logOptions, rootPath } <- ask
970979

971980
-- Take care of the caches
972981
FS.mkdirp Paths.globalCachePath
973-
FS.mkdirp Paths.localCachePath
974-
FS.mkdirp Paths.localCachePackagesPath
975-
logDebug $ "Global cache: " <> show Paths.globalCachePath
976-
logDebug $ "Local cache: " <> show Paths.localCachePath
982+
FS.mkdirp $ rootPath </> Paths.localCachePath
983+
FS.mkdirp $ rootPath </> Paths.localCachePackagesPath
984+
logDebug $ "Workspace root path: " <> Path.quote rootPath
985+
logDebug $ "Global cache: " <> Path.quote Paths.globalCachePath
986+
logDebug $ "Local cache: " <> Paths.localCachePath
977987

978988
-- Make sure we have git and purs
979989
git <- Git.getGit
980990
purs <- Purs.getPurs
981-
{ logOptions } <- ask
982991
db <- liftEffect $ Db.connect
983992
{ database: Paths.databasePath
984993
, logger: \str -> Reader.runReaderT (logDebug $ "DB: " <> str) { logOptions }
@@ -997,7 +1006,7 @@ mkRegistryEnv offline = do
9971006

9981007
mkLsEnv :: forall a. Fetch.PackageTransitiveDeps -> Spago (Fetch.FetchEnv a) Ls.LsEnv
9991008
mkLsEnv dependencies = do
1000-
{ logOptions, workspace } <- ask
1009+
{ logOptions, workspace, rootPath } <- ask
10011010
selected <- case workspace.selected of
10021011
Just s -> pure s
10031012
Nothing ->
@@ -1013,15 +1022,16 @@ mkLsEnv dependencies = do
10131022
[ toDoc "No package was selected. Please select (with -p) one of the following packages:"
10141023
, indent (toDoc $ map _.package.name workspacePackages)
10151024
]
1016-
pure { logOptions, workspace, dependencies, selected }
1025+
pure { logOptions, workspace, dependencies, selected, rootPath }
10171026

10181027
mkDocsEnv :: a. DocsArgs -> Fetch.PackageTransitiveDeps -> Spago (Fetch.FetchEnv a) (Docs.DocsEnv ())
10191028
mkDocsEnv args dependencies = do
1020-
{ logOptions, workspace } <- ask
1029+
{ logOptions, rootPath, workspace } <- ask
10211030
purs <- Purs.getPurs
10221031
pure
10231032
{ purs
10241033
, logOptions
1034+
, rootPath
10251035
, workspace
10261036
, dependencies
10271037
, depsOnly: args.depsOnly

0 commit comments

Comments
 (0)