packages feed

cabal-cache 0.2.0.2 → 1.0.0.0

raw patch · 16 files changed

+455/−176 lines, 16 filesdep +containersdep +cryptonitePVP ok

version bump matches the API change (PVP)

Dependencies added: containers, cryptonite

API changes (from Hackage documentation)

- HaskellWorks.Ci.Assist.Core: relativePaths2 :: FilePath -> FilePath -> PackageInfo -> [TarGroup]
- HaskellWorks.Ci.Assist.PackageConfig: storePathMacro :: ByteString
- HaskellWorks.Ci.Assist.PackageConfig: templateConfig :: FilePath -> ByteString -> ByteString
- HaskellWorks.Ci.Assist.PackageConfig: unTemplateConfig :: FilePath -> ByteString -> ByteString
- HaskellWorks.Ci.Assist.Tar: mapEntriesWith :: (FilePath -> Bool) -> (ByteString -> ByteString) -> Entries e -> Entries e
- HaskellWorks.Ci.Assist.Tar: updateEntryWith :: (FilePath -> Bool) -> (ByteString -> ByteString) -> Entry -> Entry
+ App.Commands.Options.Types: [$sel:storePathHash:SyncFromArchiveOptions] :: SyncFromArchiveOptions -> Maybe String
+ App.Commands.Options.Types: [$sel:storePathHash:SyncToArchiveOptions] :: SyncToArchiveOptions -> Maybe String
+ HaskellWorks.Ci.Assist.Hash: hashStorePath :: String -> String
+ HaskellWorks.Ci.Assist.IO.Error: maybeToExcept :: Monad m => String -> Maybe a -> ExceptT String m a
+ HaskellWorks.Ci.Assist.IO.Error: maybeToExceptM :: Monad m => String -> m (Maybe a) -> ExceptT String m a
+ HaskellWorks.Ci.Assist.IO.File: listMaybeDirectory :: MonadIO m => FilePath -> ExceptT String m [FilePath]
+ HaskellWorks.Ci.Assist.IO.Lazy: firstExistingResource :: (MonadUnliftIO m, MonadCatch m, MonadIO m) => Env -> [Location] -> m (Maybe Location)
+ HaskellWorks.Ci.Assist.IO.Lazy: linkOrCopyResource :: MonadUnliftIO m => Env -> Location -> Location -> ExceptT String m ()
+ HaskellWorks.Ci.Assist.Metadata: createMetadata :: MonadIO m => FilePath -> PackageInfo -> [(Text, ByteString)] -> m TarGroup
+ HaskellWorks.Ci.Assist.Metadata: deleteMetadata :: MonadIO m => FilePath -> m ()
+ HaskellWorks.Ci.Assist.Metadata: loadMetadata :: MonadIO m => FilePath -> m (Map Text ByteString)
+ HaskellWorks.Ci.Assist.Metadata: metaDir :: String
+ HaskellWorks.Ci.Assist.Version: archiveVersion :: IsString s => s
- App.Commands.Options.Types: SyncFromArchiveOptions :: Region -> Location -> FilePath -> Int -> SyncFromArchiveOptions
+ App.Commands.Options.Types: SyncFromArchiveOptions :: Region -> Location -> FilePath -> Maybe String -> Int -> SyncFromArchiveOptions
- App.Commands.Options.Types: SyncToArchiveOptions :: Region -> Location -> FilePath -> Int -> SyncToArchiveOptions
+ App.Commands.Options.Types: SyncToArchiveOptions :: Region -> Location -> FilePath -> Maybe String -> Int -> SyncToArchiveOptions
- HaskellWorks.Ci.Assist.Core: relativePaths :: PackageInfo -> [FilePath]
+ HaskellWorks.Ci.Assist.Core: relativePaths :: FilePath -> PackageInfo -> [TarGroup]
- HaskellWorks.Ci.Assist.IO.Error: exceptFatal :: MonadIO m => String -> ExceptT String m a -> ExceptT String m a
+ HaskellWorks.Ci.Assist.IO.Error: exceptFatal :: MonadIO m => ExceptT String m a -> ExceptT String m a
- HaskellWorks.Ci.Assist.IO.Error: exceptWarn :: MonadIO m => String -> ExceptT String m a -> ExceptT String m a
+ HaskellWorks.Ci.Assist.IO.Error: exceptWarn :: MonadIO m => ExceptT String m a -> ExceptT String m a
- HaskellWorks.Ci.Assist.IO.Lazy: resourceExists :: (MonadResource m, MonadCatch m, MonadIO m) => Env -> Location -> m Bool
+ HaskellWorks.Ci.Assist.IO.Lazy: resourceExists :: (MonadUnliftIO m, MonadCatch m, MonadIO m) => Env -> Location -> m Bool
- HaskellWorks.Ci.Assist.IO.Tar: createTar :: FilePath -> [TarGroup] -> ExceptT String IO ()
+ HaskellWorks.Ci.Assist.IO.Tar: createTar :: MonadIO m => FilePath -> [TarGroup] -> ExceptT String m ()
- HaskellWorks.Ci.Assist.IO.Tar: extractTar :: FilePath -> FilePath -> ExceptT String IO ()
+ HaskellWorks.Ci.Assist.IO.Tar: extractTar :: MonadIO m => FilePath -> FilePath -> ExceptT String m ()

Files

