cabal-dev-0.7: src/Distribution/Dev/AddSource.hs
{- Copyright (c) 2010 Galois, Inc. -}
{-|
add-source command
Puts local source packages into a repository readable by cabal-install
-}
{-# LANGUAGE CPP #-}
module Distribution.Dev.AddSource
( actions
)
where
#ifndef MIN_VERSION_Cabal
#define MIN_VERSION_Cabal(a,b,c) 1
#endif
import Control.Applicative ( (<$>), (<*>) )
import Control.Arrow ( right )
import Control.Exception ( bracket )
import Control.Monad ( guard, (<=<), forM_ )
import Control.Monad.Error ( runErrorT, throwError )
import Control.Monad.Trans ( liftIO )
import Data.List ( isPrefixOf )
import Distribution.PackageDescription ( PackageDescription(buildType), BuildType(Custom)
, packageDescription, package )
import Distribution.Package ( PackageIdentifier(..) )
#if MIN_VERSION_Cabal(1,6,0)
import Distribution.PackageDescription.Parse ( ParseResult(ParseOk), readPackageDescription
, parsePackageDescription )
import Distribution.Package ( PackageName(..) )
#elif MIN_VERSION_Cabal(1,4,0)
import Distribution.PackageDescription ( ParseResult(ParseOk), readPackageDescription
, parsePackageDescription )
#else
#error Unsupported Cabal version
#endif
import Distribution.Text ( simpleParse, display )
import Network.HTTP ( mkRequest, RequestMethod(GET)
, rspBody, simpleHTTP
)
import Network.URI ( parseURI, uriScheme, URI
, uriPath )
import System.Cmd ( rawSystem )
import System.Console.GetOpt ( OptDescr(..) )
import System.Directory ( getDirectoryContents
, renameFile, copyFile
, getCurrentDirectory
, setCurrentDirectory
, createDirectoryIfMissing
, getTemporaryDirectory
)
import System.Exit ( ExitCode(..) )
import System.FilePath ( takeExtension, takeBaseName
, splitDirectories, (<.>), (</>)
, splitExtension
)
import System.IO ( withBinaryFile, IOMode(..), hClose
, openTempFile, hFlush, hSetBinaryMode
)
import System.IO.Error ( isDoesNotExistError )
import qualified Codec.Archive.Tar as T
import qualified Codec.Archive.Tar.Entry as T
import qualified Codec.Compression.GZip as Z
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8 ( unpack )
import qualified Distribution.Verbosity as V
import Distribution.Dev.InvokeCabal ( cabalArgs )
import Distribution.Dev.Command ( CommandActions(..), CommandResult(..) )
import Distribution.Dev.Flags ( Config, getVerbosity )
import Distribution.Dev.Sandbox ( resolveSandbox, localRepoPath
, Sandbox, indexTar, indexTarBase
)
import Distribution.Simple.Utils ( debug, notice )
actions :: CommandActions
actions = CommandActions
{ cmdDesc = "Add packages to a local cabal install repository"
, cmdRun = \cfg _ -> addSources cfg
, cmdOpts = [] :: [OptDescr ()]
, cmdPassFlags = False
}
addSources :: Config -> [String] -> IO CommandResult
addSources _ [] = return $ CommandError "No package locations supplied"
addSources flgs fns = do
sandbox <- resolveSandbox flgs
let v = getVerbosity flgs
debug v $ "Making a cabal repo in " ++ localRepoPath sandbox ++
" out of " ++ show fns
eArgs <- cabalArgs flgs
case eArgs of
Left err -> return $ CommandError $ "Error getting cabal arguments: " ++ err
Right args ->
do
results <- mapM (processLocalSource v) fns
let errs = [e | Left e <- results]
srcs = [s | Right s <- results]
if not $ null errs
then return $ CommandError $ unlines $
"Errors finding cabal files:":map (showString " ") errs
else do
let (sources, newEntries) = unzip srcs
res <- readExistingIndex sandbox
case res of
Left err -> return $
CommandError $ "Error reading existing index: " ++ err
Right existingIndex ->
do let newIndex = mergeIndices existingIndex newEntries
-- Now we have the new index ready and have
-- sanity-checked all of the package locations
-- to be sure that they contain a cabal package
-- (or at least a .cabal file)
--
-- Now to install the tarballs for the
-- directories:
forM_ sources $ \(src, pkgId, pkgDesc) ->
installTarball flgs sandbox src pkgId pkgDesc args
-- and now that the tarballs are in place,
-- write out the updated index
writeIndex sandbox newIndex
return CommandOk
-- |Atomically write an index tarball in the supplied directory
writeIndex :: Sandbox a -- ^The local repository path
-> [T.Entry] -- ^The index entries
-> IO ()
writeIndex sandbox ents =
do newIndexName <- withTmpIndex $ \(fn, h) -> do
hSetBinaryMode h True
L.hPut h (T.write ents)
hFlush h
return fn
renameFile newIndexName $ indexTar sandbox
where
pth = localRepoPath sandbox
withTmpIndex = bracket (openTempFile pth indexTarBase) (hClose . snd)
-- |Merge two lists of tar entries, filtering out the entries from the
-- original list that will be duplicated by the second list of
-- entries
mergeIndices :: [T.Entry] -> [T.Entry] -> [T.Entry]
mergeIndices old new = filter notInNew old ++ new
where
newPaths = map T.entryTarPath new
notInNew e = not $ T.entryTarPath e `elem` newPaths
-- |Create a tar entry for the package identifier and cabal file contents
toIndexEntry :: PackageIdentifier -> L.ByteString -> Either String T.Entry
toIndexEntry pkgId c = right toEnt $ T.toTarPath False (indexName pkgId)
where
toEnt p = T.fileEntry p c
-- |Read an existing index tarball from the local repository, if one
-- exists. If the file does not exist, behave as if the index has no
-- entries.
readExistingIndex :: Sandbox a -> IO (Either String [T.Entry])
readExistingIndex sandbox =
readIndexFile `catch` \e ->
if isDoesNotExistError e
then return $ Right []
else ioError e
where
readIndexFile = withBinaryFile (indexTar sandbox) ReadMode $
(forceEntries . T.read <=< L.hGetContents)
forceEntries es =
let step _ l@(Left _) = l
step x (Right xs) = Right (x:xs)
es' = T.foldEntries step (Right []) Left es
in either (const 0) length es' `seq` return es'
-- |What kind of package source is this?
data LocalSource = DirPkg FilePath | TarPkg FilePath deriving Show
-- |Determine if this filename looks like a tarball (otherwise, it
-- assumes that it's a directory and treats it as such)
classifyLocalSource :: String -> Either URI LocalSource
classifyLocalSource fn =
case parseURI fn of
Just u | isHttpUri u && isTarGzUri u -> Left u
_ | isTarball fn -> Right $ TarPkg fn
| otherwise -> Right $ DirPkg fn
where
isHttpUri = (`elem` ["http:", "https:"]) . uriScheme
isTarGzUri = (reverse ".tar.gz" `isPrefixOf`) . reverse . uriPath
-- |Put the tarball for this package in the local repository
installTarball :: Config
-> Sandbox a -- ^Location of the local repository
-> LocalSource -- ^What kind of package source
-> PackageIdentifier
-> PackageDescription -- ^Package description
-> [String] -- ^Cabal args, computed at the top level
-> IO (Either String ())
installTarball flgs sandbox src pkgId pkgDesc args =
do createDirectoryIfMissing True $ localRepoPath sandbox </> repoDir pkgId
case src of
TarPkg fn -> do copyFile fn dest
return $ Right ()
DirPkg fn -> do
res <- makeSDist fn
case res of
Left err -> return $ Left err
Right tarFn -> do
renameFile tarFn dest
return $ Right ()
where
dest = localRepoPath sandbox </> tarballName pkgId
makeSDist fn = do
debug (getVerbosity flgs) $ "Running cabal sdist in " ++ fn
bracket getCurrentDirectory setCurrentDirectory $ \_ -> do
setCurrentDirectory fn
-- If the build-type is custom, run 'configure'
-- and invoke the generated setup program.
-- Otherwise, just run plain sdist. We do this to
-- work around
-- http://hackage.haskell.org/trac/hackage/ticket/410
cabalRes <- case buildType pkgDesc of
Just Custom -> do
-- We run configure without args
-- first to compile the Setup
-- program. We do this to work
-- around a different bug in Cabal
-- (http://hackage.haskell.org/trac/hackage/ticket/731)
rawSystem "cabal" ["configure"]
rawSystem "dist/setup/setup" ["sdist"]
_ -> rawSystem "cabal" ["sdist"]
case cabalRes of
ExitSuccess ->
do here <- getCurrentDirectory
return $ Right $ here </> "dist" </>
display pkgId <.> "tar" <.> "gz"
ExitFailure code ->
return $ Left $
"cabal sdist failed with " ++ show code
downloadTarball :: URI -> IO (Either String FilePath)
downloadTarball u = do
tmpLoc <- getTemporaryDirectory
bracket (openTempFile tmpLoc "package-.tar.gz") (hClose . snd) $ \(fn, h) ->
do httpRes <- simpleHTTP $ mkRequest GET u
case httpRes of
Left err -> return $ Left $ show err
Right resp ->
do L.hPut h $ rspBody resp
return $ Right fn
-- |Extract the index information from the supplied path, either as a
-- tarball or as a local package directory
processLocalSource :: V.Verbosity -> FilePath
-> IO (Either String ((LocalSource, PackageIdentifier, PackageDescription), T.Entry))
processLocalSource v fn =
runErrorT $ do
let cls = classifyLocalSource fn
src <- case cls of
Left u -> do
liftIO $ notice v $ "Downloading " ++ show u
TarPkg `fmap` eitherErrorIO (downloadTarball u)
Right s -> return s
(pkgId, c, pkgDesc) <- eitherErrorIO $
case src of
TarPkg x -> processTarball x
DirPkg x -> processDirectory v x
ent <- eitherError $ toIndexEntry pkgId c
return ((src, pkgId, pkgDesc), ent)
where
eitherError = either throwError return
eitherErrorIO = eitherError <=< liftIO
-- |Extract the index information from a tarball
processTarball :: FilePath
-> IO (Either String (PackageIdentifier, L.ByteString, PackageDescription))
processTarball fn =
withBinaryFile fn ReadMode $ \h ->
do ents <- T.read . Z.decompress <$> L.hGetContents h
case extractCabalFile ents of
Nothing -> return $ Left "No cabal file found"
-- Force reading the cabal file before we exit withFile
Just res@(_, bs, _) -> do
forceBS bs
return (Right res)
#if MIN_VERSION_Cabal(1,6,0)
mkPackageName :: String -> PackageName
mkPackageName = PackageName
displayPackageName :: PackageName -> String
displayPackageName = display
#elif MIN_VERSION_Cabal(1,4,0)
mkPackageName :: String -> String
mkPackageName = id
displayPackageName :: String -> String
displayPackageName = id
#else
#error Unsupported cabal version
#endif
-- |Extract the index information from a directory containing a cabal
-- file
processDirectory :: V.Verbosity -> FilePath
-> IO (Either String (PackageIdentifier, L.ByteString, PackageDescription))
processDirectory v d = go `catch` \e ->
if expected e
then return $ Left $ show e
else ioError e
where
expected e = any ($ e) [ isDoesNotExistError
]
go = do
fns <- getDirectoryContents d
case filter isCabalFile fns of
[c] -> processCabalFile c
[] -> return $ Left "No cabal file found"
_ -> return $ Left "More than one cabal file present"
processCabalFile c = do
let fn = d </> c
pkgDesc <- packageDescription <$>
readPackageDescription V.normal fn
let pkgId = package pkgDesc
if mkPackageName (takeBaseName c) == pkgName pkgId
then do
notice v $ "Building source dist at " ++ d ++ " for " ++ display pkgId
cabalFile <- withBinaryFile fn ReadMode $
forcedBS <=< L.hGetContents
return $ Right (pkgId, cabalFile, pkgDesc)
else
return $ Left $ unlines $
[ "Package name does not match cabal file name:"
, " filename = " ++ fn
, " package name 1 = " ++ show (mkPackageName (takeBaseName c))
, " package name 2 = " ++ show (pkgName pkgId)
]
-- |Force a lazy ByteString to be read
forceBS :: L.ByteString -> IO ()
forceBS bs = L.length bs `seq` return ()
-- |Force a lazy ByteString to be read, and pass it on to the next action
forcedBS :: L.ByteString -> IO L.ByteString
forcedBS bs = forceBS bs >> return bs
-- |Extract a cabal file from a package tarball
extractCabalFile :: T.Entries -> Maybe (PackageIdentifier, L.ByteString, PackageDescription)
extractCabalFile = T.foldEntries step Nothing (const Nothing)
where
step ent Nothing = (,,) <$> entPackageId ent <*> entBytes ent <*> (parseDesc $ entBytes ent)
step _ ans = ans
parseDesc bs = do
str <- L8.unpack <$> bs
let result = parsePackageDescription str
case result of
ParseOk _ pkg -> return $ packageDescription pkg
_ -> Nothing
entPackageId ent =
case splitDirectories $ T.entryPath ent of
[d, f] ->
do i <- simpleParse d
let cabalName = mkPackageName $ takeBaseName f
guard $ isCabalFile f && cabalName == pkgName i
return i
_ -> Nothing
entBytes ent = case T.entryContent ent of
T.NormalFile x _ -> return x
_ -> Nothing
-- | Does this filename look like a cabal file?
isCabalFile :: FilePath -> Bool
isCabalFile = (== ".cabal") . takeExtension
-- |Does this filename look like a gzipped tarball?
isTarball :: FilePath -> Bool
isTarball fn = (ext2, ext1) == (".tar", ".gz")
where
(fn1, ext1) = splitExtension fn
(_, ext2) = splitExtension fn1
-- |The path to the .cabal file in the 00-index.tar file
indexName :: PackageIdentifier -> FilePath
indexName pkgId = repoDir pkgId </>
(displayPackageName (pkgName pkgId) <.> "cabal")
-- |The path to the tarball in the local repository
tarballName :: PackageIdentifier -> FilePath
tarballName pkgId = repoDir pkgId </> (display pkgId <.> "tar" <.> "gz")
repoDir :: PackageIdentifier -> FilePath
repoDir pkgId = displayPackageName (pkgName pkgId) </>
display (pkgVersion pkgId)