@@ -7,21 +7,31 @@ module Stackage.ServerBundle
77 , epochTime
88 , bpAllPackages
99 , docsListing
10+ , createBundleV2
11+ , CreateBundleV2 (..)
12+ , SnapshotType (..)
13+ , writeIndexStyle
14+ , DocMap
15+ , PackageDocs (..)
1016 ) where
1117
1218import qualified Codec.Archive.Tar as Tar
1319import qualified Codec.Archive.Tar.Entry as Tar
1420import qualified Codec.Compression.GZip as GZip
1521import qualified Data.Map as M
22+import Data.Aeson (ToJSON (..), (.=), object, FromJSON (..), (.:), withObject)
23+import System.IO.Temp (withTempDirectory)
1624import qualified Data.Yaml as Y
17−import Filesystem (isFile)
25+import Filesystem (isFile, getWorkingDirectory, listDirectory, isDirectory, canonicalizePath)
1826import Foreign.C.Types (CTime (CTime))
1927import Stackage.BuildConstraints
2028import Stackage.BuildPlan
2129import Stackage.Prelude
30+import System.IO.Temp (withTempDirectory)
2231import qualified System.PosixCompat.Time as PC
2332import qualified Text.XML as X
2433import Text.XML.Cursor
34+import System.PosixCompat.Files (createSymbolicLink)
2535
2636-- | Get current time
2737epochTime :: IO Tar.EpochTime
@@ -73,13 +83,30 @@ serverBundle time title slug bp@BuildPlan {..} = GZip.compress $ Tar.write
7383 map (\(PackageName name) -> name)
7484 (M.keys $ siCorePackages bpSystemInfo)
7585
86+-- | Package name is key
87+type DocMap = Map Text PackageDocs
88+data PackageDocs = PackageDocs
89+ { pdVersion :: Text
90+ , pdModules :: Map Text [Text]
91+ -- ^ module name, path
92+ }
93+instance ToJSON PackageDocs where
94+ toJSON PackageDocs {..} = object
95+ [ "version" .= pdVersion
96+ , "modules" .= pdModules
97+ ]
98+instance FromJSON PackageDocs where
99+ parseJSON = withObject "PackageDocs" $ \o -> PackageDocs
100+ <$> o .: "version"
101+ <*> o .: "modules"
102+
76103docsListing :: BuildPlan
77104 -> FilePath -- ^ docs directory
78− -> IO ByteString
105+ -> IO DocMap
79106docsListing bp docsDir =
80− fmap (Y.encode . fold) $ mapM go $ mapToList $ bpAllPackages bp
107+ fmap fold $ mapM go $ mapToList $ bpAllPackages bp
81108 where
82− go :: (PackageName, Version) -> IO (Map Text Y.Value)
109+ go :: (PackageName, Version) -> IO DocMap
83110 go (package, version) = do -- handleAny (const $ return mempty) $ do
84111 let dirname = fpFromText (concat
85112 [ display package
@@ -107,8 +134,138 @@ docsListing bp docsDir =
107134 return $ if e
108135 then asMap $ singletonMap name [fpToText dirname, href]
109136 else mempty
110− return $ singletonMap (display package) $ Y.object
111− [ "version" Y..= display version
112− , "modules" Y..= m
113− ]
137+ return $ singletonMap (display package) $ PackageDocs
138+ { pdVersion = display version
139+ , pdModules = m
140+ }
114141 else return mempty
142+
143+data SnapshotType = STNightly
144+ | STLTS !Int !Int -- ^ major, minor
145+ deriving (Show, Read, Eq, Ord)
146+
147+instance ToJSON SnapshotType where
148+ toJSON STNightly = object
149+ [ "type" .= asText "nightly"
150+ ]
151+ toJSON (STLTS major minor) = object
152+ [ "type" .= asText "lts"
153+ , "major" .= major
154+ , "minor" .= minor
155+ ]
156+instance FromJSON SnapshotType where
157+ parseJSON = withObject "SnapshotType" $ \o -> do
158+ t <- o .: "type"
159+ case asText t of
160+ "nightly" -> return STNightly
161+ "lts" -> STLTS
162+ <$> o .: "major"
163+ <*> o .: "minor"
164+ _ -> fail $ "Unknown type for SnapshotType: " ++ unpack t
165+
166+data CreateBundleV2 = CreateBundleV2
167+ { cb2Plan :: BuildPlan
168+ , cb2Type :: SnapshotType
169+ , cb2DocsDir :: FilePath
170+ , cb2Dest :: FilePath
171+ }
172+
173+-- | Create a V2 bundle, which contains the build plan, metadata, docs, and doc
174+-- map.
175+createBundleV2 :: CreateBundleV2 -> IO ()
176+createBundleV2 CreateBundleV2 {..} = do
177+ docsDir <- canonicalizePath cb2DocsDir
178+ docMap <- docsListing cb2Plan cb2DocsDir
179+
180+ Y.encodeFile (fpToString $ docsDir </> "build-plan.yaml") cb2Plan
181+ Y.encodeFile (fpToString $ docsDir </> "build-type.yaml") cb2Type
182+ Y.encodeFile (fpToString $ docsDir </> "docs-map.yaml") docMap
183+ void $ writeIndexStyle Nothing cb2DocsDir
184+
185+ currentDir <- getWorkingDirectory
186+ files <- listDirectory docsDir
187+
188+ let args = "cfJ"
189+ : fpToString (currentDir </> cb2Dest)
190+ : "--dereference"
191+ : map (fpToString . filename) files
192+ cp = (proc "tar" args) { cwd = Just $ fpToString docsDir }
193+ withCheckedProcess cp $ \ClosedStream Inherited Inherited -> return ()
194+
195+writeIndexStyle :: Maybe Text -- ^ snapshot id
196+ -> FilePath -- ^ docs dir
197+ -> IO [String]
198+writeIndexStyle msnapid dir = do
199+ dirs <- fmap sort
200+ $ runResourceT
201+ $ sourceDirectory dir
202+ $$ filterMC (liftIO . isDirectory)
203+ =$ mapC (fpToString . filename)
204+ =$ sinkList
205+ writeFile (dir </> "index.html") $ mkIndex
206+ (unpack <$> msnapid)
207+ dirs
208+ writeFile (dir </> "style.css") styleCss
209+ return dirs
210+
211+mkIndex :: Maybe String -> [String] -> String
212+mkIndex msnapid dirs = concat
213+ [ "<!DOCTYPE html>\n<html lang='en'><head><title>Haddocks index</title>"
214+ , "<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css'>"
215+ , "<link rel='stylesheet' href='style.css'>"
216+ , "<link rel='shortcut icon' href='http://www.stackage.org/static/img/favicon.ico' />"
217+ , "</head>"
218+ , "<body><div class='container'>"
219+ , "<div class='row'><div class='span12 col-md-12'>"
220+ , "<h1>Haddock documentation index</h1>"
221+ , flip foldMap msnapid $ \snapid -> concat
222+ [ "<p class='return'><a href=\"http://www.stackage.org/stackage/"
223+ , snapid
224+ , "\">Return to snapshot</a></p>"
225+ ]
226+ , "<ul>"
227+ , concatMap toLI dirs
228+ , "</ul></div></div></div></body></html>"
229+ ]
230+ where
231+ toLI name = concat
232+ [ "<li><a href='"
233+ , name
234+ , "/index.html'>"
235+ , name
236+ , "</a></li>"
237+ ]
238+
239+styleCss :: String
240+styleCss = concat
241+ [ "@media (min-width: 530px) {"
242+ , "ul { -webkit-column-count: 2; -moz-column-count: 2; column-count: 2 }"
243+ , "}"
244+ , "@media (min-width: 760px) {"
245+ , "ul { -webkit-column-count: 3; -moz-column-count: 3; column-count: 3 }"
246+ , "}"
247+ , "ul {"
248+ , " margin-left: 0;"
249+ , " padding-left: 0;"
250+ , " list-style-type: none;"
251+ , "}"
252+ , "body {"
253+ , " background: #f0f0f0;"
254+ , " font-family: 'Lato', sans-serif;"
255+ , " text-shadow: 1px 1px 1px #ffffff;"
256+ , " font-size: 20px;"
257+ , " line-height: 30px;"
258+ , " padding-bottom: 5em;"
259+ , "}"
260+ , "h1 {"
261+ , " font-weight: normal;"
262+ , " color: #06537d;"
263+ , " font-size: 45px;"
264+ , "}"
265+ , ".return a {"
266+ , " color: #06537d;"
267+ , " font-style: italic;"
268+ , "}"
269+ , ".return {"
270+ , " margin-bottom: 1em;"
271+ , "}"]