-- | Create a bundle to be uploaded to Stackage Server. {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE FlexibleContexts #-} module Stackage.ServerBundle ( serverBundle , epochTime , bpAllPackages , docsListing , createBundleV2 , CreateBundleV2 (..) , SnapshotType (..) , writeIndexStyle , DocMap , PackageDocs (..) ) where import qualified Codec.Archive.Tar as Tar import qualified Codec.Archive.Tar.Entry as Tar import qualified Codec.Compression.GZip as GZip import qualified Data.Map as M import qualified Data.Yaml as Y import Filesystem (getWorkingDirectory, listDirectory) import qualified Filesystem.Path.CurrentOS as F import Foreign.C.Types (CTime (CTime)) import Stackage.BuildConstraints import Stackage.BuildPlan import Stackage.Prelude import qualified System.PosixCompat.Time as PC import qualified Text.XML as X import Text.XML.Cursor import System.Directory (canonicalizePath, doesDirectoryExist, doesFileExist) import System.FilePath (takeFileName) -- | Get current time epochTime :: IO Tar.EpochTime epochTime = (\(CTime t) -> fromIntegral t) <$> PC.epochTime -- | All package/versions in a build plan, including core packages. -- -- Note that this may include packages not available on Hackage. bpAllPackages :: BuildPlan -> Map PackageName Version bpAllPackages BuildPlan {..} = siCorePackages bpSystemInfo ++ map ppVersion bpPackages serverBundle :: Tar.EpochTime -> Text -- ^ title -> Text -- ^ slug -> BuildPlan -> LByteString serverBundle time title slug bp@BuildPlan {..} = GZip.compress $ Tar.write [ fe "build-plan.yaml" (fromStrict $ Y.encode bp) , fe "hackage" hackage , fe "slug" (fromStrict $ encodeUtf8 slug) , fe "desc" (fromStrict $ encodeUtf8 title) , fe "core" corePackagesList ] where fe name contents = case Tar.toTarPath False name of Left s -> error s Right name' -> (Tar.fileEntry name' contents) { Tar.entryTime = time } hackage = builderToLazy $ foldMap goPair $ mapToList packageMap -- need to remove some packages that don't exist on Hackage packageMap = foldr deleteMap (bpAllPackages bp) $ map PackageName [ "bin-package-db" , "ghc" , "rts" ] goPair (name, version) = toBuilder (display name) ++ toBuilder (asText "-") ++ toBuilder (display version) ++ toBuilder (asText "\n") corePackagesList = builderToLazy $ toBuilder $ unlines $ map (\(PackageName name) -> name) (M.keys $ siCorePackages bpSystemInfo) docsListing :: BuildPlan -> FilePath -- ^ docs directory -> IO DocMap docsListing bp docsDir = fmap fold $ mapM go $ mapToList $ bpAllPackages bp where go :: (PackageName, Version) -> IO DocMap go (package, version) = do -- handleAny (const $ return mempty) $ do let dirname = unpack (concat [ display package , "-" , display version ]) indexFP = (docsDir dirname "index.html") ie <- doesFileExist indexFP if ie then do doc <- flip X.readFile indexFP X.def { X.psDecodeEntities = X.decodeHtmlEntities } let cursor = fromDocument doc getPair x = take 1 $ do href <- attribute "href" x let name = concat $ x $// content guard $ not $ null name return (href, name) pairs = cursor $// attributeIs "class" "module" &/ laxElement "a" >=> getPair m <- fmap fold $ forM pairs $ \(href, name) -> do let suffix = dirname unpack href e <- doesFileExist $ docsDir suffix return $ if e then asMap $ singletonMap name [pack dirname, href] else mempty return $ singletonMap (display package) $ PackageDocs { pdVersion = display version , pdModules = m } else return mempty data CreateBundleV2 = CreateBundleV2 { cb2Plan :: BuildPlan , cb2Type :: SnapshotType , cb2DocsDir :: FilePath , cb2Dest :: FilePath , cb2DocmapFile :: !FilePath } -- | Create a V2 bundle, which contains the build plan, metadata, docs, and doc -- map. createBundleV2 :: CreateBundleV2 -> IO () createBundleV2 CreateBundleV2 {..} = do docsDir <- canonicalizePath cb2DocsDir docMap <- docsListing cb2Plan cb2DocsDir Y.encodeFile (docsDir "build-plan.yaml") cb2Plan Y.encodeFile (docsDir "build-type.yaml") cb2Type Y.encodeFile (docsDir "docs-map.yaml") docMap Y.encodeFile cb2DocmapFile docMap void $ writeIndexStyle Nothing cb2DocsDir currentDir <- getWorkingDirectory files <- listDirectory $ fromString docsDir let args = "cfJ" : (F.encodeString currentDir cb2Dest) : "--dereference" : map (takeFileName . F.encodeString) files cp = (proc "tar" args) { cwd = Just docsDir } withCheckedProcess cp $ \ClosedStream Inherited Inherited -> return () writeIndexStyle :: Maybe Text -- ^ snapshot id -> FilePath -- ^ docs dir -> IO [String] writeIndexStyle msnapid dir = do dirs <- fmap sort $ runResourceT $ sourceDirectory dir $$ filterMC (liftIO . doesDirectoryExist) =$ mapC takeFileName =$ sinkList writeFile (dir "index.html") $ encodeUtf8 $ pack $ mkIndex (unpack <$> msnapid) dirs writeFile (dir "style.css") $ encodeUtf8 $ pack styleCss return dirs mkIndex :: Maybe String -> [String] -> String mkIndex msnapid dirs = concat [ "\nHaddocks index" , "" , "" , "" , "" , "
" , "
" , "

Haddock documentation index

" , flip foldMap msnapid $ \snapid -> concat [ "

Return to snapshot

" ] , "
    " , concatMap toLI dirs , "
" ] where toLI name = concat [ "
  • " , name , "
  • " ] styleCss :: String styleCss = concat [ "@media (min-width: 530px) {" , "ul { -webkit-column-count: 2; -moz-column-count: 2; column-count: 2 }" , "}" , "@media (min-width: 760px) {" , "ul { -webkit-column-count: 3; -moz-column-count: 3; column-count: 3 }" , "}" , "ul {" , " margin-left: 0;" , " padding-left: 0;" , " list-style-type: none;" , "}" , "body {" , " background: #f0f0f0;" , " font-family: 'Lato', sans-serif;" , " text-shadow: 1px 1px 1px #ffffff;" , " font-size: 20px;" , " line-height: 30px;" , " padding-bottom: 5em;" , "}" , "h1 {" , " font-weight: normal;" , " color: #06537d;" , " font-size: 45px;" , "}" , ".return a {" , " color: #06537d;" , " font-style: italic;" , "}" , ".return {" , " margin-bottom: 1em;" , "}"]