README.md view
@@ -1,2 +1,159 @@ # cabal-cache+[![master](https://circleci.com/gh/haskell-works/cabal-cache/tree/master.svg?style=svg)](https://circleci.com/gh/haskell-works/cabal-cache/tree/master)+ Tool for caching built cabal new-build packages.++The tool is useful in development when you want to share your build haskell package dependencies of+of a particular project with another developer and also in CI where caching is useful for reducing+build times.++`cabal-cache` supports syncing to an archive directory or to an S3 bucket.++## Installation++Several installation methods are available.++### From source++```bash+cabal new-install cabal-cache+```++### Ubuntu binaries++Dowload Ubuntu binaries from https://github.com/haskell-works/cabal-cache/releases++### Using Homebrew on Mac OS X++```bash+brew tap haskell-works/homebrew-haskell-works git@github.com:haskell-works/homebrew-haskell-works.git+brew update+brew install cabal-cache+```++## Example usage++Syncing built packages with S3 requires you have an S3 bucket with AWS+credentials stored in the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environent variables.+You should also know the AWS region the bucket was created in.++### Sync to archive++Change into your project directory.++Build the project with `cabal v2-build`.  This will ensure your dependencies are built and+will produce a `plan.json` file that is required for the `cabal-cache` tool to know which built+packages to sync up.++Run the following command to sync to S3.++```bash+cabal-cache sync-to-archive --threads 16 --archive-uri s3://my-cabal-cache-bucket/archive --region Sydney+```++Run the following command to sync to archive directory.++```bash+cabal-cache sync-to-archive --threads 16 --archive-uri archive --region Sydney+```++### Sync from S3++Change into your project directory.++Build the project with `cabal v2-configure`.  This will product a `plan.json` file that is required+for the `cabal-cache` tool to know which built packages to sync down.++Run the following command to sync from S3.++```bash+cabal-cache sync-from-archive --threads 16 --archive-uri s3://my-cabal-cache-bucket/archive --region Sydney+```++Run the following command to sync from archive directory.++```bash+cabal-cache sync-from-archive --threads 16 --archive-uri archive --region Sydney+```++## The archive++### Archive tarball format++Built packages are stored in tarballs which contain the following files:++```bash+x ${compiler_id}/${package_id}/_CC_METADATA/store-path+x ${compiler_id}/lib/libHS${package_id}-*.dylib+x ${compiler_id}/${package_id}+x ${compiler_id}/package.db/${package_id}.conf+```++Aside from the files in the `_CC_METADATA` directory, everything else is copied verbatim from cabal+store from the corresponding location.  This includes the `conf` file which may contain absolute paths+that would cause the built package to be non-relocatable.++As a work-around, the tarball also inclues the `_CC_METADATA/store-path`+file which stores the cabal store path from which the cached package was derived.++Upon unpacking, `cabal-cache` will rewrite the `conf` file to contain the new store path using the+information store in the `_CC_METADATA/store-path` file.  `_CC_METADATA` directory and its contents+will be additionally unpacked making it easy to recognise packages that have been restored using+`cabal-cache`.++### Archive directory structure++The archive contains files in the following locations:++```bash+/Users/jky/moo-archive/${archive_version}/${compiler_id}/${package_id}.tar.gz+/Users/jky/moo-archive/${archive_version}/${store_hash}/${compiler_id}/${package_id}.tar.gz+```++Both tarballs are identical.  If they both exist then the first may be a symlink to the second+when store on the filesystem.++The direct subdirectories of the archive is the `${archive_verson}`, for example `v1`.  This is the+version of the archive format.  This corresponds to the major version of the `cabal-cache` package.++The next directory may be the `${store_hash}` or the `${compiler_id}`.  If it is the `${store_hash}`+then the `${compiler_id}` will be a subdirectory of that.++The `${store_hash}` is the hash of the store path from which the cached package originally came.++`cabal-cache` will preferentially restore using this version if it is available and the `${store_hash}`+matches the cabal store path that is being restore to.++If the package matching the `${store_hash}` cannot be found, `cabal-cache` will fallback to the version+without the `${store_hash}`.++A version without a `${store-hash}` may not exist.  See [Caveats](#caveats) for more information.++## Caveats++### Packages that use absolute paths to the cabal store++Packages sometimes do things that cause their built artefacts to contain absolute paths to the cabal+store.  This unfortunately makes such built packages non-relocatable.++It is recommended that you use a fixed cabal store path rather than the default `$HOME/.cabal/store`+to avoid any potential issues.++See https://github.com/haskell/cabal/issues/4097 for more information.++Following are examples of how this might happen:++#### Paths_$pkgname++`Paths_$pkgname` modules have embedded within them the absolute path to the package in the cabal store+which means that packages that use some features of this module are not relocatable depending on what+they do.++Packages may query this module to get access to the package's cabal store `share` directory which+contains data files that the package can read at runtime.  Using `cabal-cache` for such packages+could mean that the package will be unable to find such data files.++To protect against this, `cabal-cache` will by default not sync packages down from the archive+if the package's cabal store `share` directory contain unusual files or directories _unless_ the+`${store_hash}` matches.  Currently it only considers the `doc` subdirectory to be usual.  More+exceptions may be added later.
cabal-cache.cabal view
@@ -1,7 +1,7 @@ cabal-version:          2.2  name:                   cabal-cache-version:                0.2.0.2+version:                1.0.0.0 synopsis:               CI Assistant for Haskell projects description:            CI Assistant for Haskell projects.  Implements package caching. homepage:               https://github.com/haskell-works/cabal-cache@@ -27,6 +27,8 @@ common antiope-s3           { build-depends: antiope-s3           >= 7.0.0      && < 8      } common bytestring           { build-depends: bytestring           >= 0.10.8.2   && < 0.11   } common conduit-extra        { build-depends: conduit-extra        >= 1.3.1.1    && < 1.4    }+common cryptonite           { build-depends: cryptonite           >= 0.25       && < 1      }+common containers           { build-depends: containers           >= 0.6.0.1    && < 0.7    } common deepseq              { build-depends: deepseq              >= 1.4.4.0    && < 1.5    } common directory            { build-depends: directory            >= 1.3.3.0    && < 1.4    } common exceptions           { build-depends: exceptions           >= 0.10.1     && < 0.11   }@@ -65,6 +67,8 @@           , antiope-s3           , bytestring           , conduit-extra+          , cryptonite+          , containers           , deepseq           , directory           , exceptions@@ -72,7 +76,6 @@           , generic-lens           , http-types           , lens-          , lens           , mtl           , optparse-applicative           , process@@ -97,6 +100,7 @@       App.Commands.Version       App.Static       HaskellWorks.Ci.Assist.Core+      HaskellWorks.Ci.Assist.Hash       HaskellWorks.Ci.Assist.IO.Console       HaskellWorks.Ci.Assist.GhcPkg       HaskellWorks.Ci.Assist.IO.Error@@ -104,12 +108,12 @@       HaskellWorks.Ci.Assist.IO.Lazy       HaskellWorks.Ci.Assist.IO.Tar       HaskellWorks.Ci.Assist.Location+      HaskellWorks.Ci.Assist.Metadata       HaskellWorks.Ci.Assist.Options-      HaskellWorks.Ci.Assist.PackageConfig       HaskellWorks.Ci.Assist.Show-      HaskellWorks.Ci.Assist.Tar       HaskellWorks.Ci.Assist.Text       HaskellWorks.Ci.Assist.Types+      HaskellWorks.Ci.Assist.Version  executable cabal-cache   import:   base, config
src/App/Commands/Options/Parser.hs view
@@ -1,10 +1,10 @@ {-# LANGUAGE OverloadedStrings #-}-module App.Commands.Options.Parser-where+module App.Commands.Options.Parser where  import Antiope.Core                    (FromText, Region (..), fromText) import App.Commands.Options.Types      (SyncFromArchiveOptions (..), SyncToArchiveOptions (..), VersionOptions (..)) import App.Static                      (homeDirectory)+import Control.Applicative import HaskellWorks.Ci.Assist.Location (Location (..), toLocation, (</>)) import Options.Applicative @@ -30,6 +30,13 @@       <>  metavar "DIRECTORY"       <>  value (homeDirectory </> ".cabal" </> "store")       )+  <*> optional+      ( strOption+        (   long "store-path-hash"+        <>  help "Store path hash (do not use)"+        <>  metavar "HASH"+        )+      )   <*> option auto       (   long "threads"       <>  help "Number of concurrent threads"@@ -56,6 +63,13 @@       <>  help "Path to cabal store"       <>  metavar "DIRECTORY"       <>  value (homeDirectory </> ".cabal" </> "store")+      )+  <*> optional+      ( strOption+        (   long "store-path-hash"+        <>  help "Store path hash (do not use)"+        <>  metavar "HASH"+        )       )   <*> option auto       (   long "threads"
src/App/Commands/Options/Types.hs view
@@ -11,17 +11,19 @@ import Network.AWS.Types               (Region)  data SyncToArchiveOptions = SyncToArchiveOptions-  { region     :: Region-  , archiveUri :: Location-  , storePath  :: FilePath-  , threads    :: Int+  { region        :: Region+  , archiveUri    :: Location+  , storePath     :: FilePath+  , storePathHash :: Maybe String+  , threads       :: Int   } deriving (Eq, Show, Generic)  data SyncFromArchiveOptions = SyncFromArchiveOptions-  { region     :: Region-  , archiveUri :: Location-  , storePath  :: FilePath-  , threads    :: Int+  { region        :: Region+  , archiveUri    :: Location+  , storePath     :: FilePath+  , storePathHash :: Maybe String+  , threads       :: Int   } deriving (Eq, Show, Generic)  data VersionOptions = VersionOptions deriving (Eq, Show, Generic)
src/App/Commands/SyncFromArchive.hs view
@@ -7,33 +7,40 @@   ( cmdSyncFromArchive   ) where -import Antiope.Core                         (runResAws, toText)-import Antiope.Env                          (LogLevel, mkEnv)-import App.Commands.Options.Parser          (optsSyncFromArchive)-import App.Static                           (homeDirectory)-import Control.Lens                         hiding ((<.>))-import Control.Monad                        (unless, void, when)+import Antiope.Core                    (runResAws, toText)+import Antiope.Env                     (LogLevel, mkEnv)+import App.Commands.Options.Parser     (optsSyncFromArchive)+import App.Static                      (homeDirectory)+import Control.Lens                    hiding ((<.>))+import Control.Monad                   (unless, void, when) import Control.Monad.Except-import Control.Monad.IO.Class               (liftIO)-import Control.Monad.Trans.Resource         (runResourceT)-import Data.Generics.Product.Any            (the)-import Data.Semigroup                       ((<>))-import Data.Text                            (Text)-import HaskellWorks.Ci.Assist.Core          (PackageInfo (..), Presence (..), Tagged (..), getPackages, loadPlan)-import HaskellWorks.Ci.Assist.Location      ((<.>), (</>))-import HaskellWorks.Ci.Assist.PackageConfig (unTemplateConfig)+import Control.Monad.IO.Class          (liftIO)+import Control.Monad.Trans.Resource    (runResourceT)+import Data.ByteString.Lazy.Search     (replace)+import Data.Generics.Product.Any       (the)+import Data.Maybe+import Data.Semigroup                  ((<>))+import Data.Text                       (Text)+import HaskellWorks.Ci.Assist.Core     (PackageInfo (..), Presence (..), Tagged (..), getPackages, loadPlan)+import HaskellWorks.Ci.Assist.IO.Error (exceptWarn, maybeToExcept, maybeToExceptM)+import HaskellWorks.Ci.Assist.Location ((<.>), (</>))+import HaskellWorks.Ci.Assist.Metadata (deleteMetadata, loadMetadata) import HaskellWorks.Ci.Assist.Show-import HaskellWorks.Ci.Assist.Tar           (mapEntriesWith)-import Network.AWS.Types                    (Region (Oregon))-import Options.Applicative                  hiding (columns)-import System.Directory                     (createDirectoryIfMissing, doesDirectoryExist)+import HaskellWorks.Ci.Assist.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.Ci.Assist.GhcPkg     as GhcPkg+import qualified HaskellWorks.Ci.Assist.Hash       as H import qualified HaskellWorks.Ci.Assist.IO.Console as CIO import qualified HaskellWorks.Ci.Assist.IO.Lazy    as IO import qualified HaskellWorks.Ci.Assist.IO.Tar     as IO@@ -48,29 +55,36 @@  runSyncFromArchive :: Z.SyncFromArchiveOptions -> IO () runSyncFromArchive opts = do-  let storePath   = opts ^. the @"storePath"-  let archiveUri  = opts ^. the @"archiveUri"-  let threads     = opts ^. the @"threads"+  let storePath           = opts ^. the @"storePath"+  let archiveUri          = opts ^. the @"archiveUri"+  let threads             = opts ^. the @"threads"+  let versionedArchiveUri = archiveUri </> archiveVersion+  let storePathHash       = opts ^. the @"storePathHash" & fromMaybe (H.hashStorePath storePath)+  let scopedArchiveUri    = versionedArchiveUri </> T.pack storePathHash -  CIO.putStrLn $ "Store path: "   <> toText storePath-  CIO.putStrLn $ "Archive URI: "  <> toText archiveUri-  CIO.putStrLn $ "Threads: "      <> tshow threads+  CIO.putStrLn $ "Store path: "       <> toText storePath+  CIO.putStrLn $ "Store path hash: "  <> T.pack storePathHash+  CIO.putStrLn $ "Archive URI: "      <> toText archiveUri+  CIO.putStrLn $ "Archive version: "  <> archiveVersion+  CIO.putStrLn $ "Threads: "          <> tshow threads    GhcPkg.testAvailability    mbPlan <- loadPlan   case mbPlan of     Right planJson -> do-      env <- mkEnv (opts ^. the @"region") (\_ _ -> pure ())+      envAws <- mkEnv (opts ^. the @"region") (\_ _ -> pure ())       let compilerId                  = planJson ^. the @"compilerId"-      let archivePath                 = archiveUri </> compilerId-      let baseDir                     = opts ^. the @"storePath"-      let storeCompilerPath           = baseDir </> T.unpack 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"        CIO.putStrLn "Creating store directories"-      createDirectoryIfMissing True baseDir+      createDirectoryIfMissing True storePath       createDirectoryIfMissing True storeCompilerPath       createDirectoryIfMissing True storeCompilerLibPath @@ -80,29 +94,34 @@         CIO.putStrLn "Package DB missing. Creating Package DB"         GhcPkg.init storeCompilerPackageDbPath -      packages <- getPackages baseDir planJson+      packages <- getPackages storePath planJson        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 = archiveUri </> T.pack archiveBaseName-          let packageStorePath = baseDir </> packageDir pInfo+          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-            arhiveFileExists <- runResourceT $ IO.resourceExists env archiveFile-            when arhiveFileExists $ do-              CIO.putStrLn $ "Extracting: " <> T.pack archiveBaseName-              runResAws env $ do-                maybeArchiveFileContents <- IO.readResource env archiveFile+            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                  case maybeArchiveFileContents of                   Just archiveFileContents -> do-                    let tempArchiveFile = tempPath </> archiveBaseName :: FilePath-                    liftIO $ LBS.writeFile tempArchiveFile archiveFileContents-                    liftIO $ runExceptT $ IO.extractTar tempArchiveFile storePath+                    existingArchiveFileContents <- IO.readResource envAws existingArchiveFile & maybeToExceptM ("Archive unavailable: " <> show (toText archiveFile))+                    let tempArchiveFile = tempPath </> archiveBaseName+                    liftIO $ LBS.writeFile tempArchiveFile existingArchiveFileContents+                    IO.extractTar tempArchiveFile storePath +                    meta <- loadMetadata packageStorePath+                    oldStorePath <- maybeToExcept "store-path is missing from Metadata" (Map.lookup "store-path" meta)+                     case confPath pInfo of                       Tagged conf _ -> do                         let theConfPath = storePath </> conf@@ -110,12 +129,13 @@                         confPathExists <- liftIO $ IO.doesFileExist theConfPath                         when confPathExists $ do                           confContents <- liftIO $ LBS.readFile theConfPath-                          liftIO $ LBS.writeFile tempConfPath (unTemplateConfig baseDir confContents)+                          liftIO $ LBS.writeFile tempConfPath (replace (LBS.toStrict oldStorePath) (C8.pack storePath) confContents)                           liftIO $ IO.renamePath tempConfPath theConfPath-                   Nothing -> do-                    CIO.putStrLn $ "Archive unavailable: " <> toText archiveFile+                    CIO.putStrLn $ "Archive unavailable: " <> toText existingArchiveFile +                    deleteMetadata packageStorePath+       CIO.putStrLn "Recaching package database"       GhcPkg.recache storeCompilerPackageDbPath @@ -123,6 +143,10 @@       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)  cmdSyncFromArchive :: Mod CommandFields (IO ()) cmdSyncFromArchive = command "sync-from-archive"  $ flip info idm $ runSyncFromArchive <$> optsSyncFromArchive
src/App/Commands/SyncToArchive.hs view
@@ -7,31 +7,34 @@   ( cmdSyncToArchive   ) where -import Antiope.Core                         (toText)-import Antiope.Env                          (LogLevel, mkEnv)-import App.Commands.Options.Parser          (optsSyncToArchive)-import App.Static                           (homeDirectory)-import Control.Lens                         hiding ((<.>))-import Control.Monad                        (unless, when)+import Antiope.Core                    (toText)+import Antiope.Env                     (LogLevel, mkEnv)+import App.Commands.Options.Parser     (optsSyncToArchive)+import App.Static                      (homeDirectory)+import Control.Lens                    hiding ((<.>))+import Control.Monad                   (unless, when) import Control.Monad.Except-import Control.Monad.Trans.Resource         (runResourceT)-import Data.Generics.Product.Any            (the)-import Data.List                            (isSuffixOf)-import Data.Semigroup                       ((<>))-import HaskellWorks.Ci.Assist.Core          (PackageInfo (..), Presence (..), Tagged (..), getPackages, loadPlan, relativePaths, relativePaths2)-import HaskellWorks.Ci.Assist.Location      ((<.>), (</>))-import HaskellWorks.Ci.Assist.PackageConfig (templateConfig)+import Control.Monad.Trans.Resource    (runResourceT)+import Data.Generics.Product.Any       (the)+import Data.List                       (isSuffixOf, (\\))+import Data.Maybe+import Data.Semigroup                  ((<>))+import HaskellWorks.Ci.Assist.Core     (PackageInfo (..), Presence (..), Tagged (..), getPackages, loadPlan, relativePaths)+import HaskellWorks.Ci.Assist.Location ((<.>), (</>))+import HaskellWorks.Ci.Assist.Metadata (createMetadata) import HaskellWorks.Ci.Assist.Show-import HaskellWorks.Ci.Assist.Tar           (updateEntryWith)-import Options.Applicative                  hiding (columns)-import System.Directory                     (createDirectoryIfMissing, doesDirectoryExist)+import HaskellWorks.Ci.Assist.Version  (archiveVersion)+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.Lazy              as LBS+import qualified Data.ByteString.Lazy.Char8        as LC8 import qualified Data.Text                         as T import qualified HaskellWorks.Ci.Assist.GhcPkg     as GhcPkg+import qualified HaskellWorks.Ci.Assist.Hash       as H import qualified HaskellWorks.Ci.Assist.IO.Console as CIO import qualified HaskellWorks.Ci.Assist.IO.Error   as IO import qualified HaskellWorks.Ci.Assist.IO.File    as IO@@ -39,6 +42,7 @@ import qualified HaskellWorks.Ci.Assist.IO.Tar     as IO import qualified HaskellWorks.Ci.Assist.Types      as Z import qualified System.Directory                  as IO+import qualified System.FilePath.Posix             as FP import qualified System.IO                         as IO import qualified System.IO.Temp                    as IO import qualified UnliftIO.Async                    as IO@@ -48,27 +52,33 @@  runSyncToArchive :: Z.SyncToArchiveOptions -> IO () runSyncToArchive opts = do-  let storePath   = opts ^. the @"storePath"-  let archiveUri  = opts ^. the @"archiveUri"-  let threads     = opts ^. the @"threads"+  let storePath           = opts ^. the @"storePath"+  let archiveUri          = opts ^. the @"archiveUri"+  let threads             = opts ^. the @"threads"+  let versionedArchiveUri = archiveUri </> archiveVersion+  let storePathHash       = opts ^. the @"storePathHash" & fromMaybe (H.hashStorePath storePath)+  let scopedArchiveUri    = versionedArchiveUri </> T.pack storePathHash -  CIO.putStrLn $ "Store path: "   <> toText storePath-  CIO.putStrLn $ "Archive URI: "  <> toText archiveUri-  CIO.putStrLn $ "Threads: "      <> tshow threads+  CIO.putStrLn $ "Store path: "       <> toText storePath+  CIO.putStrLn $ "Store path hash: "  <> T.pack storePathHash+  CIO.putStrLn $ "Archive URI: "      <> toText archiveUri+  CIO.putStrLn $ "Archive version: "  <> archiveVersion+  CIO.putStrLn $ "Threads: "          <> tshow threads    mbPlan <- loadPlan   case mbPlan of     Right planJson -> do       let compilerId = planJson ^. the @"compilerId"       envAws <- mkEnv (opts ^. the @"region") (\_ _ -> pure ())-      let archivePath = archiveUri </> compilerId+      let archivePath       = versionedArchiveUri </> compilerId+      let scopedArchivePath = scopedArchiveUri </> compilerId       IO.createLocalDirectoryIfMissing archivePath-      let baseDir = opts ^. the @"storePath"+      IO.createLocalDirectoryIfMissing scopedArchivePath       CIO.putStrLn "Extracting package list" -      packages <- getPackages baseDir planJson+      packages <- getPackages storePath planJson -      let storeCompilerPath           = baseDir </> T.unpack compilerId+      let storeCompilerPath           = storePath </> T.unpack compilerId       let storeCompilerPackageDbPath  = storeCompilerPath </> "package.db"        storeCompilerPackageDbPathExists <- doesDirectoryExist storeCompilerPackageDbPath@@ -81,40 +91,35 @@       IO.withSystemTempDirectory "cabal-cache" $ \tempPath -> do         CIO.putStrLn $ "Temp path: " <> tshow tempPath -        CIO.putStrLn "Copying package.db directory for transformation"-        let workingStoreCompilerPath = tempPath </> T.unpack compilerId-        let workingStoreCompilerPackageDbPath = tempPath </> T.unpack compilerId </> "package.db"--        runExceptT $ IO.exceptFatal "Fatal error" $ do-          liftIO $ IO.createDirectoryIfMissing True workingStoreCompilerPackageDbPath--        packageDbFiles <- IO.listDirectory storeCompilerPackageDbPath-        let confFiles = filter (isSuffixOf ".conf") packageDbFiles--        forM_ confFiles $ \confFile -> do-          stream <- LBS.readFile (storeCompilerPackageDbPath </> confFile)-          LBS.writeFile (workingStoreCompilerPackageDbPath </> confFile) (templateConfig baseDir stream)-         IO.pooledForConcurrentlyN_ (opts ^. the @"threads") packages $ \pInfo -> do           let archiveFileBasename = packageDir pInfo <.> ".tar.gz"-          let archiveFile = archiveUri </> T.pack archiveFileBasename-          let packageStorePath = baseDir </> packageDir pInfo-          archiveFileExists <- runResourceT $ IO.resourceExists envAws archiveFile+          let archiveFile         = versionedArchiveUri </> T.pack archiveFileBasename+          let scopedArchiveFile   = versionedArchiveUri </> T.pack storePathHash </> T.pack archiveFileBasename+          let packageStorePath    = storePath </> packageDir pInfo+          let packageSharePath    = packageStorePath </> "share"+          archiveFileExists <- runResourceT $ IO.resourceExists envAws scopedArchiveFile            unless archiveFileExists $ do             packageStorePathExists <- doesDirectoryExist packageStorePath -            when packageStorePathExists $ void $ runExceptT $ IO.exceptWarn "Warning" $ do-              let rp2 = relativePaths2 storePath tempPath pInfo-              CIO.putStrLn $ "Creating " <> toText archiveFile+            when packageStorePathExists $ void $ runExceptT $ IO.exceptWarn $ do+              let workingStorePackagePath = tempPath </> packageDir pInfo+              liftIO $ IO.createDirectoryIfMissing True workingStorePackagePath +              let rp2 = relativePaths storePath pInfo+              CIO.putStrLn $ "Creating " <> toText scopedArchiveFile+               let tempArchiveFile = tempPath </> archiveFileBasename -              IO.createTar tempArchiveFile rp2+              metas <- createMetadata tempPath pInfo [("store-path", LC8.pack storePath)] -              liftIO (LBS.readFile tempArchiveFile >>= IO.writeResource envAws archiveFile)+              IO.createTar tempArchiveFile (metas:rp2) -              return ()+              liftIO (LBS.readFile tempArchiveFile >>= IO.writeResource envAws scopedArchiveFile)++              shareEntries <- (\\ ["doc"]) <$> IO.listMaybeDirectory packageSharePath++              when (null shareEntries) $ IO.linkOrCopyResource envAws scopedArchiveFile archiveFile      Left errorMessage -> do       CIO.hPutStrLn IO.stderr $ "ERROR: Unable to parse plan.json file: " <> T.pack errorMessage
src/HaskellWorks/Ci/Assist/Core.hs view
@@ -11,7 +11,6 @@   , Presence(..)   , getPackages   , relativePaths-  , relativePaths2   , loadPlan   ) where @@ -55,20 +54,14 @@   , libs       :: [Library]   } deriving (Show, Eq, Generic, NFData) -relativePaths2 :: FilePath -> FilePath -> PackageInfo -> [IO.TarGroup]-relativePaths2 basePath tmpPath pInfo =+relativePaths :: FilePath -> PackageInfo -> [IO.TarGroup]+relativePaths basePath pInfo =   [ IO.TarGroup basePath $ mempty       <> (pInfo ^. the @"libs")       <> [packageDir pInfo]-  , IO.TarGroup tmpPath $ mempty+  , IO.TarGroup basePath $ mempty       <> ([pInfo ^. the @"confPath"] & filter ((== Present) . (^. the @"tag")) <&> (^. the @"value"))   ]--relativePaths :: PackageInfo -> [FilePath]-relativePaths pInfo = mempty-  <>  ([pInfo ^. the @"confPath"] & filter ((== Present) . (^. the @"tag")) <&> (^. the @"value"))-  <>  [packageDir pInfo]-  <>  (pInfo ^. the @"libs")  getPackages :: FilePath -> Z.PlanJson -> IO [PackageInfo] getPackages basePath planJson = forM packages (mkPackageInfo basePath compilerId)
+ src/HaskellWorks/Ci/Assist/Hash.hs view
@@ -0,0 +1,10 @@+module HaskellWorks.Ci.Assist.Hash+  ( hashStorePath+  ) where++import qualified Crypto.Hash        as CH+import qualified Data.Text          as T+import qualified Data.Text.Encoding as T++hashStorePath :: String -> String+hashStorePath = take 10 . show . CH.hashWith CH.SHA256 . T.encodeUtf8 . T.pack
src/HaskellWorks/Ci/Assist/IO/Error.hs view
@@ -3,6 +3,8 @@ module HaskellWorks.Ci.Assist.IO.Error   ( exceptFatal   , exceptWarn+  , maybeToExcept+  , maybeToExceptM   ) where  import Control.Monad.Except@@ -13,15 +15,21 @@ import qualified System.Exit                       as IO import qualified System.IO                         as IO -exceptFatal :: MonadIO m => String -> ExceptT String m a -> ExceptT String m a-exceptFatal message f = catchError f handler+exceptFatal :: MonadIO m => ExceptT String m a -> ExceptT String m a+exceptFatal f = catchError f handler   where handler e = do           liftIO . CIO.hPutStrLn IO.stderr . T.pack $ "Fatal Error: " <> e           liftIO IO.exitFailure           throwError e -exceptWarn :: MonadIO m => String -> ExceptT String m a -> ExceptT String m a-exceptWarn message f = catchError f handler+exceptWarn :: MonadIO m => ExceptT String m a -> ExceptT String m a+exceptWarn f = catchError f handler   where handler e = do           liftIO . CIO.hPutStrLn IO.stderr . T.pack $ "Warning: " <> e           throwError e++maybeToExcept :: Monad m => String -> Maybe a -> ExceptT String m a+maybeToExcept message = maybe (throwError message) pure++maybeToExceptM :: Monad m => String -> m (Maybe a) -> ExceptT String m a+maybeToExceptM message = ExceptT . fmap (maybe (Left message) Right)
src/HaskellWorks/Ci/Assist/IO/File.hs view
@@ -2,6 +2,7 @@  module HaskellWorks.Ci.Assist.IO.File   ( copyDirectoryRecursive+  , listMaybeDirectory   ) where  import Control.Monad.Except@@ -9,6 +10,7 @@  import qualified Data.Text                         as T import qualified HaskellWorks.Ci.Assist.IO.Console as CIO+import qualified System.Directory                  as IO import qualified System.Exit                       as IO import qualified System.IO                         as IO import qualified System.Process                    as IO@@ -21,3 +23,10 @@   case exitCode of     IO.ExitSuccess   -> return ()     IO.ExitFailure n -> throwError ""++listMaybeDirectory :: MonadIO m => FilePath -> ExceptT String m [FilePath]+listMaybeDirectory filepath = do+  exists <- liftIO $ IO.doesDirectoryExist filepath+  if exists+    then liftIO $ IO.listDirectory filepath+    else return []
src/HaskellWorks/Ci/Assist/IO/Lazy.hs view
@@ -4,9 +4,11 @@ module HaskellWorks.Ci.Assist.IO.Lazy   ( readResource   , resourceExists+  , firstExistingResource   , headS3Uri   , writeResource   , createLocalDirectoryIfMissing+  , linkOrCopyResource   ) where  import Antiope.Core@@ -21,6 +23,7 @@ import Data.Either                     (isRight) import Data.Text                       (Text) import HaskellWorks.Ci.Assist.Location (Location (..))+import HaskellWorks.Ci.Assist.Show import Network.AWS                     (MonadAWS, chunkedFile) import Network.AWS.Data.Body           (_streamBody) @@ -32,11 +35,14 @@ import qualified HaskellWorks.Ci.Assist.IO.Console as CIO import qualified Network.AWS                       as AWS import qualified Network.AWS.Data                  as AWS+import qualified Network.AWS.S3.CopyObject         as AWS import qualified Network.AWS.S3.HeadObject         as AWS import qualified Network.AWS.S3.PutObject          as AWS import qualified Network.HTTP.Types                as HTTP import qualified System.Directory                  as IO+import qualified System.FilePath.Posix             as FP import qualified System.IO                         as IO+import qualified System.IO.Error                   as IO  {-# ANN module ("HLint: ignore Redundant do"        :: String) #-} {-# ANN module ("HLint: ignore Reduce duplication"  :: String) #-}@@ -47,11 +53,36 @@   S3 s3Uri    -> runAws envAws $ AWS.downloadFromS3Uri s3Uri   Local path  -> liftIO $ Just <$> LBS.readFile path -resourceExists :: (MonadResource m, MonadCatch m, MonadIO m) => AWS.Env -> Location -> m Bool+safePathIsSymbolLink :: FilePath -> IO Bool+safePathIsSymbolLink filePath = catch (IO.pathIsSymbolicLink filePath) handler+  where handler :: IOError -> IO Bool+        handler e = if IO.isDoesNotExistError e+          then return False+          else return True++resourceExists :: (MonadUnliftIO m, MonadCatch m, MonadIO m) => AWS.Env -> Location -> m Bool resourceExists envAws = \case-  S3 s3Uri    -> isRight <$> headS3Uri envAws s3Uri-  Local path  -> liftIO $ IO.doesFileExist path+  S3 s3Uri    -> isRight <$> runResourceT (headS3Uri envAws s3Uri)+  Local path  -> do+    fileExists <- liftIO $ IO.doesFileExist path+    if fileExists+      then return True+      else do+        symbolicLinkExists <- liftIO $ safePathIsSymbolLink path+        if symbolicLinkExists+          then do+            target <- liftIO $ IO.getSymbolicLinkTarget path+            resourceExists envAws (Local target)+          else return False +firstExistingResource :: (MonadUnliftIO m, MonadCatch m, MonadIO m) => AWS.Env -> [Location] -> m (Maybe Location)+firstExistingResource envAws [] = return Nothing+firstExistingResource envAws (a:as) = do+  exists <- resourceExists envAws a+  if exists+    then return (Just a)+    else firstExistingResource envAws as+ headS3Uri :: (MonadResource m, MonadCatch m) => AWS.Env -> AWS.S3Uri -> m (Either String AWS.HeadObjectResponse) headS3Uri envAws (AWS.S3Uri b k) =   catch (Right <$> runAws envAws (AWS.send (AWS.headObject b k))) $ \(e :: AWS.Error) ->@@ -62,18 +93,38 @@ chunkSize :: AWS.ChunkSize chunkSize = AWS.ChunkSize (1024 * 1024) -uploadeToS3 :: MonadUnliftIO m => AWS.Env -> AWS.S3Uri -> LBS.ByteString -> m ()-uploadeToS3 envAws (AWS.S3Uri b k) lbs = do+uploadToS3 :: MonadUnliftIO m => AWS.Env -> AWS.S3Uri -> LBS.ByteString -> m ()+uploadToS3 envAws (AWS.S3Uri b k) lbs = do   let req = AWS.toBody lbs   let po  = AWS.putObject b k req   void $ runResAws envAws $ AWS.send po  writeResource :: MonadUnliftIO m => AWS.Env -> Location -> LBS.ByteString -> m () writeResource envAws loc lbs = case loc of-  S3 s3Uri   -> uploadeToS3 envAws s3Uri lbs+  S3 s3Uri   -> uploadToS3 envAws s3Uri lbs   Local path -> liftIO $ LBS.writeFile path lbs  createLocalDirectoryIfMissing :: (MonadCatch m, MonadIO m) => Location -> m () createLocalDirectoryIfMissing = \case   S3 s3Uri   -> return ()   Local path -> liftIO $ IO.createDirectoryIfMissing True path++copyS3Uri :: MonadUnliftIO m => AWS.Env -> AWS.S3Uri -> AWS.S3Uri -> ExceptT String m ()+copyS3Uri envAws (AWS.S3Uri sourceBucket sourceObjectKey) (AWS.S3Uri targetBucket targetObjectKey) = ExceptT $ do+  response <- runResourceT $ runAws envAws $ AWS.send (AWS.copyObject targetBucket (toText sourceBucket <> "/" <> toText sourceObjectKey) targetObjectKey)+  let responseCode = response ^. AWS.corsResponseStatus+  if 200 <= responseCode && responseCode < 300+    then return (Right ())+    else return (Left "")++linkOrCopyResource :: MonadUnliftIO m => AWS.Env -> Location -> Location -> ExceptT String m ()+linkOrCopyResource envAws source target = case source of+  S3 sourceS3Uri -> case target of+    S3 targetS3Uri -> do copyS3Uri envAws sourceS3Uri targetS3Uri+    Local _        -> throwError "Can't copy between different file backends"+  Local sourcePath -> case target of+    S3 _             -> throwError "Can't copy between different file backends"+    Local targetPath -> do+      liftIO $ IO.createDirectoryIfMissing True (FP.takeDirectory targetPath)+      targetPathExists <- liftIO $ IO.doesFileExist targetPath+      unless targetPathExists $ liftIO $ IO.createFileLink sourcePath targetPath
src/HaskellWorks/Ci/Assist/IO/Tar.hs view
@@ -13,6 +13,7 @@ import Control.DeepSeq             (NFData) import Control.Lens import Control.Monad.Except+import Control.Monad.IO.Class      (MonadIO, liftIO) import Data.Generics.Product.Any import Data.List import GHC.Generics@@ -29,7 +30,7 @@   , entryPaths :: [FilePath]   } deriving (Show, Eq, Generic, NFData) -createTar :: FilePath -> [TarGroup] -> ExceptT String IO ()+createTar :: MonadIO m => FilePath -> [TarGroup] -> ExceptT String m () createTar tarFile groups = do   let args = ["-zcf", tarFile] <> foldMap tarGroupToArgs groups   process <- liftIO $ IO.spawnProcess "tar" args@@ -38,7 +39,7 @@     IO.ExitSuccess   -> return ()     IO.ExitFailure n -> throwError "" -extractTar :: FilePath -> FilePath -> ExceptT String IO ()+extractTar :: MonadIO m => FilePath -> FilePath -> ExceptT String m () extractTar tarFile targetPath = do   process <- liftIO $ IO.spawnProcess "tar" ["-C", targetPath, "-zxf", tarFile]   exitCode <- liftIO $ IO.waitForProcess process
+ src/HaskellWorks/Ci/Assist/Metadata.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE TupleSections #-}+module HaskellWorks.Ci.Assist.Metadata+where++import Control.Lens                  ((<&>))+import Control.Monad                 (forM_)+import Control.Monad.IO.Class        (MonadIO, liftIO)+import HaskellWorks.Ci.Assist.Core   (PackageInfo (..))+import HaskellWorks.Ci.Assist.IO.Tar (TarGroup (..))+import System.FilePath               (makeRelative, takeFileName, (<.>), (</>))++import qualified Data.ByteString.Lazy as LBS+import qualified Data.Map.Strict      as Map+import qualified Data.Text            as T+import qualified System.Directory     as IO++metaDir :: String+metaDir = "_CC_METADATA"++createMetadata :: MonadIO m => FilePath -> PackageInfo -> [(T.Text, LBS.ByteString)] -> m TarGroup+createMetadata storePath pkg values = liftIO $ do+  let pkgMetaPath = storePath </> packageDir pkg </> metaDir+  IO.createDirectoryIfMissing True pkgMetaPath+  forM_ values $ \(k, v) -> LBS.writeFile (pkgMetaPath </> T.unpack k) v+  pure $ TarGroup storePath [packageDir pkg </> metaDir]++loadMetadata :: MonadIO m => FilePath -> m (Map.Map T.Text LBS.ByteString)+loadMetadata pkgStorePath = liftIO $ do+  let pkgMetaPath = pkgStorePath </> metaDir+  exists <- IO.doesDirectoryExist pkgMetaPath+  if not exists+    then pure Map.empty+    else IO.listDirectory pkgMetaPath+          <&> fmap (pkgMetaPath </>)+          >>= traverse (\mfile -> (T.pack (takeFileName mfile),) <$> LBS.readFile mfile)+          <&> Map.fromList++deleteMetadata :: MonadIO m => FilePath -> m ()+deleteMetadata pkgStorePath =+  liftIO $ IO.removeDirectoryRecursive (pkgStorePath </> metaDir)
− src/HaskellWorks/Ci/Assist/PackageConfig.hs
@@ -1,20 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-module HaskellWorks.Ci.Assist.PackageConfig-where---import Data.ByteString.Char8       (pack)-import Data.ByteString.Lazy.Search (replace)-import HaskellWorks.Ci.Assist.Tar--import qualified Data.ByteString      as BS-import qualified Data.ByteString.Lazy as LBS--storePathMacro :: BS.ByteString-storePathMacro = "${STORE_PATH}"--templateConfig :: FilePath -> LBS.ByteString -> LBS.ByteString-templateConfig storePath = replace (pack storePath) storePathMacro--unTemplateConfig :: FilePath -> LBS.ByteString -> LBS.ByteString-unTemplateConfig storePath = replace storePathMacro (pack storePath)
− src/HaskellWorks/Ci/Assist/Tar.hs
@@ -1,27 +0,0 @@-module HaskellWorks.Ci.Assist.Tar-where--import Codec.Archive.Tar-import Codec.Archive.Tar.Entry--import qualified Data.ByteString.Lazy as LBS--updateEntryWith :: (FilePath -> Bool)-  -> (LBS.ByteString -> LBS.ByteString)-  -> Entry-  -> Entry-updateEntryWith pred transform entry =-  if pred (entryPath entry)-    then case entryContent entry of-        NormalFile bs size ->-          let bs' = transform bs-          in entry { entryContent = NormalFile bs' (LBS.length bs') }-        _ -> entry-    else entry--mapEntriesWith :: (FilePath -> Bool)-  -> (LBS.ByteString -> LBS.ByteString)-  -> Entries e-  -> Entries e-mapEntriesWith pred transform =-  mapEntriesNoFail (updateEntryWith pred transform)
+ src/HaskellWorks/Ci/Assist/Version.hs view
@@ -0,0 +1,8 @@+{-# LANGUAGE OverloadedStrings #-}++module HaskellWorks.Ci.Assist.Version where++import Data.String++archiveVersion :: IsString s => s+archiveVersion = "v1"