dash-haskell 1.0.0.0 → 1.0.0.1
raw patch · 19 files changed
+1309/−2 lines, 19 files
Files
- dash-haskell.cabal +19/−1
- src/Control/Monad/M.hs +53/−0
- src/Data/String/Indent.hs +6/−0
- src/Data/String/Util.hs +10/−0
- src/Haddock.hs +33/−0
- src/Haddock/Artifact.hs +43/−0
- src/Haddock/Sqlite.hs +107/−0
- src/Main.hs +1/−1
- src/Options.hs +103/−0
- src/Options/Cabal.hs +138/−0
- src/Options/CabalConstraints.hs +92/−0
- src/Options/DbProvider.hs +71/−0
- src/Options/Documentation.hs +114/−0
- src/Package.hs +10/−0
- src/Package/Conf.hs +10/−0
- src/PackageId.hs +9/−0
- src/Pipes/Conf.hs +76/−0
- src/Pipes/Db.hs +175/−0
- src/Pipes/FileSystem.hs +239/−0
dash-haskell.cabal view
@@ -1,5 +1,5 @@ name: dash-haskell-version: 1.0.0.0+version: 1.0.0.1 synopsis: Command line tool to generate Dash docsets (IDE docs) from package haddock homepage: http://www.github.com/jfeltz/dash-haskell Bug-reports: https://github.com/jfeltz/dash-haskell/issues@@ -103,6 +103,24 @@ executable dash-haskell main-is: Main.hs+ other-modules: Pipes.Db+ Pipes.Conf+ Pipes.FileSystem+ Options+ Package+ Control.Monad.M + Data.String.Util+ Data.String.Indent+ Haddock+ Options.CabalConstraints+ Options.Documentation+ Options.Cabal+ Options.DbProvider+ Haddock.Sqlite+ Haddock.Artifact+ PackageId+ Package.Conf+ build-depends: Cabal >= 1.18 , base >= 4.7 && <4.8 , bytestring >= 0.10.0.1
+ src/Control/Monad/M.hs view
@@ -0,0 +1,53 @@+module Control.Monad.M where+import Control.Monad.Trans.Either+import Control.Monad.Trans.Reader+import Control.Monad.IO.Class+import Control.Monad+import Control.Applicative ((<$>))+import System.Exit+import Pipes++import Data.String.Indent++type M r = ReaderT Env (EitherT String IO) r++data Env = Env { indention :: Int, verbosity :: Bool }++newEnv :: Bool -> Env +newEnv = Env 0+ +env :: M Env +env = ask + +warning :: String -> M () +warning str = do + i <- indention <$> env + liftIO . putStr $ fromIndent ("warning: " ++ str) i++indent :: Int -> Env -> Env+indent amount (Env i v) = Env (i + amount) v++indentM :: Int -> M r -> M r+indentM amount = local (indent amount) ++msg :: (MonadIO m) => String -> ReaderT Env m () +msg str = do+ e <- ask + when (verbosity e) . liftIO . putStr . fromIndent str $ indention e++err :: String -> M r+err = lift . left++runM :: Env -> M () -> IO () +runM env' comp = do + result <- runEitherT (runReaderT comp env')+ case result of+ Left e -> do + liftIO . putStrLn $ "fatal error:\n\n" ++ e+ exitFailure+ Right () -> + exitSuccess++type ConsumerM i r = Consumer i (ReaderT Env (EitherT String IO)) r+type ProducerM i r = Producer i (ReaderT Env (EitherT String IO)) r+type PipeM a b r = Pipe a b (ReaderT Env (EitherT String IO)) r
+ src/Data/String/Indent.hs view
@@ -0,0 +1,6 @@+module Data.String.Indent (fromIndent) where+import qualified Data.List as L++fromIndent :: String -> Int -> String+fromIndent orig margin = + L.unlines . L.map (L.replicate margin ' ' ++) . L.lines $ orig
+ src/Data/String/Util.hs view
@@ -0,0 +1,10 @@+module Data.String.Util (preposition) where+import Data.String.Indent+import qualified Data.List as L++preposition :: String -> String -> String -> String -> [String] -> String +preposition problem prep subjectLabel subject problems = + L.intercalate "\n" $+ (problem ++ ' ':prep ++ ' ':subjectLabel ++ ":") + : (' ':subject)+ : "with problem(s):" : L.lines (fromIndent (L.unlines problems) 1)
+ src/Haddock.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE OverloadedStrings #-}+module Haddock where+import qualified Filesystem.Path.CurrentOS as FPC+import Documentation.Haddock++data HaddockArg = HaddockArg { name :: String, value :: Maybe String } ++instance Show HaddockArg where+ show arg = "--" ++ name arg ++ maybe [] (\v -> '=':v) (value arg) ++runHaddock :: [HaddockArg] -> IO () +runHaddock = haddock . map show ++flag :: String -> HaddockArg+flag = flip HaddockArg Nothing++readInterface :: FPC.FilePath -> HaddockArg +readInterface ipath = + HaddockArg "read-interface" . Just $ + (FPC.encodeString . FPC.filename . FPC.dropExtension $ ipath) ++ ',': FPC.encodeString ipath++-- | Given a directory with .haddock interface specs, +-- write index files and TOC for those to the document directory. +runHaddockIndex :: FPC.FilePath -> FPC.FilePath -> IO ()+runHaddockIndex interface document_dir =+ runHaddock $ [+ -- generate html index file(s)+ flag "gen-index", + -- generate the html contents file+ flag "gen-contents",+ -- output directory for index file(s) is same as document dir + HaddockArg "odir" . Just . FPC.encodeString $ document_dir+ ] ++ [readInterface interface]
+ src/Haddock/Artifact.hs view
@@ -0,0 +1,43 @@+module Haddock.Artifact where+import Control.Monad.IO.Class+import Control.Monad.M+import Data.String.Util+import Documentation.Haddock+import qualified Filesystem.Path.CurrentOS as P+import qualified Module as Ghc+import qualified Name as Ghc++-- This ADT approach is heavily influenced by philopen's haddocset package:+-- https://github.com/philopon/haddocset++data Artifact+ = Haddock P.FilePath -- Note, this is not yet honored. TODO+ | Package + | Module Ghc.Module+ | Function Ghc.Module Ghc.Name++parseError :: String -> P.FilePath -> M r+parseError e p = + err $ preposition "parser error" "in" "haddock interface" (P.encodeString p) [e]++fromInterfaces :: Ghc.PackageId -> [InstalledInterface] -> [Artifact] +fromInterfaces _ [] = [] +fromInterfaces pkg (i:rest) =+ let moduleName = instMod i in+ if OptHide `notElem` instOptions i then + Module moduleName : + foldl + (\a e -> Function moduleName e : a)+ -- append to artifacts from rest of installed interfaces+ (fromInterfaces pkg rest) + (instVisibleExports i)+ else+ fromInterfaces pkg rest + +toArtifacts :: Ghc.PackageId -> P.FilePath -> M [Artifact]+toArtifacts pkg haddock' = do + interface_file <- liftIO $ readInterfaceFile freshNameCache (P.encodeString haddock')+ case interface_file of+ Left e -> parseError e haddock'+ Right (InterfaceFile _ installed_ifaces) ->+ return $ Haddock haddock' : Package : fromInterfaces pkg installed_ifaces
+ src/Haddock/Sqlite.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE OverloadedStrings #-}+module Haddock.Sqlite where+import Control.Monad.IO.Class+import Control.Monad.M+import Data.Monoid+import qualified Data.Text as T+import Database.SQLite.Simple+import Haddock.Artifact+import qualified Module as Ghc+import qualified Name as Ghc++data IndexRow = IndexRow {+ nameAttr :: T.Text + , typeAttr :: T.Text + , pathAttr :: T.Text + , modAttr :: T.Text +} deriving (Show)++instance Monoid IndexRow where+ mempty = IndexRow mempty mempty mempty mempty+ mappend l r = + IndexRow + (mappend (nameAttr l) (nameAttr r))+ (mappend (typeAttr l) (typeAttr r))+ (mappend (pathAttr l) (pathAttr r))+ (mappend (modAttr l) (modAttr r))++-- TODO lensify+instance ToRow IndexRow where+ toRow index = + [SQLText . nameAttr $ index+ , SQLText . typeAttr $ index+ , SQLText . pathAttr $ index+ , SQLText . modAttr $ index+ ]+ +-- I probably chould derive this from a type, but that's overkill right now.+table :: String+table = "searchIndex(name, type, path, module)"++createTable :: Connection -> IO ()+createTable conn =+ mapM_ (execute_ conn) + ["CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT, module TEXT);",+ Query . T.pack $ "CREATE UNIQUE INDEX anchor ON " ++ table ++ ";"]++insertRow :: Connection -> IndexRow -> IO ()+insertRow conn =+ execute conn + (Query . T.pack $ "INSERT OR IGNORE INTO " ++ table ++ " VALUES (?,?,?,?);")++modUrl :: Ghc.Module -> String+modUrl = + map (\c-> if c == '.' then '-' else c)+ . Ghc.moduleNameString + . Ghc.moduleName ++escapeSpecial :: String -> String +escapeSpecial = + concatMap (\c -> if c `elem` specialChars then '-': show (fromEnum c) ++ "-" else [c])+ where+ specialChars = "!&|+$%(,)*<>-/=#^\\?"+ +-- | Update the sqlite database with the given haddock artifact.+fromArtifact :: Ghc.PackageId -> Connection -> Artifact -> M ()+fromArtifact p conn art = do+ attributes <- toAttributes+ case attributes of + Just (name, type', path, m) -> + liftIO . insertRow conn $ + mempty { + nameAttr = T.pack name+ , typeAttr = type'+ , pathAttr = T.pack path+ , modAttr = T.pack m+ }+ Nothing -> + return ()+ where+ modStr m = Ghc.moduleNameString $ Ghc.moduleName m+ -- | Convert haddock artifacts to attributes for table update.+ toAttributes = + case art of + Haddock _ ->+ -- TODO unsupported right now + return Nothing + Package -> + return . Just $ + (Ghc.packageIdString p, "Package", "index.html", [])+ Module ghcmod -> + return . Just $+ (modStr ghcmod, "Module" , modUrl ghcmod ++ ".html" , modStr ghcmod)+ Function ghcmod ghcname -> + let (declType, pfx) = toPair ghcname in+ return . Just $ + ( modStr ghcmod ++ '.':Ghc.getOccString ghcname,+ declType, + url pfx,+ modStr ghcmod)+ where+ url pfx =+ modUrl ghcmod ++ ".html#" ++ pfx : ':' : + escapeSpecial (Ghc.getOccString ghcname)+ toPair n+ | Ghc.isTyConName n = ("Type" , 't')+ | Ghc.isDataConName n = ("Constructor" , 'v')+ | otherwise = ("Function" , 'v')
src/Main.hs view
@@ -34,6 +34,6 @@ where parserInfo :: ParserInfo Options parserInfo = info (helper <*> parser) $- header "dash-haskell v1.0.0.0, a dash docset construction tool for Haskell packages"+ header "dash-haskell v1.0.0.1, a dash docset construction tool for Haskell packages" <> progDesc "additional help is available with \"dash-haskell help <topic|option>\"" <> footer "http://www.github.com/jfeltz/dash-haskell (C) John P. Feltz 2014"
+ src/Options.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE TupleSections #-}++module Options where++import Control.Applicative+import Control.Monad.M+import qualified Data.List as L++import Data.Monoid+import qualified Distribution.Package as C+import qualified Distribution.Version as CV+import Distribution.Text+import Options.Applicative.Builder+import Options.Applicative.Common+import Options.Cabal+import Options.CabalConstraints (toConstraints, none, CabalConstraints)+import Options.DbProvider+import PackageId+import Pipes+import qualified Data.Set as S+ +data Options = Options { + dbprovider :: DbProvider,+ outputDir :: FilePath,+ quiet :: Bool,+ cabal :: Maybe FilePath,+ cabalConstraints :: CabalConstraints, + packages :: [C.PackageId]+} deriving Show++parser :: Parser Options+parser = + Options <$> + option toProvider + (long "dbprovider" + <> short 'p'+ <> metavar "<provider,args>"+ <> value (CabalSandbox Nothing) + <> help "a ghc package db provider: cabal|ghc|dir\n")+ <*>+ strOption (+ long "output" + <> short 'o'+ <> metavar "<dir>" + <> value "./docsets" + <> help "the directory to write created docsets to")+ <*>+ switch (long "quiet" <> short 'q' <> help "set to quiet output")+ <*>+ option (return . Just) + (long "cabal"+ <> short 'c' + <> metavar "<file.cabal>" + <> value Nothing+ <> help "the cabal file to retrieve package dependencies from")+ <*>+ option toConstraints+ (long "cabal-constraints" + <> short 'r'+ <> value none + <> metavar "executable=name, .."+ <> help "limit package results from a cabal file source, see documentation")+ <*>+ many (+ argument simpleParse (metavar "packages" <> + help "a list of packages to specifically build, e.g. either-1.0.1 text"+ ))++-- | Given two lists of package satisfying strings, +-- return a list that is non-duplicate, the most versioned of the two.+-- e.g. if 'either-4.1.0' is in one, and 'either' is the other, +-- the versioned is chosen. If both are versioned, both appear in+-- the final result.+reduce :: [C.PackageId] -> [C.PackageId]+reduce = fromAsc . L.sort where+ -- Here we exploit the fact that you only need to examine the next member of + -- an ascending list to determine a version+ fromAsc :: [C.PackageId] -> [C.PackageId]+ fromAsc [] = []+ fromAsc (p:[]) = [p] + fromAsc (p:nxt:rest)+ | p == nxt = -- duplicate + fromAsc (nxt:rest) + | unversioned p == unversioned nxt = + if unversioned p == p then+ fromAsc $ nxt:rest+ else -- both are versioned by list ordering+ p : nxt : fromAsc rest + | otherwise = -- they're different packages+ p : fromAsc ( nxt : rest )+ +versionless :: String -> C.PackageId+versionless n = C.PackageIdentifier (C.PackageName n) $ CV.Version [] [] ++prod_Packages :: Options -> ProducerM (S.Set String) () +prod_Packages options = do+ cabal_dependencies <-+ lift $ case cabal options of + Nothing -> return [] + Just fp -> (S.toList . S.map versionless) + <$> readPackages fp (cabalConstraints options)+ yield . S.fromList . map (show . disp) $ + reduce (packages options ++ cabal_dependencies)
+ src/Options/Cabal.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TupleSections #-}+ +module Options.Cabal where+import qualified Data.Set as S+import Control.Monad.M+import Data.Monoid+import qualified Data.Map as M+import qualified Data.List as L+import Control.Applicative+import Control.Monad+import qualified Distribution.PackageDescription.Parse as C+import qualified Distribution.Package as C+import qualified Distribution.PackageDescription as C+import Data.Either+import Data.String.Util+import Control.Monad.IO.Class+import qualified Options.CabalConstraints as OC++type Package = String+newtype Targets = Targets { mapping :: M.Map String (S.Set Package) }++fromTargets :: Targets -> S.Set Package+fromTargets = S.unions . M.elems . mapping + +data DependencyDescription =+ DependencyDescription { + library :: Maybe (S.Set Package),+ execs :: Targets,+ suites :: Targets, + benchmarks :: Targets+ } ++-- | Return the packages to use from the target type, or fail with unfound targets.+fromTargetType :: S.Set String -> Targets -> Either [String] (S.Set String) +fromTargetType narrowing tgts = do+ used_targets <- -- return a list of used targets + if S.null narrowing then+ return . M.keysSet . mapping $ tgts + else -- error check narrowed packages+ let found = S.intersection narrowing $ M.keysSet (mapping tgts)+ unfound = S.difference narrowing found+ in if not . S.null $ unfound then+ Left . S.toList $ unfound + else + return found + -- accumulate dependencies from found keys+ return $+ S.unions + . map snd+ . M.toList+ . M.intersection (mapping tgts)+ . M.fromList $ zip (S.toList used_targets) (replicate (S.size used_targets) ())++toPackages :: FilePath -> DependencyDescription -> OC.CabalConstraints -> M (S.Set Package) +toPackages cabal desc constraints = + case toPkgs pairings of+ Left unfound ->+ err $ + preposition + "failed to find targets" + "in"+ "cabal file"+ cabal + unfound+ Right set ->+ let + matched_excluded = S.intersection set (OC.excluded constraints)+ remainder = S.difference (OC.excluded constraints) matched_excluded + in do+ unless (S.null remainder) . + warning . + L.intercalate "\n" $+ "packages to exclude were not found:" : S.toList remainder + return $ S.difference set (OC.excluded constraints)+ where+ -- | Produce a list of targets to evaluate based off selection, e.g.+ -- if any fst member of tuple is non-empty, the subset is returned.+ -- if all are non-empty, all are considered + toPkgs :: [(S.Set String, Targets)] -> Either [String] (S.Set Package) + toPkgs list =+ case L.partition (S.null . fst) list of+ (non_selections, []) -> + Right . S.unions . map (fromTargets . snd) $ non_selections + (_, selections) -> + case partitionEithers (L.map (uncurry fromTargetType) selections) of + ([],sets) -> Right . S.unions $ sets + (unfound,_) -> Left . concat $ unfound + + -- | Return pairings of expected cabal targets to actual cabal targets+ pairings :: [(S.Set String, Targets)] + pairings = + let + lib_pairing :: (S.Set Package, Targets)+ lib_pairing = + (if OC.lib constraints then S.singleton "library" else mempty,+ Targets $ maybe mempty (M.singleton "library") (library desc)) + in+ lib_pairing :+ zip (map ($ constraints) [OC.execs, OC.suites, OC.benchmarks])+ (map ($ desc) [execs, suites, benchmarks]) ++toStrName :: C.Dependency -> String+toStrName (C.Dependency (C.PackageName name) _) = name+ +toTargets :: [(String, C.CondTree a [C.Dependency] b)] -> Targets+toTargets = + let + toStrDeps target tree = (target, S.fromList . map toStrName $ C.condTreeConstraints tree) + in+ Targets . M.fromList . map (uncurry toStrDeps)++-- | Given the defined constraints, return packages satisfying from the cabal file.+readPackages :: FilePath -> OC.CabalConstraints -> M (S.Set Package)+readPackages cabal constraints = do + parse_result <- liftIO $ C.parsePackageDescription <$> readFile cabal+ case parse_result of+ (C.ParseFailed fail_msg) ->+ err . show $ fail_msg+ (C.ParseOk warnings desc) -> do+ unless (L.null warnings) . warning $ + preposition + "warnings during parse" + "of"+ "cabal file"+ "warnings"+ (map show warnings)+ + toPackages cabal (toDescription desc) constraints + where+ -- Produce a simplified description of the cabal file for processing.+ toDescription :: C.GenericPackageDescription -> DependencyDescription + toDescription gpd = + DependencyDescription+ (S.fromList . map toStrName . C.condTreeConstraints <$> C.condLibrary gpd)+ (toTargets . C.condExecutables $ gpd)+ (toTargets . C.condTestSuites $ gpd)+ (toTargets . C.condBenchmarks $ gpd)
+ src/Options/CabalConstraints.hs view
@@ -0,0 +1,92 @@+module Options.CabalConstraints where++import qualified Data.Set as S+import Data.Monoid+import Text.ParserCombinators.Parsec+import Control.Applicative hiding ((<|>), many, optional)+import Control.Monad+import qualified Options.Applicative.Types as O++data CabalConstraints = + CabalConstraints {+ lib :: Bool,+ execs :: S.Set String, -- The execs to limit to+ suites :: S.Set String, -- The suites to limit to+ benchmarks :: S.Set String, -- The benchmarks to limit to+ excluded :: S.Set String -- Packages to exclude+ } deriving (Show, Eq)++instance Monoid CabalConstraints where + mempty = CabalConstraints False mempty mempty mempty mempty+ mappend l r = + CabalConstraints + (lib r || lib l) -- This just biases to True + (mappend (execs l) (execs r))+ (mappend (suites l) (suites r))+ (mappend (benchmarks l) (benchmarks r))+ (mappend (excluded l) (excluded r))++none :: CabalConstraints +none = mempty ++type Constructor = S.Set String -> CabalConstraints++excluded',benchmarks',suites',execs' :: Constructor +excluded' s = none { excluded = s } +benchmarks' s = none { benchmarks = s } +execs' s = none { execs = s } +suites' s = none { suites = s } ++-- | Parser for the cabal constraints option.+-- Note: I don't think this can be simpliifed with sepBy since the +-- non-sep parser may fail, causing input and positioning to be lost.+-- If someone knows a better way, please advise/patch.+constraints :: Maybe Constructor -> Parser CabalConstraints +constraints Nothing =+ -- parse a lhs binding or lib + choice $ map try [+ -- lib as leader+ do + c <- string "library" >> return (none { lib = True })+ choice [+ char ',' >> mappend c <$> constraints Nothing, + eof >> return c+ ]+ , do + ctor <- binding+ constraints (Just ctor) + ]+ where+ binding :: Parser Constructor + binding = do + ctor <- choice $ map try [ + string "executables" >> return execs',+ string "suites" >> return suites',+ string "benchmarks" >> return benchmarks',+ string "excluded" >> return excluded'+ ]+ void . char $ '=' + return ctor+constraints (Just ctor) = do+ -- parse the next value+ v <- value + -- the next is either eol, or a deliminated binding/value + c <- choice [+ eof >> return none,+ try (char ',' >> constraints Nothing), -- binding pre-empts value+ try (char ',' >> constraints (Just ctor))+ ]+ return $ mappend (ctor . S.singleton $ v) c+ where+ value :: Parser String+ value = do + var <- many1 (alphaNum <|> char '-')+ notFollowedBy (char '=')+ return var++toConstraints :: String -> O.ReadM CabalConstraints+toConstraints expr = + O.ReadM $ + case parse (constraints Nothing) [] expr of + Left err -> Left . O.ErrorMsg . show $ err+ Right c -> Right c
+ src/Options/DbProvider.hs view
@@ -0,0 +1,71 @@+module Options.DbProvider where+import qualified Data.List as L+import Options.Applicative.Types+import Control.Applicative+import Control.Monad+import Data.Maybe++data DbProvider = + CabalSandbox (Maybe String)+ | Ghc (Maybe String) + | Db FilePath ++instance (Show DbProvider) where+ show dbp = + let (cmd, args) = toExec dbp in + L.intercalate "\n"+ ["lookup strategy: " ++ desc, "cmd: " ++ cmd ++ "\nargs: " ++ unwords args] + where+ desc = + case dbp of + CabalSandbox _ -> "cabal sandbox db index" + Ghc _ -> "ghc db index" + Db _ -> "ghc db index with directory narrowing" ++fromSplit :: Char -> String -> ReadM (String, Maybe String)+fromSplit c opt = + case opt of + [] -> return ([], Nothing) + (s:str) ->+ if s == c then do+ param <- fromParam str + return ([], param)+ else do + (l, r) <- fromSplit c str+ return (s:l, r)+ where + fromParam [] = return Nothing+ fromParam (c':str)=+ if c' == c then+ ReadM . Left . ErrorMsg $ "encountered delimeter(" ++ c:") twice" + else+ Just . maybe [c'] (c':) <$> fromParam str++toExec :: DbProvider -> (String, [String])+toExec (CabalSandbox args) = + (,) "cabal" $ ["sandbox", "hc-pkg", "list"] ++ maybeToList args+toExec (Ghc args) = + (,) "ghc-pkg" $ "list" : maybeToList args+toExec (Db fp) = + (,) "ghc-pkg" ("list":["--package-db=" ++ fp]) ++toProvider :: String -> ReadM DbProvider+toProvider expr = do + (prov, arg) <- fromSplit ',' expr+ join $ constructor prov <*> pure arg+ where + constructor :: String -> ReadM (Maybe String -> ReadM DbProvider) + constructor prov =+ ReadM $ maybe (Left . ErrorMsg $ "invalid db provider") Right f+ where + f :: Maybe (Maybe String -> ReadM DbProvider)+ f = + -- return a constructor given an arg+ L.lookup prov+ [("ghc" , return . Ghc),+ ("cabal" , return . CabalSandbox),+ ("dir" , + maybe + (ReadM . Left . ErrorMsg $ "requires directory path")+ (return . Db))+ ]
+ src/Options/Documentation.hs view
@@ -0,0 +1,114 @@+module Options.Documentation where+import qualified Data.Set as S+import qualified Data.List as L+import qualified Data.Map as M+import Data.String.Indent++data Topic = Topic { title :: String, content :: String } +instance Eq Topic where+ l == r = title l == title r+instance Ord Topic where+ compare l r = compare (title l) (title r)++type Tag = String ++type Documentation = M.Map Tag Topic ++tags :: Documentation -> [String] +tags = M.keys ++cabalTopic, providerTopic, outputTopic, packageTopic :: Topic+cabalTopic = + Topic "cabal constraints" $+ L.intercalate "\n" [+ "These are constraints on packages pulled from a cabal file source.",+ "These constraints allow one to simply select subsets of cabal targets to use",+ "and subsets of packages to avoid sourcing.\n",+ "Some options support multivalue bindings, and are of the form:",+ " -r <var=value>,<var2=value1,value2>",+ "e.g. \"-r executables=foo,excluded=base,ghc\"",+ "\npossible variables:\n",+ " executables : the executable targets to limit to",+ " suites : the suite targets to limit to",+ " benchmarks : the benchmark targets to limit to",+ " library : limit to library, this takes no arguments\n",+ " excluded : unversioned packages to avoid using"+ ]++providerTopic = Topic "package database provider" $+ L.intercalate "\n" [+ "The external program to call to produce package databases\n"+ , "options are the form <var,args>" + , "e.g:" + , " -p ghc,'--user'"+ , " or -p dir,/home/jpf/.ghc/x86_64-linux-7.8.3/package.conf.d\n"+ , "Note, only one provider at once is supported at this time.\n"+ , "pairings for <var,args>:"+ , " var args "+ , "----------------------------------------- --------------------------------"+ , " cabal : use cabal sandbox package db(s) flag string to pass to cabal"+ , " ghc : use ghc's package db(s), flag string to pass to ghc"+ , " dir : use package db dir directory the package db directory"+ + ]++outputTopic= Topic "output" $ + L.intercalate "\n" [+ "The directory root of which to write .docset directories to\n"+ , "For each package sourced by dash-haskell, a matching docset"+ , "will be written to that directory in its full version form, e.g:\n"+ , " \"output/package-1.2.3.docset\""+ ]++packageTopic= Topic "package" $+ L.intercalate "\n" + ["a ghc package, e.g. either, or either-4.1.0" ,+ "1. dash-haskell will choose the versioned package if provided", + "both unversioned and versioned",+ "2. If the package is unversioned it will choose the first as located by",+ " the package db provider."+ ]++toEntry :: S.Set String -> Topic -> Documentation -> Documentation +toEntry s t doc = L.foldl (\m' k -> M.insert k t m') doc $ S.toList s ++docs :: Documentation+docs = + let fromListing m (t,l) = toEntry (S.fromList l) t m in + L.foldl fromListing M.empty [+ (cabalTopic , ["r", "cabal-constraints"]),+ (providerTopic , ["p", "dbprovider", "provider"]),+ (outputTopic , ["o", "outputdir", "output", "docset"]),+ (packageTopic , ["packages", "package"])+ ]++-- | Given an arbitrary stream of options,+-- convert to possible topic indexes for documenation lookup.+toIndexes :: [String] -> [String] +toIndexes = S.toList . S.fromList . map toTopic + where+ toTopic :: String -> String+ toTopic [] = [] + toTopic ('-':rest) = toTopic rest + toTopic remainder = remainder ++toHelp :: Documentation -> [String] -> IO () +toHelp d [] = + putStrLn $ "available help topics:\n" ++ L.intercalate ", " (tags d)+toHelp d args = + -- fold over each index, producing a list of docs to display + let topic_set = L.foldl folded S.empty $ toIndexes args in + if S.null topic_set then do + putStrLn "Sorry, no documentation available for expressions given.\n"+ toHelp docs [] + else do+ putStrLn "accessing help on topics, \n"+ putStr . L.intercalate "\n" . L.map headed $ S.toList topic_set+ putStr "\n"+ where + headed :: Topic -> String + headed topic = ' ':title topic ++ ':':'\n':'\n': + fromIndent (content topic) 2++ folded :: S.Set Topic -> String -> S.Set Topic+ folded s index = maybe s (`S.insert` s) $ M.lookup index d
+ src/Package.hs view
@@ -0,0 +1,10 @@+module Package where+import Data.Char++unversioned :: String -> String+unversioned [] = + [] +unversioned ('-':c:rest) =+ if isDigit c then [] else '-':c:unversioned rest +unversioned (c:rest) = + c:unversioned rest
+ src/Package/Conf.hs view
@@ -0,0 +1,10 @@+module Package.Conf where+import qualified Module as Ghc+import qualified Filesystem.Path.CurrentOS as P++data Conf = Conf+ { pkg :: Ghc.PackageId+ , interfaceFile :: P.FilePath -- interface, i.e. .haddock file+ , htmlDir :: P.FilePath -- root html source directory+ , exposed :: Bool -- module exposure flag+ }
+ src/PackageId.hs view
@@ -0,0 +1,9 @@+module PackageId where+import qualified Distribution.Package as C+import qualified Distribution.Version as CV++emptyVersion :: CV.Version+emptyVersion = CV.Version [] []+ +unversioned :: C.PackageId -> C.PackageId+unversioned p = p { C.pkgVersion = emptyVersion }
+ src/Pipes/Conf.hs view
@@ -0,0 +1,76 @@+module Pipes.Conf where+import Control.Applicative+import Control.Monad+import Control.Monad.M+import Control.Monad.Trans.Either+import qualified Data.List as L+import Data.Maybe+import Data.String.Util+import Distribution.InstalledPackageInfo as DIP+import qualified Filesystem.Path.CurrentOS as P+import qualified Module as Ghc+import Package.Conf+import qualified PackageConfig as Ghc+import Pipes+import qualified System.Directory as D++checkPath :: Bool -> P.FilePath -> String -> IO (Maybe String) +checkPath dir path name = do + exists <- predicate . P.encodeString $ path + return $ if exists then Nothing else Just $ "missing: " ++ name ++ ' ':msg'+ where + (predicate, msg') =+ if dir then (D.doesDirectoryExist, "dir") else (D.doesFileExist, "file")++confError :: FilePath -> String -> M r+confError path fatal = + err $ preposition "parse errors" "in" "conf file" path [fatal] + +fromParseResults :: FilePath -> ParseResult InstalledPackageInfo -> M Conf+fromParseResults conf (ParseFailed _) = + confError conf "package conf file parse failed"+fromParseResults conf (ParseOk cabalWarnings fields)+ | null (haddockHTMLs fields) =+ confError conf "failed to extract html field from conf file"+ | null (haddockInterfaces fields) = + confError conf "failed to extract haddock interface field from conf file"+ | otherwise = do+ unless (L.null cabalWarnings) $+ liftIO . putStr $+ "parse of:\n " ++ conf ++ "\n completed with warnings: " + ++ L.intercalate "\n" (map show cabalWarnings) + lift . right $+ Conf + -- We're not using Cabal's type information beyond just+ -- extracting package data. Ghc types are used for the rest.+ (Ghc.mkPackageId . sourcePackageId $ fields)+ -- TODO Respect multiple interfaces, however + -- this is not the common consensus for use of haddock interfaces. + (P.decodeString (head $ haddockInterfaces fields))+ (P.decodeString $ head (haddockHTMLs fields) ++ "/")+ (DIP.exposed fields)++diagnosePaths :: Conf -> M [String]+diagnosePaths conf = do+ (html_doc_warnings, interface_warnings) <- liftIO $ + liftM2 (,)+ (checkPath True (htmlDir conf) "html doc")+ (checkPath False (interfaceFile conf) "haddock interface")+ return $ catMaybes [interface_warnings, html_doc_warnings] ++pipe_Conf :: PipeM FilePath Conf ()+pipe_Conf = forever $ do+ pkg_db_conf <- await+ -- Retrieve the package conf file from the package db+ parse_results <- liftIO $ DIP.parseInstalledPackageInfo <$> Prelude.readFile pkg_db_conf+ c <- lift $ fromParseResults pkg_db_conf parse_results++ errors <- lift $ diagnosePaths c+ if L.null errors then+ yield c+ else + lift $+ do msg "\n"+ warning $ "failed to process: " ++ Ghc.packageIdString (pkg c)+ warning $ preposition "path errors" "in" "pkg conf file" pkg_db_conf errors+ msg "\n"
+ src/Pipes/Db.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE OverloadedStrings #-}+module Pipes.Db (pipe_ConfFp) where+import Control.Applicative+import Control.Monad+import Control.Monad.M+import Control.Monad.State+import Data.Either+import qualified Data.List as L+import qualified Data.Set as S+import qualified Data.Text as T+import Filesystem+import qualified Filesystem.Path.CurrentOS as P+import qualified Module as Ghc+import Options.DbProvider+import Package (unversioned)+import Pipes+import System.Exit+import System.Process++isPackage :: String -> Bool+isPackage str = str /= " (no packages)" && " " `L.isPrefixOf` str ++-- | If parsed matches a member of the set by version first, return,+-- otherwise return unversioned match.+toPkgMatch :: String -> State (S.Set String) (Maybe (String, Ghc.PackageId)) +toPkgMatch parsed = do+ unassigned <- get + if S.member parsed unassigned -- found versioned + then fromFound parsed+ else + let parsed' = unversioned parsed in+ if S.member parsed' unassigned + then fromFound parsed'+ else return Nothing + where+ fromFound :: String -> State (S.Set String) (Maybe (String, Ghc.PackageId))+ fromFound p = do+ modify (S.delete p)+ return . Just $ (p, Ghc.stringToPackageId parsed)++-- | A crude parser that extracts db -> package set relations based+-- on whitespace indentation +accumPaths :: + [String] -- output of ghc-pkg or ghc-pkg like listing+ -> S.Set String -- set of packages not yet associated to a db + -> [(FilePath, [(String, Ghc.PackageId)])]+ -> M [(FilePath, [(String, Ghc.PackageId)])]+accumPaths [] _ assigned = + return assigned +accumPaths (l:rest) unassigned assigned = + if S.null unassigned then -- we're done + return assigned+ else+ case assigned of + [] -> + if isPath l then+ accumPaths rest unassigned [(toPath l, [])] + else+ err "parse error on package list output, check program used" + ((db, members):dbs) ->+ uncurry (accumPaths rest) $ + if isPath l then -- it's another db+ (,) unassigned ((toPath l, []):assigned)+ else -- it could be a package + if isPackage l then+ let + (r, unassigned') =+ runState (toPkgMatch (unhide $ drop 4 l)) unassigned+ in (,) unassigned' $ + maybe + assigned -- no change + (\p -> (db, p : members) : dbs) -- at to members + r+ else+ (unassigned, assigned)+ where+ unhide :: String -> String+ unhide ('(':str) = L.take (L.length str - 1) str + unhide s = s ++ -- | remove last ':' and junk + -- Interestingly, when running cabal and ghc proc, it's stdout actually + -- appends colons to paths, instead of what appears on console.+ -- So we're stripping them blindly for now.+ toPath :: String -> String+ toPath s = L.take (L.length s - 1) s++ isPath :: String -> Bool+ isPath [] = False+ isPath s = head s `L.notElem` "\n\r\t "++toMapping :: String -> [String] -> S.Set String -> M [(FilePath, [(String, Ghc.PackageId)])]+toMapping cmd args pkgs = do+ res <- liftIO $ readProcessWithExitCode cmd args []+ case res of + (ExitFailure _, out, err') ->+ err $ + "failed to retrieve package db's from " ++ cmd ++", output:"+ ++ out ++ '\n':err' + (ExitSuccess, out, _) ->+ accumPaths (L.lines out) pkgs []++-- | This returns a non-empty list of package db's, or failure.+fromProvider :: DbProvider -> S.Set String -> M [(FilePath, [(String, Ghc.PackageId)])]+fromProvider prov pkgs = do+ case prov of + (Ghc _) -> toMapping cmd extra_args pkgs + (CabalSandbox _) -> toMapping cmd extra_args pkgs+ (Db fp) -> fromProvider (Ghc . Just $ "--package-db=" ++ fp) pkgs+ where+ (cmd , extra_args) = toExec prov++isConf :: Ghc.PackageId -> P.FilePath -> Bool +isConf p f = P.hasExtension f "conf" && pkgRelated p f+ +findConf :: Ghc.PackageId -> State (S.Set P.FilePath) (Either Ghc.PackageId P.FilePath)+findConf package = do+ files <- get + let matching = S.filter (isConf package) files+ if not . S.null $ matching+ then let found = S.findMin matching in do+ modify (S.delete (S.findMin matching))+ return $ Right found + else+ return $ Left package + +-- | Return a list of package configurations for the given+-- db and handled packages+fromPair :: FilePath -> [Ghc.PackageId] -> M [FilePath]+fromPair _ [] =+ return [] +fromPair db members = do + confs <- S.fromList <$> liftIO (listDirectory (P.decodeString db))+ let (remainder, confs') = partitionEithers $ evalState (mapM findConf members) confs + unless (L.null remainder) . warning $ + "The following packages were not found in the pkg db dir: \n" ++ + L.intercalate "\n" (map Ghc.packageIdString remainder)+ mapM (return . P.encodeString) confs'++pkgRelated :: Ghc.PackageId -> P.FilePath -> Bool+pkgRelated p = + T.isPrefixOf (T.pack . Ghc.packageIdString $ p) + . T.pack + . P.encodeString + . P.filename++pipe_ConfFp :: DbProvider -> PipeM (S.Set String) FilePath ()+pipe_ConfFp prov = do + packages <- await + if S.null packages + then + lift . err $ "no results possible due to no packages provided" + else do+ lift $ do+ msg "db provider:"+ indentM 2 $ msg . show $ prov + msg "\n"+ pairings <- lift $ fromProvider prov packages++ let found = S.unions $ map (S.fromList . map fst . snd) pairings + unfound = S.difference packages found ++ if not . S.null $ unfound then+ lift . err $ + L.intercalate "\n" + ("The following packages were not found in searched package db's:" + : S.toList unfound ++ ["Please be sure to provide exact package versions."])++ else -- yield over each returned file,+ -- types are added to make this _much_ easier to understand + let + mapped :: (FilePath, [(String, Ghc.PackageId)]) -> M [FilePath]+ mapped = uncurry fromPair . (\(db, members) -> (db, map snd members))+ in + mapM_ yield =<< lift (L.concat <$> mapM mapped pairings)
+ src/Pipes/FileSystem.hs view
@@ -0,0 +1,239 @@+{-# LANGUAGE OverloadedStrings #-}+module Pipes.FileSystem where+import Control.Applicative+import Control.Monad+import Control.Monad.M+import qualified Data.ByteString as BS+import Data.ByteString.Char8 (pack)+import qualified Data.List as L+import Data.String.Util+import qualified Data.Text as T+import Data.Text.Encoding+import Database.SQLite.Simple+import Filesystem as F+import Filesystem.Path.CurrentOS ((</>))+import qualified Filesystem.Path.CurrentOS as P+import qualified Module as Ghc+import Package.Conf+import Pipes+import System.Directory ( doesDirectoryExist, getDirectoryContents )+import qualified System.Directory as D+import Text.HTML.TagSoup+import Text.HTML.TagSoup.Match+import Haddock.Artifact+import Haddock.Sqlite++-- | See:+-- https://developer.apple.com/library/ios/documentation/general/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html+data Plist = Plist+ { cfBundleIdentifier :: String+ , cfBundleName :: String+ , docSetPlatformFamily :: String+ } deriving Show++-- TODO/FIXME the utility of some of these fields are still unclear to me,+-- at the moment they are filled simply to satisfy the docset spec.+plist :: Ghc.PackageId -> BS.ByteString+plist p = Data.ByteString.Char8.pack . unlines $+ [ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+ , "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"+ , "<plist version=\"1.0\">"+ , "<dict>"+ , "<key>CFBundleIdentifier</key>"+ , "<string>" ++ Ghc.packageIdString p ++ "</string>"+ , "<key>CFBundleName</key>"+ , "<string>docset for Haskell package " ++ Ghc.packageIdString p ++ "</string>"+ , "<key>DocSetPlatformFamily</key>"+ , "<string>haskell</string>" + , "<key>isDashDocset</key>"+ , "<true/>"+ , "<key>dashIndexFilePath</key>"+ , "<string>index.html</string>"+ , "</dict>"+ , "</plist>"+ ]++docsetDir :: Ghc.PackageId -> P.FilePath+docsetDir p = P.decodeString $ Ghc.packageIdString p ++ ".docset" ++leafs :: (P.FilePath -> Bool) -> P.FilePath -> ProducerM P.FilePath ()+leafs incPred p = do+ names <- liftIO . getDirectoryContents $ P.encodeString p+ forM_ (filter (`notElem` [".", ".."]) names) $ \name' -> do+ let path = p </> P.decodeString name'+ dir <- liftIO . doesDirectoryExist $ P.encodeString path+ (if dir then + leafs incPred + else if not . incPred $ path then const (return ()) else yield)+ path++type Tag' = Tag T.Text ++remoteUrl :: T.Text -> Bool +remoteUrl url = any (T.isPrefixOf url) ["http://", "https://"]++toStripped :: P.FilePath -> P.FilePath -> Either String P.FilePath+toStripped pfx original =+ -- I don't understand why System.FilePath.CurrentOS necessitates+ -- additional checking after the prefix has already been determined.+ case P.stripPrefix pfx original of + Nothing ->+ Left $+ "attempted to strip prefix: \n " + ++ P.encodeString pfx ++ " from: \n " ++ P.encodeString original+ Just remainder -> + Right remainder++toRelativePath :: P.FilePath -> P.FilePath -> Either String P.FilePath+toRelativePath base path = do+ let sharedPfx = P.commonPrefix [base, path]+ relative <- relativePath sharedPfx + (</>) relative <$> toStripped sharedPfx path + where + relativePath :: P.FilePath -> Either String P.FilePath+ relativePath pfx = + P.concat + . flip replicate ".." + . length + . P.splitDirectories <$> toStripped pfx base ++relativize :: Ghc.PackageId -> P.FilePath -> Either String T.Text +relativize package p = + let filename = P.filename p+ packageSubpath = P.decodeString $ Ghc.packageIdString package+ matches = filter (packageSubpath ==) . reverse $ P.splitDirectories (P.parent p)+ in + T.pack . P.encodeString <$> + if L.null matches then + return p -- preserve the path so that it still can be used + else -- assume as a package doc file and make relative+ toRelativePath packageSubpath $ L.head matches </> filename++convertUrl :: Ghc.PackageId -> T.Text -> Either String T.Text +convertUrl p urlExp + | T.null urlExp = Right T.empty+ | otherwise = + if T.isPrefixOf "file:///" urlExp then + relativize p (P.fromText . T.drop 7 $ urlExp)+ else if T.isPrefixOf "/" urlExp then + relativize p $ P.fromText urlExp+ else+ Right urlExp+ +attributes :: P.FilePath -> Tag T.Text -> Either String [Attribute T.Text] +attributes _ (TagOpen _ list) = + Right list+attributes src other = + Left $+ "failed to retrieve expected attributes from tag:\n "+ ++ show other ++ "\n in: \n" ++ P.encodeString src + +-- | Convert local package-compiled haddock links to local relative. +convertLink :: Ghc.PackageId -> P.FilePath -> Tag' -> Either String Tag'+convertLink package src tag =+ -- We're only interested in processing links + if not $ tagOpenLit "a" (anyAttrNameLit "href") tag then + Right tag+ else do+ preserved <- filter (\(n,_) -> n /= "href") <$> attributes src tag + let url = fromAttrib "href" tag+ + if remoteUrl url + then + Right tag -- ignore remote links+ else do+ url' <- convertUrl package url + Right . TagOpen "a" $ ("href", url') : preserved ++pipe_htmlConvert :: Ghc.PackageId -> PipeM P.FilePath (P.FilePath, Maybe BS.ByteString) ()+pipe_htmlConvert p = + forever $ do+ src <- await+ if P.extension src /= Just "html" + then + yield (src, Nothing)+ else do + buf <- liftIO $ F.readTextFile src+ -- Link conversion errors are non-fatal.+ case mapM (convertLink p src) . parseTags $ buf of+ Left e -> do + lift . warning $ + preposition "failed to convert links" "for" "file" (P.encodeString src) [e]+ yield (src, Nothing) + Right tags ->+ yield (src, Just . encodeUtf8 . renderTags $ tags) ++-- | This consumes a doc file and copies it to a path in 'dstRoot'. +-- By pre-condition: +-- path has src_root as an ancestor +-- By post-condition: +-- written dst is the difference of path and src_root,+-- with by the concatenation of dst_root as it's parent. +cons_writeFile :: P.FilePath -> P.FilePath -> ConsumerM (P.FilePath, Maybe BS.ByteString) () +cons_writeFile src_root dst_root = forever $ do + (path, buf) <- await+ case P.stripPrefix src_root path of+ Nothing -> lift . err $ + "filepath error when attempting to find common prefix between src: \n" + ++ P.encodeString path ++ "\n and: \n" ++ P.encodeString src_root+ Just dst_relative_path ->+ -- Yes, this could be shorter, but I try not to unnecessarily obfuscate+ liftIO $ do + let dst_path = dst_root </> dst_relative_path+ -- create requisite parent directories for the file at the destination+ F.createTree $ P.parent dst_path + case buf of + Nothing -> F.copyFile path dst_path + Just buf' -> F.writeFile dst_path buf'+ +cons_writeFiles :: P.FilePath -> ConsumerM Conf ()+cons_writeFiles docsets_root = forever $ do+ conf <- await+ + lift . msg $ "processing: " ++ (Ghc.packageIdString . pkg $ conf)+ let docset_folder = docsetDir (pkg conf) + dst_root = docsets_root </> docset_folder + dst_doc_root = dst_root </> P.decodeString "Contents/Resources/Documents/"++ liftIO . F.createTree $ dst_doc_root + + -- Copy all files and convert if necessary + lift . indentM 2 $ msg "writing files.."+ + lift . runEffect $ + cons_writeFile (htmlDir conf) dst_doc_root + <-< pipe_htmlConvert (pkg conf)+ <-< leafs (\p -> P.extension p /= Just "haddock") (htmlDir conf)+ + -- FIXME/TODO Since the haddock index is already produced in source docs+ -- with latest packaging systems, this is likely unnecessary + -- liftIO $ do + -- putStrLn "running haddock indexes"+ -- runHaddockIndex (interfaceFile conf) dst_doc_root++ lift . indentM 2 $ msg "writing plist.."++ liftIO . F.writeFile (dst_root </> "Contents/Info.plist") $ plist (pkg conf) ++ lift . indentM 2 $ msg "populating database.."++ let db_path = dst_root </> P.decodeString "Contents/Resources/docSet.dsidx" ++ liftIO $ do+ db_exists <- D.doesFileExist . P.encodeString $ db_path + when db_exists $ F.removeFile db_path++ -- Initialize the SQlite Db+ c' <- liftIO $ do + c <- open . P.encodeString $ db_path + createTable c+ return c++ -- Populate the SQlite Db + liftIO $ execute_ c' "BEGIN;"+ artifacts <- lift $ toArtifacts (pkg conf) (interfaceFile conf) + lift $ mapM_ (fromArtifact (pkg conf) c') artifacts+ liftIO $ execute_ c' "COMMIT;"+ liftIO . close $ c'+ lift . indentM 2 $ msg "finished populating sqlite database.."+ lift $ msg "\n"