Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stricter path types for stricter path-related logic #1296

Merged
merged 35 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b84981d
Stricter path types for stricter path-related logic
fsoikin Oct 25, 2024
c0d1fdf
Satisfy Windows
fsoikin Oct 25, 2024
05b5da6
Dev docs
fsoikin Oct 27, 2024
86a24e9
Run purs graph with a CWD too
fsoikin Oct 28, 2024
ad73d88
What would I even do if I didn't have to run the autoformatter?
fsoikin Oct 28, 2024
bd2d151
Even though I walk through the valley of the shadow of Windows, I wil…
fsoikin Oct 28, 2024
d69abfb
And format again
fsoikin Oct 29, 2024
f20c3f9
And we are still not done with the forward slashes
fsoikin Oct 29, 2024
2377daf
And we are still not done with the forward slashes
fsoikin Oct 29, 2024
e5df7e6
Merge remote-tracking branch 'upstream/master' into file-paths
fsoikin Oct 30, 2024
de8173c
Fixup after merge
fsoikin Oct 30, 2024
8dbbed0
Merge remote-tracking branch 'upstream/master' into file-paths
fsoikin Oct 31, 2024
4da39ea
Merge remote-tracking branch 'upstream/master' into file-paths
fsoikin Nov 8, 2024
b4a52f1
Merge remote-tracking branch 'upstream/master' into file-paths
fsoikin Nov 22, 2024
7809b56
Merge remote-tracking branch 'upstream/master' into file-paths
fsoikin Jan 17, 2025
1d7c4f3
Fix after rebase
fsoikin Jan 17, 2025
e11d2b5
Remove debug
fsoikin Jan 17, 2025
212ef66
Remove debug type signature, add back script, unnest tests
fsoikin Jan 17, 2025
af3e06d
Fix a typo, move no-doc crashes closer to where the doc is used
fsoikin Jan 18, 2025
09db00e
Debug Windows failure
fsoikin Jan 19, 2025
ca2be2d
Debug Windows failure
fsoikin Jan 19, 2025
a5fd09d
Debug Windows failure
fsoikin Jan 19, 2025
35f448c
Debug Windows failure
fsoikin Jan 19, 2025
73324bb
Debug Windows failure
fsoikin Jan 19, 2025
8c9212e
Debug Windows failure
fsoikin Jan 19, 2025
d22ab4a
Debug Windows failure
fsoikin Jan 19, 2025
8bafbd1
Debug Windows failure
fsoikin Jan 19, 2025
4b40174
Debug Windows failure
fsoikin Jan 19, 2025
7e69b37
Debug Windows failure
fsoikin Jan 19, 2025
15d1b77
Debug Windows failure
fsoikin Jan 19, 2025
47b3dd2
Debug Windows failure
fsoikin Jan 19, 2025
baa2a41
Debug Windows failure
fsoikin Jan 19, 2025
c0154b7
Add a comment, remove debugging scaffolding
fsoikin Jan 19, 2025
b0e2ae9
Rename AdHocFilePath to RawFilePath
fsoikin Jan 19, 2025
22af591
Increase timeout
fsoikin Jan 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}-
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 47 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
4 changes: 2 additions & 2 deletions bin/src/Flags.purs
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,15 @@ depsOnly =
<> O.help "Build depedencies only"
)

publicKeyPath :: Parser FilePath
publicKeyPath :: Parser RawFilePath
publicKeyPath =
O.strOption
( O.short 'i'
<> O.metavar "PUBLIC_KEY_PATH"
<> 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'
Expand Down
96 changes: 53 additions & 43 deletions bin/src/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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(..))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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" }
Expand Down Expand Up @@ -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 }
Expand All @@ -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" ])
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -895,6 +899,7 @@ mkBuildEnv buildArgs dependencies = do

pure
{ logOptions
, rootPath
, purs
, git
, dependencies
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 }
Expand All @@ -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 ->
Expand All @@ -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
Expand Down
Loading
Loading