packages feed

cabal-cache 1.0.0.2 → 1.0.0.3

raw patch · 11 files changed

+299/−96 lines, 11 filesdep +topographPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: topograph

API changes (from Hackage documentation)

+ HaskellWorks.CabalCache.Concurrent.DownloadQueue: runQueue :: MonadIO m => DownloadQueue -> (PackageId -> m Bool) -> m ()
+ HaskellWorks.CabalCache.Concurrent.Fork: forkThreadsWait :: Int -> IO () -> IO ()
+ HaskellWorks.CabalCache.Concurrent.Type: [$sel:tFailures:DownloadQueue] :: DownloadQueue -> TVar (Set PackageId)
+ HaskellWorks.CabalCache.Topology: PlanData :: Set PackageId -> PlanData
+ HaskellWorks.CabalCache.Topology: [nonShareable] :: PlanData -> Set PackageId
+ HaskellWorks.CabalCache.Topology: buildPlanData :: PlanJson -> [PackageId] -> PlanData
+ HaskellWorks.CabalCache.Topology: canShare :: PlanData -> PackageId -> Bool
+ HaskellWorks.CabalCache.Topology: instance GHC.Generics.Generic HaskellWorks.CabalCache.Topology.PlanData
+ HaskellWorks.CabalCache.Topology: newtype PlanData
+ HaskellWorks.CabalCache.Types: Components :: Maybe Lib -> Components
+ HaskellWorks.CabalCache.Types: Lib :: [Text] -> [Text] -> Lib
+ HaskellWorks.CabalCache.Types: [$sel:components:Package] :: Package -> Maybe Components
+ HaskellWorks.CabalCache.Types: [$sel:depends:Lib] :: Lib -> [Text]
+ HaskellWorks.CabalCache.Types: [$sel:exeDepends:Lib] :: Lib -> [Text]
+ HaskellWorks.CabalCache.Types: [$sel:exeDepends:Package] :: Package -> [Text]
+ HaskellWorks.CabalCache.Types: [$sel:lib:Components] :: Components -> Maybe Lib
+ HaskellWorks.CabalCache.Types: data Lib
+ HaskellWorks.CabalCache.Types: instance Data.Aeson.Types.FromJSON.FromJSON HaskellWorks.CabalCache.Types.Components
+ HaskellWorks.CabalCache.Types: instance Data.Aeson.Types.FromJSON.FromJSON HaskellWorks.CabalCache.Types.Lib
+ HaskellWorks.CabalCache.Types: instance GHC.Classes.Eq HaskellWorks.CabalCache.Types.Components
+ HaskellWorks.CabalCache.Types: instance GHC.Classes.Eq HaskellWorks.CabalCache.Types.Lib
+ HaskellWorks.CabalCache.Types: instance GHC.Generics.Generic HaskellWorks.CabalCache.Types.Components
+ HaskellWorks.CabalCache.Types: instance GHC.Generics.Generic HaskellWorks.CabalCache.Types.Lib
+ HaskellWorks.CabalCache.Types: instance GHC.Show.Show HaskellWorks.CabalCache.Types.Components
+ HaskellWorks.CabalCache.Types: instance GHC.Show.Show HaskellWorks.CabalCache.Types.Lib
+ HaskellWorks.CabalCache.Types: newtype Components
+ HaskellWorks.CabalCache.Types: type CompilerId = Text
- HaskellWorks.CabalCache.Concurrent.Type: DownloadQueue :: TVar (Relation ConsumerId ProviderId) -> TVar (Set PackageId) -> DownloadQueue
+ HaskellWorks.CabalCache.Concurrent.Type: DownloadQueue :: TVar (Relation ConsumerId ProviderId) -> TVar (Set PackageId) -> TVar (Set PackageId) -> DownloadQueue
- HaskellWorks.CabalCache.IO.File: listMaybeDirectory :: MonadIO m => FilePath -> ExceptT String m [FilePath]
+ HaskellWorks.CabalCache.IO.File: listMaybeDirectory :: MonadIO m => FilePath -> m [FilePath]
- HaskellWorks.CabalCache.Types: Package :: Text -> Text -> Text -> Text -> Maybe Text -> Maybe Text -> Maybe [PackageId] -> Package
+ HaskellWorks.CabalCache.Types: Package :: Text -> PackageId -> Text -> Text -> Maybe Text -> Maybe Text -> Maybe Components -> [Text] -> [Text] -> Package
- HaskellWorks.CabalCache.Types: PlanJson :: Text -> [Package] -> PlanJson
+ HaskellWorks.CabalCache.Types: PlanJson :: CompilerId -> [Package] -> PlanJson
- HaskellWorks.CabalCache.Types: [$sel:compilerId:PlanJson] :: PlanJson -> Text
+ HaskellWorks.CabalCache.Types: [$sel:compilerId:PlanJson] :: PlanJson -> CompilerId
- HaskellWorks.CabalCache.Types: [$sel:depends:Package] :: Package -> Maybe [PackageId]
+ HaskellWorks.CabalCache.Types: [$sel:depends:Package] :: Package -> [Text]
- HaskellWorks.CabalCache.Types: [$sel:id:Package] :: Package -> Text
+ HaskellWorks.CabalCache.Types: [$sel:id:Package] :: Package -> PackageId

