@@ -1,6 +1,3 @@
1−{-# LANGUAGE DeriveFoldable #-}
2−{-# LANGUAGE DeriveFunctor #-}
3−{-# LANGUAGE DeriveTraversable #-}
41{-# LANGUAGE FlexibleContexts #-}
52{-# LANGUAGE LambdaCase #-}
63{-# LANGUAGE MultiParamTypeClasses #-}
@@ -18,18 +15,16 @@ import Control.Monad (filterM, unless, void)
1815import Control.Monad.Except (runExceptT, throwError)
1916import Control.Monad.IO.Class (liftIO)
2017import Control.Monad.Reader (MonadIO)
21−import Data.Bifoldable (bifoldr1)
22−import Data.Bifunctor (bimap)
2318import Data.Coerce (coerce)
2419import Data.Default (def)
2520import Data.Either (partitionEithers)
2621import Data.Foldable (fold, for_, toList)
22+import Data.Functor ((<&>))
2723import Data.Functor.Identity (runIdentity)
2824import Data.List.NonEmpty (NonEmpty)
2925import qualified Data.List.NonEmpty as NE
3026import Data.Map (Map)
3127import qualified Data.Map as Map
32−import qualified Data.Map.Monoidal as MMap
3328import Data.Maybe
3429import Data.Set (Set)
3530import qualified Data.Set as Set
@@ -55,9 +50,7 @@ import qualified Hpack.Render as Hpack
5550import qualified Hpack.Yaml as Hpack
5651import Language.Haskell.Extension
5752import Network.Socket hiding (Debug)
58−import System.Directory
5953import System.Environment (getExecutablePath)
60−import System.FilePath
6154import qualified System.Info
6255import System.IO.Temp (withSystemTempDirectory)
6356
@@ -68,7 +61,6 @@ import Obelisk.CliApp (
6861 failWith,
6962 proc,
7063 putLog,
71− readCreateProcessWithExitCode,
7264 readProcessAndLogStderr,
7365 setCwd,
7466 setDelegateCtlc,
@@ -76,9 +68,10 @@ import Obelisk.CliApp (
7668 withSpinner,
7769 )
7870import Obelisk.Command.Nix
79−import Obelisk.Command.Project (nixShellWithoutPkgs, withProjectRoot, findProjectAssets)
71+import Obelisk.Command.Path
72+import Obelisk.Command.Project (findProjectAssets, nixShellWithoutPkgs, obeliskDirName, withProjectRoot)
8073import Obelisk.Command.Thunk (attrCacheFileName)
81−import Obelisk.Command.Utils (findExePath, ghcidExePath)
74+import Obelisk.Command.Utils (ghcidExePath)
8275
8376data CabalPackageInfo = CabalPackageInfo
8477 { _cabalPackageInfo_packageFile :: FilePath
@@ -103,26 +96,6 @@ textInterpret = \case
10396 Interpret_Interpret -> "Interpret"
10497 Interpret_NoInterpret -> "NoInterpret"
10598
106−-- | Describe a set of 'FilePath's as a tree to facilitate merging them in a convenient way.
107−data PathTree a = PathTree_Node
108− (Maybe a) -- An optional leaf at this point in the tree
109− (Map FilePath (PathTree a)) -- Branches to deeper leaves
110− deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
111−
112−emptyPathTree :: PathTree a
113−emptyPathTree = PathTree_Node Nothing mempty
114−
115−-- | 2D ASCII drawing of a 'PathTree'. Adapted from Data.Tree.draw.
116−drawPathTree :: (a -> Text) -> PathTree a -> Text
117−drawPathTree showA (PathTree_Node _ ts0) = T.intercalate "\n" $ goForest (Map.toList ts0)
118− where
119− annotated ma = maybe id (\a b -> b <> " [" <> showA a <> "]") ma . T.pack
120− goTree (fp, PathTree_Node ma forest) = annotated ma fp : goForest (Map.toList forest)
121− goForest [] = []
122− goForest [tree] = shift "└─ " " " (goTree tree)
123− goForest (tree:forest) = shift "├─ " "│ " (goTree tree) <> goForest forest
124− shift first other = zipWith (<>) (first : repeat other)
125−
12699-- | Used to signal to obelisk that it's being invoked as a preprocessor
127100preprocessorIdentifier :: String
128101preprocessorIdentifier = "__preprocessor-apply-packages"
@@ -148,10 +121,10 @@ profile profileBasePattern rtsFlags = withProjectRoot "." $ \root -> do
148121 putLog Debug $ "Assets impurely loaded from: " <> assets
149122 time <- liftIO getCurrentTime
150123 let profileBaseName = formatTime defaultTimeLocale profileBasePattern time
151− liftIO $ createDirectoryIfMissing True $ takeDirectory $ root </> profileBaseName
152− putLog Debug $ "Storing profiled data under base name of " <> T.pack (root </> profileBaseName)
124+ createDirectoryIfMissing True $ takeDirectory $ Path $ root </> profileBaseName
125+ putLog Debug [i|Storing profiled data under base name of ${root </> profileBaseName}|]
153126 freePort <- getFreePort
154− (_, _, _, ph) <- createProcess_ "runProfExe" $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> "bin" </> "ob-run") $
127+ (_, _, _, ph) <- createProcess_ "runProfExe" $ setCwd (Just root) $ setDelegateCtlc True $ proc (outPath </> rel "bin" </> rel "ob-run") $
155128 [ show freePort
156129 , T.unpack assets
157130 , profileBaseName
@@ -180,7 +153,7 @@ run root interpretPaths = do
180153runRepl :: MonadObelisk m => FilePath -> PathTree Interpret -> m ()
181154runRepl root interpretPaths = do
182155 pkgs <- getParsedLocalPkgs root interpretPaths
183− ghciArgs <- getGhciSessionSettings pkgs "." True
156+ ghciArgs <- getGhciSessionSettings pkgs (rel ".") True
184157 withGhciScript pkgs $ \dotGhciPath ->
185158 runGhciRepl root pkgs (ghciArgs <> mkGhciScriptArg dotGhciPath)
186159
@@ -194,7 +167,7 @@ runWatch root interpretPaths = do
194167exportGhciConfig :: MonadObelisk m => Bool -> FilePath -> PathTree Interpret -> m [String]
195168exportGhciConfig useRelativePaths root interpretPaths = do
196169 pkgs <- getParsedLocalPkgs root interpretPaths
197− getGhciSessionSettings pkgs "." useRelativePaths
170+ getGhciSessionSettings pkgs (rel ".") useRelativePaths
198171
199172nixShellForInterpretPaths :: MonadObelisk m => Bool -> String -> FilePath -> PathTree Interpret -> Maybe String -> m ()
200173nixShellForInterpretPaths isPure shell root interpretPaths cmd = do
@@ -210,53 +183,37 @@ getParsedLocalPkgs root interpretPaths = parsePackagesOrFail =<< getLocalPkgs ro
210183-- These are a combination of the obelisk predefined local packages,
211184-- and any packages that the user has set with the @packages@ argument
212185-- to the Nix @project@ function.
213−getLocalPkgs :: forall m. MonadObelisk m => FilePath -> PathTree Interpret -> m (Set FilePath)
186+getLocalPkgs :: forall m. MonadObelisk m => FilePath -> PathTree Interpret -> m (Set (Path Canonical))
214187getLocalPkgs root interpretPaths = do
215188 putLog Debug $ [i|Finding packages with root ${root} and interpret paths:|] <> "\n" <> drawPathTree textInterpret interpretPaths
216− obeliskPackagePaths <- runFind ["-L", root, "-name", ".obelisk", "-type", "d"]
217189
218190 -- We do not want to find packages that are embedded inside other obelisk projects, unless that
219191 -- obelisk project is our own.
220− obeliskPackageExclusions <- liftIO $ fmap Set.fromList $ traverse canonicalizePath $
221− filter (/= root) $ map takeDirectory obeliskPackagePaths
222− putLog Debug [i|Excluding obelisk packages: ${T.pack $ unwords $ Set.toList obeliskPackageExclusions}|]
223− let rootsAndExclusions = calcIntepretFinds "" interpretPaths
224−
225− fmap fold $ for (MMap.toAscList rootsAndExclusions) $ \(interpretPathRoot, exclusions) ->
226− let allExclusions = obeliskPackageExclusions <> exclusions <> Set.singleton ("*" </> attrCacheFileName)
227− in fmap (Set.fromList . map normalise) $ runFind $
228− ["-L", interpretPathRoot, "(", "-name", "*.cabal", "-o", "-name", Hpack.packageConfig, ")", "-a", "-type", "f"]
229− <> concat [["-not", "-path", p </> "*"] | p <- toList allExclusions]
230− where
231− runFind args = do
232− (_exitCode, out, err) <- readCreateProcessWithExitCode $ proc findExePath args
233− putLog Debug $ T.strip $ T.pack err
234− pure $ map T.unpack $ T.lines $ T.strip $ T.pack out
235−
236−-- | Calculates a set of root 'FilePath's along with each one's corresponding set of exclusions.
237−-- This is used when constructing a set of @find@ commands to run to produce a set of packages
238−-- that matches the user's @--interpret@/@--no-interpret@ settings.
239−calcIntepretFinds :: FilePath -> PathTree Interpret -> MMap.MonoidalMap FilePath (Set FilePath)
240−calcIntepretFinds treeRoot0 tree0 = runIdentity $ go treeRoot0 tree0
241− where
242− go treeRoot tree = foldPathTreeFor (== Interpret_Interpret) treeRoot tree $ \parent children -> do
243− exclusions <- foldPathTreeFor (== Interpret_NoInterpret) parent children $ \parent' children' ->
244− pure $ Map.singleton parent' children'
245− deeperFinds <- Map.traverseWithKey go exclusions
246− pure $ MMap.singleton parent (Map.keysSet exclusions) <> fold (MMap.MonoidalMap deeperFinds)
247−
248−-- | Traverses a 'PathTree' and folds all leaves matching a given predicate.
249−foldPathTreeFor
250− :: forall m a b. (Applicative m, Monoid b)
251− => (a -> Bool)
252− -> FilePath
253− -> PathTree a
254− -> (FilePath -> PathTree a -> m b)
255− -> m b
256−foldPathTreeFor predicate parent children f = case children of
257− PathTree_Node (Just x) children' | predicate x -> f parent (PathTree_Node Nothing children')
258− PathTree_Node _ children' -> fmap fold $ flip Map.traverseWithKey children' $ \k children'' ->
259− foldPathTreeFor predicate (parent </> k) children'' f
192+ canonicalRoot <- canonicalizePath root
193+ obeliskPackageExclusions <- findFiles'
194+ (\_ p -> if takeFileName p == Path obeliskDirName && p /= canonicalRoot
195+ then liftIO (doesDirectoryExist p) <&> \case
196+ True -> FindMatch_Match $ Set.singleton p
197+ False -> FindMatch_SearchTree
198+ else pure FindMatch_SearchTree
199+ )
200+ root
201+
202+ putLog Debug [i|Excluding obelisk packages: ${T.pack $ unwords $ map show $ Set.toList obeliskPackageExclusions}|]
203+
204+ let
205+ findRoots = runIdentity $ foldPathTreeFor (== Interpret_Interpret) "" interpretPaths (\p _ -> pure $ Set.singleton $ Path p)
206+ exclusions = runIdentity $ foldPathTreeFor (== Interpret_NoInterpret) "" interpretPaths (\p _ -> pure $ Set.singleton $ Path p)
207+
208+ fmap fold $ for (toList findRoots) $ \interpretPathRoot -> do
209+ let allExclusions = obeliskPackageExclusions <> exclusions
210+ findFiles'
211+ (\_ p -> pure $ case () of
212+ _ | p `Set.member` allExclusions || attrCacheFileName `elem` splitDirectories p -> FindMatch_SkipTree
213+ | takeExtension p == ".cabal" || takeFileName p == rel Hpack.packageConfig -> FindMatch_Match $ Set.singleton p
214+ | otherwise -> FindMatch_SearchTree
215+ )
216+ interpretPathRoot
260217
261218data GuessPackageFileError = GuessPackageFileError_Ambiguous [FilePath] | GuessPackageFileError_NotFound
262219 deriving (Eq, Ord, Show)
@@ -267,17 +224,17 @@ newtype CabalFilePath = CabalFilePath { unCabalFilePath :: FilePath } deriving (
267224
268225-- | Given a directory, try to guess what the appropriate @.cabal@ or @package.yaml@ file is for the package.
269226guessCabalPackageFile
270− :: (MonadIO m)
271− => FilePath -- ^ Directory or path to search for cabal package
227+ :: (MonadIO m, IsPath path)
228+ => path -- ^ Directory or path to search for cabal package
272229 -> m (Either GuessPackageFileError (Either CabalFilePath HPackFilePath))
273−guessCabalPackageFile pkg = do
230+guessCabalPackageFile (coerce -> pkg) = do
274231 liftIO (doesDirectoryExist pkg) >>= \case
275232 False -> case cabalOrHpackFile pkg of
276233 (Just hpack@(Right _)) -> pure $ Right hpack
277234 (Just cabal@(Left (CabalFilePath cabalFilePath))) -> do
278235 -- If the cabal file has a sibling hpack file, we use that instead
279236 -- since running hpack often generates a sibling cabal file
280− let possibleHpackSibling = takeDirectory cabalFilePath </> Hpack.packageConfig
237+ let possibleHpackSibling = takeDirectory (Path cabalFilePath) </> Hpack.packageConfig
281238 hasHpackSibling <- liftIO $ doesFileExist possibleHpackSibling
282239 pure $ Right $ if hasHpackSibling then Right (HPackFilePath possibleHpackSibling) else cabal
283240 Nothing -> pure $ Left GuessPackageFileError_NotFound
@@ -294,14 +251,14 @@ guessCabalPackageFile pkg = do
294251cabalOrHpackFile :: FilePath -> Maybe (Either CabalFilePath HPackFilePath)
295252cabalOrHpackFile = \case
296253 x | takeExtension x == ".cabal" -> Just (Left $ CabalFilePath x)
297− | takeFileName x == Hpack.packageConfig -> Just (Right $ HPackFilePath x)
254+ | takeFileName x == rel Hpack.packageConfig -> Just (Right $ HPackFilePath x)
298255 | otherwise -> Nothing
299256
300257-- | Parses the cabal package in a given directory.
301258-- This automatically figures out which .cabal file or package.yaml (hpack) file to use in the given directory.
302259parseCabalPackage
303− :: MonadObelisk m
304− => FilePath -- ^ Package directory
260+ :: (MonadObelisk m, IsPath path)
261+ => path
305262 -> m (Maybe CabalPackageInfo)
306263parseCabalPackage dir = parseCabalPackage' dir >>= \case
307264 Left err -> Nothing <$ putLog Error err
@@ -311,10 +268,10 @@ parseCabalPackage dir = parseCabalPackage' dir >>= \case
311268
312269-- | Like 'parseCabalPackage' but returns errors and warnings directly so as to avoid 'MonadObelisk'.
313270parseCabalPackage'
314− :: (MonadIO m)
315− => FilePath -- ^ Package directory
271+ :: (MonadIO m, IsPath path)
272+ => path -- ^ Package directory
316273 -> m (Either T.Text ([Dist.PWarning], CabalPackageInfo))
317−parseCabalPackage' pkg = runExceptT $ do
274+parseCabalPackage' (coerce -> pkg) = runExceptT $ do
318275 (cabalContents, packageFile, packageName) <- guessCabalPackageFile pkg >>= \case
319276 Left GuessPackageFileError_NotFound -> throwError $ "No .cabal or package.yaml file found in " <> T.pack pkg
320277 Left (GuessPackageFileError_Ambiguous _) -> throwError $ "Unable to determine which .cabal file to use in " <> T.pack pkg
@@ -344,7 +301,7 @@ parseCabalPackage' pkg = runExceptT $ do
344301 pure $ (warnings,) $ CabalPackageInfo
345302 { _cabalPackageInfo_packageName = T.pack packageName
346303 , _cabalPackageInfo_packageFile = packageFile
347− , _cabalPackageInfo_packageRoot = takeDirectory packageFile
304+ , _cabalPackageInfo_packageRoot = unPath $ takeDirectory (Path packageFile)
348305 , _cabalPackageInfo_buildable = buildable $ libBuildInfo lib
349306 , _cabalPackageInfo_sourceDirs =
350307 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
@@ -356,9 +313,9 @@ parseCabalPackage' pkg = runExceptT $ do
356313 }
357314 Right Nothing -> throwError "Haskell package has no library component"
358315 Left (_, errors) ->
359− throwError $ T.pack $ "Failed to parse " <> packageFile <> ":\n" <> unlines (map show errors)
316+ throwError $ [i|Failed to parse ${packageFile}:|] <> "\n" <> T.pack (unlines (map show errors))
360317
361−parsePackagesOrFail :: (MonadObelisk m, Foldable f) => f FilePath -> m (NE.NonEmpty CabalPackageInfo)
318+parsePackagesOrFail :: (MonadObelisk m, Foldable f, IsPath path) => f path -> m (NE.NonEmpty CabalPackageInfo)
362319parsePackagesOrFail dirs' = do
363320 (pkgDirErrs, packageInfos') <- fmap partitionEithers $ for dirs $ \dir -> do
364321 flip fmap (parseCabalPackage dir) $ \case
@@ -383,11 +340,11 @@ parsePackagesOrFail dirs' = do
383340
384341 packageInfos <- case NE.nonEmpty $ toList unambiguous of
385342 Nothing -> failWith $ T.pack $
386− "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " dirs)
343+ "No valid, buildable packages found" <> (if null dirs then "" else " in " <> intercalate ", " (coerce dirs))
387344 Just xs -> pure xs
388345
389346 unless (null pkgDirErrs) $
390− putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " pkgDirErrs
347+ putLog Warning $ T.pack $ "Failed to find buildable packages in " <> intercalate ", " (coerce pkgDirErrs)
391348
392349 pure packageInfos
393350 where
@@ -404,7 +361,7 @@ withGhciScript
404361 -> m ()
405362withGhciScript (toList -> packageInfos) f =
406363 withSystemTempDirectory "ob-ghci" $ \fp -> do
407− let dotGhciPath = fp </> ".ghci"
364+ let dotGhciPath = fp </> rel ".ghci"
408365 liftIO $ writeFile dotGhciPath dotGhci
409366 f dotGhciPath
410367 where
@@ -424,17 +381,17 @@ withGhciScript (toList -> packageInfos) f =
424381-- | Builds a list of options to pass to ghci or set in .ghci file that configures
425382-- the preprocessor and source includes.
426383getGhciSessionSettings
427− :: (MonadObelisk m, Foldable f)
384+ :: (MonadObelisk m, Foldable f, IsPath path)
428385 => f CabalPackageInfo -- ^ List of packages to load into ghci
429− -> FilePath -- ^ All paths will be relative to this path
386+ -> path -- ^ All paths will be relative to this path
430387 -> Bool -- ^ Use relative paths
431388 -> m [String]
432389getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
433390 -- N.B. ghci settings do NOT support escaping in any way. To minimize the likelihood that
434391 -- paths-with-spaces ruin our day, we first canonicalize everything, and then relativize
435392 -- all paths to 'pathBase'.
436− selfExe <- liftIO $ canonicalizePath =<< getExecutablePath
437− canonicalPathBase <- liftIO $ canonicalizePath pathBase
393+ selfExe <- canonicalizePath =<< liftIO getExecutablePath
394+ canonicalPathBase <- canonicalizePath pathBase
438395
439396 (pkgFiles, pkgSrcPaths :: [NonEmpty FilePath]) <- fmap unzip $ liftIO $ for packageInfos $ \pkg -> do
440397 canonicalSrcDirs <- traverse canonicalizePath $ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
@@ -443,11 +400,11 @@ getGhciSessionSettings (toList -> packageInfos) pathBase useRelativePaths = do
443400
444401 pure
445402 $ baseGhciOptions
446− <> ["-F", "-pgmF", selfExe, "-optF", preprocessorIdentifier]
403+ <> ["-F", "-pgmF", unPath selfExe, "-optF", preprocessorIdentifier]
447404 <> concatMap (\p -> ["-optF", p]) pkgFiles
448405 <> [ "-i" <> intercalate ":" (concatMap toList pkgSrcPaths) ]
449406 where
450− relativeTo' = if useRelativePaths then relativeTo else const
407+ relativeTo' = if useRelativePaths then relativeTo else const . unPath
451408
452409baseGhciOptions :: [String]
453410baseGhciOptions =
@@ -492,11 +449,11 @@ runGhcid root chdirToRoot ghciArgs (toList -> packages) mcmd =
492449 testCmd = maybeToList (flip fmap mcmd $ \cmd -> "--test='" <> cmd <> "'") -- TODO: Shell escape
493450
494451 adjustRoot x = if chdirToRoot then makeRelative root x else x
495− reloadFiles = map adjustRoot [root </> "config"]
452+ reloadFiles = map adjustRoot [root </> rel "config"]
496453 restartFiles = map (adjustRoot . _cabalPackageInfo_packageFile) packages
497454
498−mkGhciScriptArg :: FilePath -> [String]
499−mkGhciScriptArg dotGhci = ["-ghci-script", dotGhci]
455+mkGhciScriptArg :: IsPath path => path -> [String]
456+mkGhciScriptArg dotGhci = ["-ghci-script", coerce dotGhci]
500457
501458getFreePort :: MonadIO m => m PortNumber
502459getFreePort = liftIO $ withSocketsDo $ do
@@ -509,35 +466,3 @@ getFreePort = liftIO $ withSocketsDo $ do
509466 return sock
510467
511468
512−-- | Convert a 'FilePath' into a 'PathTree'.
513−pathToTree :: a -> FilePath -> PathTree a
514−pathToTree a p = go $ splitDirectories p
515− where
516− go [] = PathTree_Node (Just a) mempty
517− go (x : xs) = PathTree_Node Nothing $ Map.singleton x $ go xs
518−
519−-- | Like 'zipWith' but pads with a padding value instead of stopping on the shortest list.
520−zipDefaultWith :: a -> b -> (a -> b -> c) -> [a] -> [b] -> [c]
521−zipDefaultWith _da _db _f [] [] = []
522−zipDefaultWith da db f (a:as) [] = f a db : zipDefaultWith da db f as []
523−zipDefaultWith da db f [] (b:bs) = f da b : zipDefaultWith da db f [] bs
524−zipDefaultWith da db f (a:as) (b:bs) = f a b : zipDefaultWith da db f as bs
525−
526−-- | Makes the first absolute path relative to the second absolute path.
527−--
528−-- Both input paths MUST be absolute.
529−--
530−-- Unlike 'makeRelative' this does not merely strip prefixes. It will introduce
531−-- enough @..@ paths to make the resulting path truly relative in virtually every
532−-- case. The only exception is on Windows when the two paths are on different
533−-- drives. In this case the resulting path may be absolute.
534−relativeTo :: FilePath -> FilePath -> FilePath
535−relativeTo dir base
536− = bifoldr1 (</>)
537− $ bimap (collapse . (".." <$) . catMaybes) (collapse . catMaybes)
538− $ unzip
539− $ dropWhile (\(a,b) -> a == b)
540− $ zipDefaultWith Nothing Nothing (,)
541− (map Just $ splitDirectories base)
542− (map Just $ splitDirectories dir)
543− where collapse = foldr (</>) ""