----------------------------------------------------------------------------- -- | -- Module : Distribution.Client.Index -- Maintainer : cabal-devel@haskell.org -- Portability : portable -- -- Querying and modifying local build tree references in the package index. ----------------------------------------------------------------------------- module Distribution.Client.Index ( index, createEmpty, addBuildTreeRefs, removeBuildTreeRefs, listBuildTreeRefs, defaultIndexFileName ) where import qualified Distribution.Client.Tar as Tar import Distribution.Client.IndexUtils ( getSourcePackages ) import Distribution.Client.PackageIndex ( allPackages ) import Distribution.Client.Setup ( IndexFlags(..) ) import Distribution.Client.Types ( Repo(..), LocalRepo(..) , SourcePackageDb(..) , SourcePackage(..), PackageLocation(..) ) import Distribution.Client.Utils ( byteStringToFilePath, filePathToByteString , makeAbsoluteToCwd ) import Distribution.Simple.Setup ( fromFlagOrDefault ) import Distribution.Simple.Utils ( die, debug, notice, findPackageDesc ) import Distribution.Verbosity ( Verbosity ) import qualified Data.ByteString.Lazy as BS import Control.Exception ( evaluate ) import Control.Monad ( liftM, when, unless ) import Data.List ( (\\), nub ) import Data.Maybe ( catMaybes ) import System.Directory ( canonicalizePath, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, renameFile ) import System.FilePath ( (), (<.>), takeDirectory, takeExtension ) import System.IO ( IOMode(..), SeekMode(..) , hSeek, withBinaryFile ) -- | A reference to a local build tree. newtype BuildTreeRef = BuildTreeRef { buildTreePath :: FilePath } defaultIndexFileName :: FilePath defaultIndexFileName = "00-index.tar" -- | Entry point for the 'cabal index' command. index :: Verbosity -> IndexFlags -> FilePath -> IO () index verbosity indexFlags path' = do let runInit = fromFlagOrDefault False (indexInit indexFlags) let refsToAdd = indexLinkSource indexFlags let runLinkSource = not . null $ refsToAdd let refsToRemove = indexRemoveSource indexFlags let runRemoveSource = not . null $ refsToRemove let runList = fromFlagOrDefault False (indexList indexFlags) unless (or [runInit, runLinkSource, runRemoveSource, runList]) $ do die "no arguments passed to the 'index' command" path <- validateIndexPath path' when runInit $ do createEmpty verbosity path when runLinkSource $ do addBuildTreeRefs verbosity path refsToAdd when runRemoveSource $ do removeBuildTreeRefs verbosity path refsToRemove when runList $ do refs <- listBuildTreeRefs verbosity path mapM_ putStrLn refs -- | Given a path, ensure that it refers to a local build tree. buildTreeRefFromPath :: FilePath -> IO (Maybe BuildTreeRef) buildTreeRefFromPath dir = do dirExists <- doesDirectoryExist dir when (not dirExists) $ do die $ "directory '" ++ dir ++ "' does not exist" _ <- findPackageDesc dir return . Just $ BuildTreeRef { buildTreePath = dir } -- | Given a tar archive entry, try to parse it as a local build tree reference. readBuildTreePath :: Tar.Entry -> Maybe FilePath readBuildTreePath entry = case Tar.entryContent entry of (Tar.OtherEntryType typeCode bs size) | (typeCode == Tar.buildTreeRefTypeCode) && (size == BS.length bs) -> Just $ byteStringToFilePath bs | otherwise -> Nothing _ -> Nothing -- | Given a sequence of tar archive entries, extract all references to local -- build trees. readBuildTreePaths :: Tar.Entries -> [FilePath] readBuildTreePaths = catMaybes . Tar.foldrEntries (\e r -> (readBuildTreePath e):r) [] error -- | Given a path to a tar archive, extract all references to local build trees. readBuildTreePathsFromFile :: FilePath -> IO [FilePath] readBuildTreePathsFromFile = liftM (readBuildTreePaths . Tar.read) . BS.readFile -- | Given a local build tree, serialise it to a tar archive entry. writeBuildTreeRef :: BuildTreeRef -> Tar.Entry writeBuildTreeRef lbt = Tar.simpleEntry tarPath content where bs = filePathToByteString path path = buildTreePath lbt -- fromRight can't fail because the path is shorter than 255 characters. tarPath = fromRight $ Tar.toTarPath True tarPath' -- Provide a filename for tools that treat custom entries as ordinary files. tarPath' = "local-build-tree-reference" content = Tar.OtherEntryType Tar.buildTreeRefTypeCode bs (BS.length bs) -- TODO: Move this to D.C.Utils? fromRight (Left err) = error err fromRight (Right a) = a -- | Check that the provided path is either an existing directory, or a tar -- archive in an existing directory. validateIndexPath :: FilePath -> IO FilePath validateIndexPath path' = do path <- makeAbsoluteToCwd path' if (== ".tar") . takeExtension $ path then return path else do dirExists <- doesDirectoryExist path unless dirExists $ do die $ "directory does not exist: '" ++ path ++ "'" return $ path defaultIndexFileName -- | Create an empty index file. createEmpty :: Verbosity -> FilePath -> IO () createEmpty verbosity path = do indexExists <- doesFileExist path if indexExists then debug verbosity $ "Package index already exists: " ++ path else do debug verbosity $ "Creating the index file '" ++ path ++ "'" createDirectoryIfMissing True (takeDirectory path) -- Equivalent to 'tar cvf empty.tar --files-from /dev/null'. let zeros = BS.replicate (512*20) 0 BS.writeFile path zeros -- | Add given local build tree references to the index. addBuildTreeRefs :: Verbosity -> FilePath -> [FilePath] -> IO () addBuildTreeRefs _ _ [] = error "Distribution.Client.Index.addBuildTreeRefs: unexpected" addBuildTreeRefs verbosity path l' = do checkIndexExists path l <- liftM nub . mapM canonicalizePath $ l' treesInIndex <- readBuildTreePathsFromFile path -- Add only those paths that aren't already in the index. treesToAdd <- mapM buildTreeRefFromPath (l \\ treesInIndex) let entries = map writeBuildTreeRef (catMaybes treesToAdd) when (not . null $ entries) $ do offset <- fmap (Tar.foldrEntries (\e acc -> Tar.entrySizeInBytes e + acc) 0 error . Tar.read) $ BS.readFile path _ <- evaluate offset debug verbosity $ "Writing at offset: " ++ show offset withBinaryFile path ReadWriteMode $ \h -> do hSeek h AbsoluteSeek (fromIntegral offset) BS.hPut h (Tar.write entries) debug verbosity $ "Successfully appended to '" ++ path ++ "'" -- | Remove given local build tree references from the index. removeBuildTreeRefs :: Verbosity -> FilePath -> [FilePath] -> IO () removeBuildTreeRefs _ _ [] = error "Distribution.Client.Index.removeBuildTreeRefs: unexpected" removeBuildTreeRefs verbosity path l' = do checkIndexExists path l <- mapM canonicalizePath l' let tmpFile = path <.> "tmp" -- Performance note: on my system, it takes 'index --remove-source' -- approx. 3,5s to filter a 65M file. Real-life indices are expected to be -- much smaller. BS.writeFile tmpFile . Tar.writeEntries . Tar.filterEntries (p l) . Tar.read =<< BS.readFile path -- This invalidates the cache, so we don't have to update it explicitly. renameFile tmpFile path debug verbosity $ "Successfully renamed '" ++ tmpFile ++ "' to '" ++ path ++ "'" where p l entry = case readBuildTreePath entry of Nothing -> True (Just pth) -> not $ any (== pth) l -- | List the local build trees that are referred to from the index. listBuildTreeRefs :: Verbosity -> FilePath -> IO [FilePath] listBuildTreeRefs verbosity path = do checkIndexExists path let repo = Repo { repoKind = Right LocalRepo , repoLocalDir = takeDirectory path } pkgIndex <- fmap packageIndex . getSourcePackages verbosity $ [repo] let buildTreeRefs = [ pkgPath | (LocalUnpackedPackage pkgPath) <- map packageSource . allPackages $ pkgIndex ] when (null buildTreeRefs) $ do notice verbosity $ "Index file '" ++ path ++ "' has no references to local build trees." return buildTreeRefs -- | Check that the package index file exists and exit with error if it does not. checkIndexExists :: FilePath -> IO () checkIndexExists path = do indexExists <- doesFileExist path when (not indexExists) $ do die $ "index does not exist: '" ++ path ++ "'"