Files

cabal-cache.cabal view
@@ -1,7 +1,7 @@ cabal-version:          2.2  name:                   cabal-cache-version:                1.0.0.2+version:                1.0.0.3 synopsis:               CI Assistant for Haskell projects description:            CI Assistant for Haskell projects.  Implements package caching. homepage:               https://github.com/haskell-works/cabal-cache@@ -54,6 +54,7 @@ common temporary                      { build-depends: temporary                      >= 1.3        && < 1.4    } common text                           { build-depends: text                           >= 1.2.3.1    && < 1.3    } common time                           { build-depends: time                           >= 1.4        && < 1.10   }+common topograph                      { build-depends: topograph                      >= 1          && < 2      } common unliftio                       { build-depends: unliftio                       >= 0.2.10     && < 0.3    } common zlib                           { build-depends: zlib                           >= 0.6.2      && < 0.7    } @@ -92,6 +93,7 @@           , temporary           , text           , time+          , topograph           , unliftio           , zlib   other-modules:        Paths_cabal_cache@@ -107,6 +109,7 @@       App.Static       HaskellWorks.CabalCache.AWS.Env       HaskellWorks.CabalCache.Concurrent.DownloadQueue+      HaskellWorks.CabalCache.Concurrent.Fork       HaskellWorks.CabalCache.Concurrent.Type       HaskellWorks.CabalCache.Core       HaskellWorks.CabalCache.Data.Relation@@ -123,6 +126,7 @@       HaskellWorks.CabalCache.Options       HaskellWorks.CabalCache.Show       HaskellWorks.CabalCache.Text+      HaskellWorks.CabalCache.Topology       HaskellWorks.CabalCache.Types       HaskellWorks.CabalCache.Version @@ -150,7 +154,7 @@           , lens           , raw-strings-qq           , text-        +   type:                 exitcode-stdio-1.0   main-is:              Spec.hs   build-depends:        cabal-cache
src/App/Commands/SyncFromArchive.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds           #-} {-# LANGUAGE OverloadedStrings   #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections       #-} {-# LANGUAGE TypeApplications    #-}  module App.Commands.SyncFromArchive@@ -18,6 +19,7 @@ import Control.Monad.Trans.Resource     (runResourceT) import Data.ByteString.Lazy.Search      (replace) import Data.Generics.Product.Any        (the)+import Data.List                        (nub, sort) import Data.Maybe import Data.Semigroup                   ((<>)) import Data.Text                        (Text)@@ -26,34 +28,45 @@ import HaskellWorks.CabalCache.Location ((<.>), (</>)) import HaskellWorks.CabalCache.Metadata (deleteMetadata, loadMetadata) import HaskellWorks.CabalCache.Show+import HaskellWorks.CabalCache.Topology (buildPlanData) import HaskellWorks.CabalCache.Version  (archiveVersion) import Network.AWS.Types                (Region (Oregon)) import Options.Applicative              hiding (columns) import System.Directory                 (createDirectoryIfMissing, doesDirectoryExist) -import qualified App.Commands.Options.Types         as Z-import qualified Codec.Archive.Tar                  as F-import qualified Codec.Compression.GZip             as F-import qualified Data.ByteString                    as BS-import qualified Data.ByteString.Char8              as C8-import qualified Data.ByteString.Lazy               as LBS-import qualified Data.Map.Strict                    as Map-import qualified Data.Text                          as T-import qualified HaskellWorks.CabalCache.AWS.Env    as AWS-import qualified HaskellWorks.CabalCache.GhcPkg     as GhcPkg-import qualified HaskellWorks.CabalCache.Hash       as H-import qualified HaskellWorks.CabalCache.IO.Console as CIO-import qualified HaskellWorks.CabalCache.IO.Lazy    as IO-import qualified HaskellWorks.CabalCache.IO.Tar     as IO-import qualified HaskellWorks.CabalCache.Types      as Z-import qualified System.Directory                   as IO-import qualified System.IO                          as IO-import qualified System.IO.Temp                     as IO-import qualified UnliftIO.Async                     as IO+import qualified App.Commands.Options.Types                       as Z+import qualified Codec.Archive.Tar                                as F+import qualified Codec.Compression.GZip                           as F+import qualified Control.Concurrent                               as IO+import qualified Control.Concurrent.STM                           as STM+import qualified Data.ByteString                                  as BS+import qualified Data.ByteString.Char8                            as C8+import qualified Data.ByteString.Lazy                             as LBS+import qualified Data.Map                                         as M+import qualified Data.Map.Strict                                  as Map+import qualified Data.Set                                         as S+import qualified Data.Text                                        as T+import qualified HaskellWorks.CabalCache.AWS.Env                  as AWS+import qualified HaskellWorks.CabalCache.Concurrent.DownloadQueue as DQ+import qualified HaskellWorks.CabalCache.Concurrent.Fork          as IO+import qualified HaskellWorks.CabalCache.Data.Relation            as R+import qualified HaskellWorks.CabalCache.GhcPkg                   as GhcPkg+import qualified HaskellWorks.CabalCache.Hash                     as H+import qualified HaskellWorks.CabalCache.IO.Console               as CIO+import qualified HaskellWorks.CabalCache.IO.Lazy                  as IO+import qualified HaskellWorks.CabalCache.IO.Tar                   as IO+import qualified HaskellWorks.CabalCache.Types                    as Z+import qualified System.Directory                                 as IO+import qualified System.IO                                        as IO+import qualified System.IO.Temp                                   as IO+import qualified UnliftIO.Async                                   as IO  {-# ANN module ("HLint: ignore Reduce duplication"  :: String) #-} {-# ANN module ("HLint: ignore Redundant do"        :: String) #-} +skippable :: Z.Package -> Bool+skippable package = (package ^. the @"packageType" == "pre-existing")+ runSyncFromArchive :: Z.SyncFromArchiveOptions -> IO () runSyncFromArchive opts = do   let storePath           = opts ^. the @"storePath"@@ -80,9 +93,6 @@       let compilerId                  = planJson ^. the @"compilerId"       let archivePath                 = versionedArchiveUri </> compilerId       let storeCompilerPath           = storePath </> T.unpack compilerId-      -- xx let scopedArchivePath           = scopedArchiveUri </> compilerId-      -- xx let baseDir                     = opts ^. the @"storePath"-      -- xx let storeCompilerPath           = baseDir </> T.unpack compilerId       let storeCompilerPackageDbPath  = storeCompilerPath </> "package.db"       let storeCompilerLibPath        = storeCompilerPath </> "lib" @@ -99,57 +109,110 @@        packages <- getPackages storePath planJson +      let installPlan = planJson ^. the @"installPlan"+      let planPackages = M.fromList $ fmap (\p -> (p ^. the @"id", p)) installPlan++      let planData = buildPlanData planJson (packages ^.. each . the @"packageId")++      let planDeps0 = installPlan >>= \p -> fmap (p ^. the @"id", ) $ mempty+            <> (p ^. the @"depends")+            <> (p ^. the @"exeDepends")+            <> (p ^.. the @"components" . each . the @"lib" . each . the @"depends"    . each)+            <> (p ^.. the @"components" . each . the @"lib" . each . the @"exeDepends" . each)+      let planDeps  = planDeps0 <> fmap (\p -> ("[universe]", p ^. the @"id")) installPlan++      downloadQueue <- STM.atomically $ DQ.createDownloadQueue planDeps++      let pInfos = M.fromList $ fmap (\p -> (p ^. the @"packageId", p)) packages++      -- forM_ planDeps $ \(a, b) -> do+      --   let maybeName = M.lookup a planPackages <&> (^. the @"name")+      --   case maybeName of+      --     Just name -> CIO.putStrLn $ name <> " " <> a <> " -> " <> b+      --     Nothing   -> CIO.putStrLn $ "*********" <> a <> " -> " <> b+       IO.withSystemTempDirectory "cabal-cache" $ \tempPath -> do         IO.createDirectoryIfMissing True (tempPath </> T.unpack compilerId </> "package.db") -        IO.pooledForConcurrentlyN_ threads packages $ \pInfo -> do-          let archiveBaseName   = packageDir pInfo <.> ".tar.gz"-          let archiveFile       = versionedArchiveUri </> T.pack archiveBaseName-          let scopedArchiveFile = scopedArchiveUri </> T.pack archiveBaseName-          let packageStorePath  = storePath </> packageDir pInfo-          storeDirectoryExists <- doesDirectoryExist packageStorePath-          unless storeDirectoryExists $ do-            maybeExistingArchiveFile <- IO.firstExistingResource envAws [scopedArchiveFile, archiveFile]-            forM_ maybeExistingArchiveFile $ \existingArchiveFile -> do-              CIO.putStrLn $ "Extracting: " <> toText existingArchiveFile-              void $ runResAws envAws $ onErrorClean packageStorePath $ do-                maybeArchiveFileContents <- IO.readResource envAws existingArchiveFile+        IO.forkThreadsWait threads $ DQ.runQueue downloadQueue $ \packageId -> case M.lookup packageId pInfos of+          Just pInfo -> do+            let archiveBaseName   = packageDir pInfo <.> ".tar.gz"+            let archiveFile       = versionedArchiveUri </> T.pack archiveBaseName+            let scopedArchiveFile = scopedArchiveUri </> T.pack archiveBaseName+            let packageStorePath  = storePath </> packageDir pInfo+            storeDirectoryExists <- doesDirectoryExist packageStorePath+            let maybePackage = M.lookup packageId planPackages -                case maybeArchiveFileContents of-                  Just archiveFileContents -> do-                    existingArchiveFileContents <- IO.readResource envAws existingArchiveFile & maybeToExceptM ("Archive unavailable: " <> show (toText archiveFile))-                    let tempArchiveFile = tempPath </> archiveBaseName-                    liftIO $ LBS.writeFile tempArchiveFile existingArchiveFileContents-                    IO.extractTar tempArchiveFile storePath+            case maybePackage of+              Nothing -> do+                CIO.hPutStrLn IO.stderr $ "Warning: package not found" <> packageId+                return True+              Just package -> if skippable package+                then do+                  CIO.putStrLn $ "Skipping: " <> packageId+                  return True+                else if storeDirectoryExists+                  then return True+                  else do+                    maybeExistingArchiveFile <- IO.firstExistingResource envAws [scopedArchiveFile, archiveFile]+                    case maybeExistingArchiveFile of+                      Just existingArchiveFile -> do+                        CIO.putStrLn $ "Extracting: " <> toText existingArchiveFile+                        runResAws envAws $ onErrorClean packageStorePath False $ do+                          maybeArchiveFileContents <- IO.readResource envAws existingArchiveFile -                    meta <- loadMetadata packageStorePath-                    oldStorePath <- maybeToExcept "store-path is missing from Metadata" (Map.lookup "store-path" meta)+                          case maybeArchiveFileContents of+                            Just archiveFileContents -> do+                              existingArchiveFileContents <- IO.readResource envAws existingArchiveFile & maybeToExceptM ("Archive unavailable: " <> show (toText archiveFile))+                              let tempArchiveFile = tempPath </> archiveBaseName+                              liftIO $ LBS.writeFile tempArchiveFile existingArchiveFileContents+                              IO.extractTar tempArchiveFile storePath -                    case confPath pInfo of-                      Tagged conf _ -> do-                        let theConfPath = storePath </> conf-                        let tempConfPath = tempPath </> conf-                        confPathExists <- liftIO $ IO.doesFileExist theConfPath-                        when confPathExists $ do-                          confContents <- liftIO $ LBS.readFile theConfPath-                          liftIO $ LBS.writeFile tempConfPath (replace (LBS.toStrict oldStorePath) (C8.pack storePath) confContents)-                          liftIO $ IO.renamePath tempConfPath theConfPath-                  Nothing -> do-                    CIO.putStrLn $ "Archive unavailable: " <> toText existingArchiveFile+                              meta <- loadMetadata packageStorePath+                              oldStorePath <- maybeToExcept "store-path is missing from Metadata" (Map.lookup "store-path" meta) -                    deleteMetadata packageStorePath+                              case confPath pInfo of+                                Tagged conf _ -> do+                                  let theConfPath = storePath </> conf+                                  let tempConfPath = tempPath </> conf+                                  confPathExists <- liftIO $ IO.doesFileExist theConfPath+                                  when confPathExists $ do+                                    confContents <- liftIO $ LBS.readFile theConfPath+                                    liftIO $ LBS.writeFile tempConfPath (replace (LBS.toStrict oldStorePath) (C8.pack storePath) confContents)+                                    liftIO $ IO.renamePath tempConfPath theConfPath+                                  return True+                            Nothing -> do+                              CIO.putStrLn $ "Archive unavailable: " <> toText existingArchiveFile+                              deleteMetadata packageStorePath+                              return False+                      Nothing -> do+                        CIO.hPutStrLn IO.stderr $ "Warning: Sync failure: " <> packageId+                        return False+          Nothing -> do+            CIO.hPutStrLn IO.stderr $ "Warning: Invalid package id: " <> packageId+            return True +      dependenciesRemaining <- STM.atomically $ STM.readTVar (downloadQueue ^. the @"tDependencies")+       CIO.putStrLn "Recaching package database"       GhcPkg.recache storeCompilerPackageDbPath +      failures <- STM.atomically $ STM.readTVar $ downloadQueue ^. the @"tFailures"++      forM_ failures $ \packageId -> CIO.hPutStrLn IO.stderr $ "Failed to download: " <> packageId+     Left errorMessage -> do       CIO.hPutStrLn IO.stderr $ "ERROR: Unable to parse plan.json file: " <> T.pack errorMessage    return () -onErrorClean :: MonadIO m => FilePath -> ExceptT String m () -> m ()-onErrorClean pkgStorePath f =-  void $ runExceptT $ exceptWarn f `catchError` (\e -> liftIO $ IO.removeDirectoryRecursive pkgStorePath)+onErrorClean :: MonadIO m => FilePath -> a -> ExceptT String m a -> m a+onErrorClean pkgStorePath failureValue f = do+  result <- runExceptT $ catchError (exceptWarn f) handler+  case result of+    Left a  -> return failureValue+    Right a -> return a+  where handler e = liftIO (IO.removeDirectoryRecursive pkgStorePath) >> return failureValue  cmdSyncFromArchive :: Mod CommandFields (IO ()) cmdSyncFromArchive = command "sync-from-archive"  $ flip info idm $ runSyncFromArchive <$> optsSyncFromArchive
src/App/Commands/SyncToArchive.hs view
@@ -12,7 +12,7 @@ import App.Commands.Options.Parser      (optsSyncToArchive) import App.Static                       (homeDirectory) import Control.Lens                     hiding ((<.>))-import Control.Monad                    (unless, when)+import Control.Monad                    (filterM, unless, when) import Control.Monad.Except import Control.Monad.Trans.Resource     (runResourceT) import Data.Generics.Product.Any        (the)@@ -23,6 +23,7 @@ import HaskellWorks.CabalCache.Location ((<.>), (</>)) import HaskellWorks.CabalCache.Metadata (createMetadata) import HaskellWorks.CabalCache.Show+import HaskellWorks.CabalCache.Topology (buildPlanData, canShare) import HaskellWorks.CabalCache.Version  (archiveVersion) import Options.Applicative              hiding (columns) import System.Directory                 (createDirectoryIfMissing, doesDirectoryExist)@@ -32,6 +33,7 @@ import qualified Codec.Compression.GZip             as F import qualified Data.ByteString.Lazy               as LBS import qualified Data.ByteString.Lazy.Char8         as LC8+import qualified Data.Set                           as Set import qualified Data.Text                          as T import qualified Data.Text.Encoding                 as T import qualified HaskellWorks.CabalCache.AWS.Env    as AWS@@ -80,7 +82,9 @@       IO.createLocalDirectoryIfMissing scopedArchivePath       CIO.putStrLn "Extracting package list" -      packages <- getPackages storePath planJson+      packages     <- getPackages storePath planJson+      nonShareable <- packages & filterM (fmap not . isShareable storePath)+      let planData = buildPlanData planJson (nonShareable ^.. each . the @"packageId")        let storeCompilerPath           = storePath </> T.unpack compilerId       let storeCompilerPackageDbPath  = storeCompilerPath </> "package.db"@@ -121,14 +125,18 @@                liftIO (LBS.readFile tempArchiveFile >>= IO.writeResource envAws scopedArchiveFile) -              shareEntries <- (\\ ["doc"]) <$> IO.listMaybeDirectory packageSharePath--              when (null shareEntries) $ IO.linkOrCopyResource envAws scopedArchiveFile archiveFile+              when (canShare planData (packageId pInfo)) $+                IO.linkOrCopyResource envAws scopedArchiveFile archiveFile      Left errorMessage -> do       CIO.hPutStrLn IO.stderr $ "ERROR: Unable to parse plan.json file: " <> T.pack errorMessage    return ()++isShareable :: MonadIO m => FilePath -> PackageInfo -> m Bool+isShareable storePath pkg =+  let packageSharePath = storePath </> packageDir pkg </> "share"+  in IO.listMaybeDirectory packageSharePath <&> (\\ ["doc"]) <&> null  cmdSyncToArchive :: Mod CommandFields (IO ()) cmdSyncToArchive = command "sync-to-archive"  $ flip info idm $ runSyncToArchive <$> optsSyncToArchive
src/HaskellWorks/CabalCache/Concurrent/DownloadQueue.hs view
@@ -1,9 +1,11 @@-{-# LANGUAGE DataKinds       #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE DataKinds         #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}  module HaskellWorks.CabalCache.Concurrent.DownloadQueue   ( createDownloadQueue   , anchor+  , runQueue   ) where  import Control.Lens@@ -18,6 +20,7 @@ import qualified Data.Text                               as T import qualified HaskellWorks.CabalCache.Concurrent.Type as Z import qualified HaskellWorks.CabalCache.Data.Relation   as R+import qualified HaskellWorks.CabalCache.IO.Console      as CIO  anchor :: Z.PackageId -> M.Map Z.ConsumerId Z.ProviderId -> M.Map Z.ConsumerId Z.ProviderId anchor root dependencies = M.union dependencies $ M.singleton root (mconcat (M.elems dependencies))@@ -26,20 +29,24 @@ createDownloadQueue dependencies = do   tDependencies <- STM.newTVar (R.fromList dependencies)   tUploading    <- STM.newTVar S.empty+  tFailures     <- STM.newTVar S.empty   return Z.DownloadQueue {..}  takeReady :: Z.DownloadQueue -> STM.STM (Maybe Z.PackageId) takeReady Z.DownloadQueue {..} = do   dependencies  <- STM.readTVar tDependencies   uploading     <- STM.readTVar tUploading+  failures      <- STM.readTVar tFailures -  let ready = R.range dependencies \\ R.domain dependencies \\ uploading+  let ready = R.range dependencies \\ R.domain dependencies \\ uploading \\ failures    case S.lookupMin ready of     Just packageId -> do       STM.writeTVar tUploading (S.insert packageId uploading)       return (Just packageId)-    Nothing -> return Nothing+    Nothing -> if S.null (R.range dependencies \\ R.domain dependencies \\ failures)+      then return Nothing+      else STM.retry  commit :: Z.DownloadQueue -> Z.PackageId -> STM.STM () commit Z.DownloadQueue {..} packageId = do@@ -49,12 +56,25 @@   STM.writeTVar tUploading    $ S.delete packageId uploading   STM.writeTVar tDependencies $ R.withoutRange (S.singleton packageId) dependencies -runQueue :: MonadIO m => Z.DownloadQueue -> (Z.PackageId -> m ()) -> m ()+failDownload :: Z.DownloadQueue -> Z.PackageId -> STM.STM ()+failDownload Z.DownloadQueue {..} packageId = do+  uploading <- STM.readTVar tUploading+  failures  <- STM.readTVar tFailures++  STM.writeTVar tUploading  $ S.delete packageId uploading+  STM.writeTVar tFailures   $ S.insert packageId failures++runQueue :: MonadIO m => Z.DownloadQueue -> (Z.PackageId -> m Bool) -> m () runQueue downloadQueue@Z.DownloadQueue {..} f = do   maybePackageId <- liftIO $ STM.atomically $ takeReady downloadQueue    case maybePackageId of     Just packageId -> do-      f packageId-      liftIO $ STM.atomically $ commit downloadQueue packageId-    Nothing -> return ()+      success <- f packageId+      if success+        then liftIO $ STM.atomically $ commit downloadQueue packageId+        else liftIO $ STM.atomically $ failDownload downloadQueue packageId+      runQueue downloadQueue f++    Nothing -> do+      return ()
+ src/HaskellWorks/CabalCache/Concurrent/Fork.hs view
@@ -0,0 +1,17 @@+module HaskellWorks.CabalCache.Concurrent.Fork where++import Control.Monad++import qualified Control.Concurrent     as IO+import qualified Control.Concurrent.STM as STM++forkThreadsWait :: Int -> IO () -> IO ()+forkThreadsWait n f = do+  tDone <- STM.atomically $ STM.newTVar (0 :: Int)+  threads <- forM [1 .. n] $ \_ -> IO.forkIO $ do+    f+    STM.atomically $ STM.modifyTVar tDone (+1)++  STM.atomically $ do+    done <- STM.readTVar tDone+    when (done < n) STM.retry
src/HaskellWorks/CabalCache/Concurrent/Type.hs view
@@ -24,4 +24,5 @@ data DownloadQueue = DownloadQueue   { tDependencies :: STM.TVar (R.Relation ConsumerId ProviderId)   , tUploading    :: STM.TVar (S.Set PackageId)+  , tFailures     :: STM.TVar (S.Set PackageId)   } deriving Generic
src/HaskellWorks/CabalCache/Core.hs view
@@ -33,8 +33,6 @@ import qualified HaskellWorks.CabalCache.Types  as Z import qualified System.Directory               as IO -type CompilerId = Text-type PackageId  = Text type PackageDir = FilePath type ConfPath   = FilePath type Library    = FilePath@@ -47,8 +45,8 @@   } deriving (Eq, Show, Generic, NFData)  data PackageInfo = PackageInfo-  { compilerId :: CompilerId-  , packageId  :: PackageId+  { compilerId :: Z.CompilerId+  , packageId  :: Z.PackageId   , packageDir :: PackageDir   , confPath   :: Tagged ConfPath Presence   , libs       :: [Library]@@ -68,16 +66,16 @@   where compilerId :: Text         compilerId = planJson ^. the @"compilerId"         packages :: [Z.Package]-        packages = planJson ^.. the @"installPlan" . each . filtered predicate+        packages = planJson ^. the @"installPlan"         predicate :: Z.Package -> Bool-        predicate package = package ^. the @"packageType" /= "pre-existing" && package ^. the @"style" == Just "global"+        predicate package = True  loadPlan :: IO (Either String Z.PlanJson) loadPlan =   eitherDecode <$> LBS.readFile ("dist-newstyle" </> "cache" </> "plan.json")  --------------------------------------------------------------------------------mkPackageInfo :: FilePath -> CompilerId -> Z.Package -> IO PackageInfo+mkPackageInfo :: FilePath -> Z.CompilerId -> Z.Package -> IO PackageInfo mkPackageInfo basePath cid pkg = do   let pid               = pkg ^. the @"id"   let compilerPath      = basePath </> T.unpack cid
src/HaskellWorks/CabalCache/IO/File.hs view
@@ -24,7 +24,7 @@     IO.ExitSuccess   -> return ()     IO.ExitFailure n -> throwError "" -listMaybeDirectory :: MonadIO m => FilePath -> ExceptT String m [FilePath]+listMaybeDirectory :: MonadIO m => FilePath -> m [FilePath] listMaybeDirectory filepath = do   exists <- liftIO $ IO.doesDirectoryExist filepath   if exists
+ src/HaskellWorks/CabalCache/Topology.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE DataKinds        #-}+{-# LANGUAGE DeriveGeneric    #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE TypeApplications #-}++module HaskellWorks.CabalCache.Topology+  ( PlanData(..)+  , buildPlanData+  , canShare+  ) where++import Control.Arrow                 ((&&&))+import Control.Lens                  (each, set, view, (&), (.~), (<&>), (^.), (^..))+import Control.Monad                 (join)+import Data.Either                   (fromRight)+import Data.Generics.Product.Any     (the)+import Data.Map.Strict               (Map)+import Data.Maybe                    (fromMaybe, mapMaybe)+import Data.Set                      (Set)+import Data.Text                     (Text)+import GHC.Generics                  (Generic)+import HaskellWorks.CabalCache.Types (Package, PackageId, PlanJson)++import qualified Data.List       as L+import qualified Data.Map.Strict as M+import qualified Data.Set        as S+import qualified Topograph       as TG++newtype PlanData = PlanData+  { nonShareable :: Set PackageId+  } deriving Generic++buildPlanData :: PlanJson   -- ^ The original plan+  -> [PackageId]            -- ^ Packages that are known to be non-shareable+  -> PlanData               -- ^ Updated plan+buildPlanData plan nonShareablePkgs =+  let dm = dependenciesMap (plan ^. the @"installPlan")+  in buildPlanData' dm nonShareablePkgs++canShare :: PlanData -> PackageId -> Bool+canShare planData pkgId = S.notMember pkgId (nonShareable planData)++-------------------------------------------------------------------------------++dependenciesMap :: [Package] -> Map PackageId (Set PackageId)+dependenciesMap plan = plan+  <&> (view (the @"id") &&& view (the @"depends"))+  <&> fmap S.fromList & M.fromList++buildPlanData' :: Map PackageId (Set PackageId) -- ^ Dependencies map+  -> [PackageId]                                -- ^ Packages to exclude+  -> PlanData                                   -- ^ All package ids to exclude+buildPlanData' plan knownNonShareable =+  fromRight (error "Could not process dependencies") $+    TG.runG plan $ \g ->+      let tg        = TG.transpose g+          nsPaths   = concatMap (fromMaybe [] . paths tg) knownNonShareable+          nsAll     = S.fromList (join nsPaths)+          dMap      = TG.adjacencyMap (TG.reduction g)+          rdMap     = TG.adjacencyMap (TG.reduction tg)+      in PlanData { nonShareable = nsAll }+  where paths g x = (fmap . fmap . fmap) (TG.gFromVertex g) $ TG.dfs g <$> TG.gToVertex g x
src/HaskellWorks/CabalCache/Types.hs view
@@ -1,41 +1,68 @@+{-# LANGUAGE ApplicativeDo         #-} {-# LANGUAGE DeriveGeneric         #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE RecordWildCards       #-}  module HaskellWorks.CabalCache.Types where  import Data.Aeson+import Data.Maybe   (fromMaybe) import Data.Text    (Text) import GHC.Generics -type PackageId = Text+type CompilerId = Text+type PackageId  = Text  data PlanJson = PlanJson-  { compilerId  :: Text+  { compilerId  :: CompilerId   , installPlan :: [Package]   } deriving (Eq, Show, Generic)  data Package = Package   { packageType   :: Text-  , id            :: Text+  , id            :: PackageId   , name          :: Text   , version       :: Text   , style         :: Maybe Text   , componentName :: Maybe Text-  , depends       :: Maybe [PackageId]+  , components    :: Maybe Components+  , depends       :: [Text]+  , exeDepends    :: [Text]   } deriving (Eq, Show, Generic) +newtype Components = Components+  { lib :: Maybe Lib+  } deriving (Eq, Show, Generic)++data Lib = Lib+  { depends    :: [Text]+  , exeDepends :: [Text]+  } deriving (Eq, Show, Generic)+ instance FromJSON PlanJson where   parseJSON = withObject "PlanJson" $ \v -> PlanJson     <$> v .: "compiler-id"     <*> v .: "install-plan"  instance FromJSON Package where-  parseJSON = withObject "Package" $ \v -> Package-    <$> v .:  "type"-    <*> v .:  "id"-    <*> v .:  "pkg-name"-    <*> v .:  "pkg-version"-    <*> v .:? "style"-    <*> v .:? "component-name"-    <*> v .:? "depends"+  parseJSON = withObject "Package" $ \v -> do+    packageType   <- v .:  "type"+    id            <- v .:  "id"+    name          <- v .:  "pkg-name"+    version       <- v .:  "pkg-version"+    style         <- v .:? "style"+    componentName <- v .:? "component-name"+    components    <- v .:? "components"+    depends       <- v .:? "depends"     .!= []+    exeDepends    <- v .:? "exe-depends" .!= []+    return Package {..}++instance FromJSON Components where+  parseJSON = withObject "Components" $ \v -> Components+    <$> v .:? "lib"++instance FromJSON Lib where+  parseJSON = withObject "Lib" $ \v -> Lib+    <$> v .:? "depends"     .!= []+    <*> v .:? "exe-depends" .!= []
test/HaskellWorks/CabalCache/QuerySpec.hs view
@@ -1,6 +1,7 @@-{-# LANGUAGE DataKinds         #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE OverloadedStrings     #-}+{-# LANGUAGE QuasiQuotes           #-}  module HaskellWorks.CabalCache.QuerySpec   ( spec@@ -35,10 +36,12 @@           , Z.version       = "2.4.0.1"           , Z.style         = Nothing           , Z.componentName = Nothing-          , Z.depends       = Just+          , Z.components    = Nothing+          , Z.depends       =             [ "array-0.5.3.0"             , "base-4.12.0.0"             ]+          , Z.exeDepends    = []           }         ]       }