packages feed

cabal-file (empty) → 0.1.0

raw patch · 8 files changed

+681/−0 lines, 8 filesdep +Cabaldep +basedep +bytestring

Dependencies added: Cabal, base, bytestring, cabal-file, directory, extra, filepath, hackage-security, optparse-applicative, simple-cabal, simple-cmd, simple-cmd-args, time

Files

+ ChangeLog.md view
@@ -0,0 +1,8 @@+# Revision history for cabal-file++## 0.1.0 -- 2020-05-24++- initial release+- cblfile commands: diff (second), list, latest, files, get, date, show,+                    metadata, preferred, depends (oldest code)+- Hackage.Index library module
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2020, Jens Petersen+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,70 @@+# cabal-file++cabal-file is a library on top of the hackage-security library for accessing+cabal files and versions, and also a small tool for getting and comparing+.cabal files.++Since it accesses package and .cabal file data directly from+the local Hackage repo cache index tar archive under ~/.cabal/ it is quite fast.++## Usage+List versions of a package:+```+$ cblfile list pandoc+0.4+:+:+2.9.2.1+```++Diff .cabal files of package versions+```+$ cblfile diff pandoc 2.8 2.9.2.1+--- pandoc-2.8.cabal    2020-05-24 19:47:58.487921450 +0800++++ pandoc-2.9.2.1.cabal        2020-05-24 19:47:58.487921450 +0800+@@ -1,10 +1,10 @@+ name:            pandoc+-version:         2.8++version:         2.9.2.1+ cabal-version:   2.0+:+:+```++Latest version of a package:+```+$ cblfile latest extra+1.7.1+```++Timestamp for the latest .cabal file (or for a specific version):+```+$ cblfile date dhall+2020-05-19 02:18:01 UTC+```++Save a .cabal file (latest or specific version)+```+$ cblfile get purescript+$ ls purescript.cabal+purescript.cabal+```++Check package source metadata:+```+$ cblfile metadata tls-1.5.4+Size: 150171+SHA256 Hash "ce42bfa581a50f62776581da4b86408ebb1a51bc6cb2f95ad0a2ac7caa19a437"+```++## Depends output+`cblfile depends` outputs quite verbose package dependency lists.++## Library+For library documentation see the [Hackage.Index](https://hackage.haskell.org/package/cabal-file/docs/Hackage-Index.html) documentation.++For example usage see `app/Cmds.hs`.++## Related+If you want full diffs of Hackage sources you can try out cabal-diff from+[cabal-extras](https://github.com/phadej/cabal-extras).
+ app/Cmds.hs view
@@ -0,0 +1,86 @@+module Cmds (+  diffCmd,+  saveCabal,+  showCabal,+  listPkg,+  latestPkg,+  listFiles,+  preferCmd,+  showMetadata,+  dateCabal+  ) where++import Control.Monad+import Control.Monad.Extra+import qualified Data.ByteString.Lazy.Char8 as BL+import Data.List+import Distribution.Version (Version)+import SimpleCabal+import SimpleCmd+import System.Directory+import System.FilePath+import System.IO.Extra (withTempDir)++import Hackage.Index++-- FIXME structural diff of PackageDescription+-- FIXME revisions?+diffCmd :: String -> Version -> Version -> IO ()+diffCmd pkg v1 v2 =+  withTempDir $ \ tmpdir -> do+    setCurrentDirectory tmpdir+    let pkgid1 = PackageIdentifier (mkPackageName pkg) v1+        pkgid2 = PackageIdentifier (mkPackageName pkg) v2+    saveCabals pkgid1 pkgid2+    void $ cmdBool "diff" ["-u", showPkgId pkgid1 <.> "cabal", showPkgId pkgid2 <.> "cabal"]++--  pkgdesc <- finalPackageDescription [] cabal+  where+    saveCabals :: PackageIdentifier -> PackageIdentifier -> IO ()+    saveCabals pkgid1 pkgid2 = do+      (bs1,bs2) <- getCabals pkgid1 pkgid2+      BL.writeFile (showPkgId pkgid1 <.> "cabal") bs1+      BL.writeFile (showPkgId pkgid2 <.> "cabal") bs2++listPkg :: Maybe PackageName -> IO ()+listPkg (Just pkgname) = do+  versions <- packageVersions pkgname+  mapM_ (putStrLn . showVersion) versions+listPkg Nothing = fmap sort listPackages >>= mapM_ putStrLn++saveCabal :: PackageIdentifier -> IO ()+saveCabal pkgid = do+  pkgver <- packageIdOrLatest pkgid+  getCabal pkgver >>= BL.writeFile (showPkgId pkgid <.> "cabal")++showCabal :: PackageIdentifier -> IO ()+showCabal pkgid = do+  pkgver <- packageIdOrLatest pkgid+  getCabal pkgver >>= BL.putStrLn++showMetadata :: PackageIdentifier -> IO ()+showMetadata pkgid = do+  pkgver <- packageIdOrLatest pkgid+  finfo <- getFileInfo pkgver+  let bytes = show . fileLength . fileInfoLength+  putStrLn $ "Size: " ++ bytes finfo+  whenJust (fileInfoSHA256 finfo) $ \ hash ->+    putStrLn $ "SHA256 " ++ show hash++latestPkg :: PackageName -> IO ()+latestPkg pkgname =+  whenJustM (latestVersion pkgname) (putStrLn . showVersion)++listFiles :: IO ()+listFiles = do+  files <- indexFiles+  mapM_ putStrLn files++preferCmd :: PackageName -> IO ()+preferCmd pkgname =+  whenJustM (preferredVersions pkgname) BL.putStrLn++dateCabal :: PackageIdentifier -> IO ()+dateCabal pkgid = do+  pkgver <- packageIdOrLatest pkgid+  whenJustM (getTimestamp pkgver) print
+ app/Depends.hs view
@@ -0,0 +1,139 @@+module Depends (+  Deps(..),+  Details(..),+  dependsCmd)+where++import Control.Monad (unless, when)+import Data.List (nub, (\\))+import Data.Maybe+import System.Directory+import System.FilePath++import Distribution.PackageDescription (Library(..), exeName, setupDepends)+import Distribution.Pretty+import Distribution.Simple.BuildToolDepends (getAllToolDependencies)+import Distribution.Text (simpleParse)+import Distribution.Types.ExeDependency++import SimpleCabal (allBuildInfo, buildDepends, buildDependencies, buildTools,+                    exeDepName, extraLibs, findCabalFile,+                    finalPackageDescription,+                    mkPackageName, package, PackageDescription(..), PackageName,+                    pkgcfgDepName, pkgconfigDepends, pkgName,+                    setupBuildInfo, setupDependencies)++import Hackage.Index (withCabalFile)++data Deps = Build | Setup | Tool | Legacy | CLib | PkgConfig | NotBuild+  deriving (Eq, Show)++data Details = Normal | Unique | Detail+  deriving Eq+++dependsCmd :: Bool -> Details -> Maybe Deps -> Maybe FilePath -> IO ()+dependsCmd quiet details opt mfile =+  case mfile of+    Nothing -> findCabalFile >>= displayDepends+    Just path -> do+      dir <- doesDirectoryExist path+      if dir+        then setCurrentDirectory path >> findCabalFile >>= displayDepends+        else+        if "cabal" `isExtensionOf` path+        then do+          isfile <- doesFileExist path+          if isfile then displayDepends path+            else error $ path ++ " does not exist"+        else+          case simpleParse path of+            Nothing -> error $ path ++ " not a package identifier"+            Just pkgid -> withCabalFile pkgid displayDepends+  where+    displayDepends :: FilePath -> IO ()+    displayDepends cabal = do+      pkgdesc <- finalPackageDescription [] cabal+      work (details == Detail) (details == Unique) (pkgName (package pkgdesc)) pkgdesc+      where+        work :: Bool -> Bool -> PackageName -> PackageDescription -> IO ()+        work detail unique self pkgdesc = do+          printDeps Build+          printDeps Setup+          printDeps Tool+          printDeps Legacy+          printDeps CLib+          printDeps PkgConfig+          printLib+          printExe+            where+              printDeps :: Deps -> IO ()+              printDeps kind =+                unless (opt == Just NotBuild && kind == Build) $ do+                  let deps = nub $ lens kind+                  unless (null deps) $+                    when (isNothing opt || opt `elem` [Just kind, Just NotBuild]) $ do+                      when (not quiet && (opt /= Just kind || opt == Just NotBuild)) $+                        title $ show kind ++ " depends"+                      mapM_ putStrLn deps++              lens :: Deps -> [String]+              lens Build =+                if detail+                then map prettyShow $ buildDepends pkgdesc+                else map prettyShow $ buildDependencies pkgdesc \\ [self | unique]+              lens Setup =+                if detail+                then map prettyShow $ maybe [] setupDepends (setupBuildInfo pkgdesc)+                else map prettyShow $ setupDependencies pkgdesc \\ (mkPackageName "Cabal" : if unique then buildDependencies pkgdesc else [])+              lens Tool =+                let bts = concatMap (getAllToolDependencies pkgdesc) $+                          allBuildInfo pkgdesc+                in if detail then map prettyShow bts+                   else map (prettyShow . exeDepPkgName) bts \\ ([prettyShow self | unique] ++ ["hsc2hs" | unique])+              lens Legacy =+                let legacy = concatMap buildTools $ allBuildInfo pkgdesc+                in (if detail then map prettyShow legacy+                    else map exeDepName legacy) \\ if unique then "hsc2hs" : prettyShow self : lens Tool else []+              lens CLib = (concatMap extraLibs .  allBuildInfo) pkgdesc+              lens PkgConfig =+                let ds = (concatMap pkgconfigDepends . allBuildInfo) pkgdesc+                in (if detail then map prettyShow+                    else map pkgcfgDepName) ds+              lens NotBuild = error "No lens for NotBuild!"++              printLib :: IO ()+              printLib =+                case library pkgdesc of+                  Nothing -> return ()+                  Just lib -> do+                    title "Library"+                    when detail $ do+                      subtitle "Exposed Modules"+                      mapM_ prettyPut $ exposedModules lib+                      let export = reexportedModules lib+                      unless (null export) $ do+                        subtitle "Re-exported Modules"+                        mapM_ prettyPut export++              printExe :: IO ()+              printExe = do+                let exes = executables pkgdesc+                unless (null exes) $ do+                  title $ "Executable" ++ ['s' | length exes > 1]+                  when detail $+                    mapM_ (prettyPut . exeName) exes++title :: String -> IO ()+title ts =+  putStrLn $ "# " ++ ts++subtitle :: String -> IO ()+subtitle ts =+  putStrLn $ "- " ++ ts++prettyPut :: Pretty a => a -> IO ()+prettyPut = putStrLn . prettyShow++exeDepPkgName :: ExeDependency -> PackageName+exeDepPkgName (ExeDependency n _ _) = n
+ app/Main.hs view
@@ -0,0 +1,70 @@+{-# LANGUAGE CPP #-}++import Data.Version.Extra (readVersion)+import Distribution.Package+import Distribution.Text (simpleParse)+import Distribution.Version+#if MIN_VERSION_simple_cmd_args(0,1,7)+#else+import Options.Applicative+#endif+import SimpleCmdArgs++import Cmds+import Depends++main :: IO ()+main =+  simpleCmdArgs Nothing "Cabal file tool"+    "cabal-file can access and compare .cabal files from the cache" $+    subcommands+    [ Subcommand "diff" "Diff .cabal files of package versions" $+      diffCmd <$> strArg "PKG" <*> versionArg <*> versionArg+    , Subcommand "list" "List package versions" $+      listPkg <$> optional pkgArg+    , Subcommand "latest" "Latest package version" $+      latestPkg <$> pkgArg+    , Subcommand "files" "List all index files" $+      pure listFiles+    , Subcommand "get" "Get .cabal file for package version" $+      saveCabal <$> pkgIdArg+    , Subcommand "date" "Timestamp for package version (revision)" $+      dateCabal <$> pkgIdArg+    , Subcommand "show" "Show .cabal file for package version" $+      showCabal <$> pkgIdArg+    , Subcommand "metadata" "Show metadata for package version" $+      showMetadata <$> pkgIdArg+    , Subcommand "preferred" "Show preferred versions for package" $+      preferCmd <$> pkgArg+    , Subcommand "depends" "Print package dependencies" $+      dependsCmd <$> switchWith 'q' "quiet" "No depends section headers"+      <*> detailOpt+      <*> optional depsOpt+      <*> optional (strArg "DIRorCABAL")+    ]+  where+    versionArg :: Parser Version+    versionArg = argumentWith versionM "VERSION"++    versionM = maybeReader (Just . mkVersion' . readVersion)++    pkgIdArg :: Parser PackageIdentifier+    pkgIdArg = argumentWith (maybeReader simpleParse) "PKG[VER]"++    pkgArg :: Parser PackageName+    pkgArg = argumentWith (maybeReader simpleParse) "PKG"++    detailOpt :: Parser Details+    detailOpt =+      flagWith' Detail 'd' "detail" "Show version ranges" <|>+      flagWith Normal Unique 'u' "unique" "Remove self and build-depends from other depends"++    depsOpt :: Parser Deps+    depsOpt =+      flagWith' Build 'b' "build" "Only build-depends" <|>+      flagWith' Setup 's' "setup" "Only setup-depends" <|>+      flagWith' Tool 't' "tool" "Only tool-build-depends" <|>+      flagWith' Legacy 'l' "legacy" "Only build-tools" <|>+      flagWith' CLib 'c' "clibs" "Only extra-libraries" <|>+      flagWith' PkgConfig 'p' "pkgconfig" "Only extra-libraries" <|>+      flagWith' NotBuild 'B' "not-build" "Hide build-depends"
+ cabal-file.cabal view
@@ -0,0 +1,83 @@+name:                cabal-file+version:             0.1.0+synopsis:            Cabal file access+description:+        cabal-file is a small library on top of the 'hackage-security' library+        for accessing the local Hackage repo index cache, and a command-line tool+        'cblfile' which can diff .cabal versions, list package versions,+        save a cabal file, and show metadata from the index. It uses+        simple-cabal to display package dependency lists.+license:             BSD3+license-file:        LICENSE+author:              Jens Petersen+maintainer:          juhpetersen@gmail.com+copyright:           2019-2020  Jens Petersen+category:            Distribution+build-type:          Simple+extra-doc-files:     README.md+                   , ChangeLog.md+cabal-version:       1.18++source-repository head+  type:                git+  location:            https://github.com/juhp/cabal-file.git++executable cblfile+  main-is:             Main.hs+  other-modules:       Cmds+                       Depends+  hs-source-dirs:      app+  build-depends:       base < 5+                     , bytestring+                     , Cabal > 2.2+                     , cabal-file+                     , directory+                     , extra+                     , filepath+                     , optparse-applicative+                     , simple-cabal > 0.1+                     , simple-cmd+                     , simple-cmd-args >= 0.1.2+  default-language:    Haskell2010+  ghc-options:         -Wall+  if impl(ghc >= 8.0)+    ghc-options:       -Wcompat+                       -Widentities+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wredundant-constraints+  if impl(ghc >= 8.2)+    ghc-options:       -fhide-source-paths+  if impl(ghc >= 8.4)+    ghc-options:       -Wmissing-export-lists+                       -Wpartial-fields++library+  build-depends:       base < 5+                     , bytestring+                     , Cabal > 2.2+                     , directory+                     , extra+                     , filepath+                     , hackage-security+                     , optparse-applicative+                     , simple-cabal > 0.1+                     , simple-cmd+                     , time+  exposed-modules:     Hackage.Index+  hs-source-dirs:      src++  ghc-options:         -Wall+  if impl(ghc >= 8.0)+    ghc-options:       -Wcompat+                       -Widentities+                       -Wincomplete-uni-patterns+                       -Wincomplete-record-updates+                       -Wredundant-constraints+  if impl(ghc >= 8.2)+    ghc-options:       -fhide-source-paths+  if impl(ghc >= 8.4)+    ghc-options:       -Wmissing-export-lists+                       -Wpartial-fields++  default-language:    Haskell2010
+ src/Hackage/Index.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE GADTs #-}++module Hackage.Index (+  getCabal,+  getCabals,+  withCabalFile,+  listPackages,+  packageVersions,+  latestVersion,+  preferredVersions,+  getTimestamp,+  indexFiles,+  getPackageDescription,+  getPackageDescription',+  packageIdOrLatest,+  getFileInfo,+  -- * Re-exports from hackage-security+  FileInfo(..),+  FileLength(..),+  fileInfoSHA256+  ) where++import qualified Data.ByteString.Lazy.Char8 as BL+import Data.List+import Data.Maybe+import Data.Time.Clock+import Data.Time.Clock.POSIX+import Data.Version.Extra (readVersion)+import System.Directory+import System.FilePath+import System.IO.Extra (withTempDir)++import Distribution.Version+#if MIN_VERSION_Cabal(3,0,0)+#else+  hiding (showVersion)+#endif+import Hackage.Security.Client+import qualified Hackage.Security.Client.Repository.Local as Local+import qualified Hackage.Security.Util.Path as Path+--import Hackage.Security.Util.Some+import qualified Hackage.Security.Client.Repository.Cache as Cache+import Hackage.Security.Util.Pretty++import SimpleCabal++-- | Get the contents of the .cabal file for package version+getCabal  :: PackageIdentifier -> IO BL.ByteString+getCabal pkgid =+  withLocalRepo $ \rep -> uncheckClientErrors $+    withIndex rep $ \ IndexCallbacks{..} ->+    trusted <$> indexLookupCabal pkgid++-- | Pass a temporary copy of .cabal file to some action+withCabalFile :: PackageIdentifier -> (FilePath -> IO a) -> IO a+withCabalFile pkgid act =+  withTempDir $ \ tmpdir -> do+    bs <- getCabal pkgid+    let filepath = tmpdir </> showPkgId pkgid <.> "cabal"+    BL.writeFile filepath bs+    act filepath++-- | Get two .cabal files at once!+getCabals  :: PackageIdentifier -> PackageIdentifier+           -> IO (BL.ByteString, BL.ByteString)+getCabals pkgid1 pkgid2 =+  withLocalRepo $ \rep -> uncheckClientErrors $+    withIndex rep $ \ IndexCallbacks{..} -> do+    bs1 <- trusted <$> indexLookupCabal pkgid1+    bs2 <- trusted <$> indexLookupCabal pkgid2+    return (bs1,bs2)++-- | Get FileInfo metadata for package version source+getFileInfo :: PackageIdentifier -> IO FileInfo+getFileInfo pkgid =+  withLocalRepo $ \rep -> uncheckClientErrors $+      withIndex rep $ \ IndexCallbacks{..} ->+        trusted <$> indexLookupFileInfo pkgid++-- | Get and try to parse the PackageDescription of a package version+getPackageDescription :: PackageIdentifier -> IO (Maybe PackageDescription)+getPackageDescription pkgid =+#if (defined(MIN_VERSION_simple_cabal) && MIN_VERSION_simple_cabal(0,1,2))+  do+  cabal <- getCabal pkgid+  parseFinalPackageDescription [] $ BL.toStrict cabal+#else+  Just <$> withCabalFile pkgid (finalPackageDescription [])+#endif++-- | Get and parse PackageDescription of package version+--+-- Raises an error on failure.+getPackageDescription' :: PackageIdentifier -> IO PackageDescription+getPackageDescription' pkgid = do+  mfpd <- getPackageDescription pkgid+  maybe (error "Failed to parse cabal file") return mfpd++-- | Easy access to the local Hackage repo+withLocalRepo :: (Repository Local.LocalFile -> IO a) -> IO a+withLocalRepo action = do+  home <- getHomeDirectory+  localrepo <- (Path.makeAbsolute . Path.fromFilePath) (home </> ".cabal")+  localcache <- (Path.makeAbsolute . Path.fromFilePath) (home </> ".cabal/packages/hackage.haskell.org")+  Local.withRepository localrepo (cache localcache) hackageRepoLayout hackageIndexLayout logTUF action+  where+    -- FIXME could also support 00-index+    cache localcache = Cache.Cache {+        Cache.cacheRoot   = localcache+      , Cache.cacheLayout = cabalCacheLayout+        { cacheLayoutIndexTar   = Path.rootPath $ Path.fragment "01-index.tar"+        , cacheLayoutIndexIdx   = Path.rootPath $ Path.fragment "01-index.tar.idx"+        , cacheLayoutIndexTarGz = Path.rootPath $ Path.fragment "01-index.tar.gz"}+    }++    logTUF msg = putStrLn $ "# " ++ pretty msg++-- | Get all versions of a package in the index+packageVersions :: PackageName -> IO [Version]+packageVersions pkgname =+  withLocalRepo $ \rep -> uncheckClientErrors $ do+    dir <- getDirectory rep+    let pkg = unPackageName pkgname+    return $ sort . mapMaybe (extractPkgVersion pkg . second) $ directoryEntries dir+  where+    second (_,b,_) = b++    extractPkgVersion :: String -> IndexPath -> Maybe Version+    extractPkgVersion pkg path =+      if Path.takeExtension path == ".cabal" then+        let namever = (Path.toUnrootedFilePath . Path.unrootPath . Path.takeDirectory) path+        in if takeDirectory namever == pkg+           then Just $ mkVersion' . readVersion $ takeFileName namever+           else Nothing+      else Nothing++-- | Get the preferred-versions metadata for package+preferredVersions :: PackageName -> IO (Maybe BL.ByteString)+preferredVersions pkgname =+  withLocalRepo $ \rep -> uncheckClientErrors $+    withIndex rep $ \ IndexCallbacks{..} ->+    fmap indexEntryContent <$> indexLookupFile (IndexPkgPrefs pkgname)++-- | List all files in the Hackage index+-- (.cabal files, metadata .json files, preferred-versions files)+indexFiles :: IO [String]+indexFiles =+  withLocalRepo $ \rep -> uncheckClientErrors $ do+    dir <- getDirectory rep+    return $ map dirEntryPath (directoryEntries dir)+  where+    second (_,b,_) = b++    dirEntryPath = Path.toUnrootedFilePath . Path.unrootPath . second++-- | Get the latest version of package from the index+--+-- Note: does not take preferred-versions into account+latestVersion :: PackageName -> IO (Maybe Version)+latestVersion pkgname = do+  versions <- packageVersions pkgname+  if null versions then return Nothing+    else return $ Just $ last versions++-- | Get the index timestamp for (the latest revision of) pkgid+getTimestamp :: PackageIdentifier -> IO (Maybe UTCTime)+getTimestamp pkgid =+  withLocalRepo $ \rep -> uncheckClientErrors $+    withIndex rep $ \ IndexCallbacks{..} ->+    fmap (posixSecondsToUTCTime . realToFrac . indexEntryTime) <$>+    indexLookupFile (IndexPkgCabal pkgid)++-- | Convert a PackageID if unversioned to latest package version+packageIdOrLatest :: PackageIdentifier -> IO PackageIdentifier+packageIdOrLatest pkgid = do+  let name = pkgName pkgid+  if pkgVersion pkgid == nullVersion then do+    mlatest <- latestVersion name+    return $ maybe pkgid (PackageIdentifier name) mlatest+    else return pkgid++-- | List all packages in the index (unsorted for performance)+listPackages :: IO [String]+listPackages =+  withLocalRepo $ \rep -> uncheckClientErrors $ do+    dir <- getDirectory rep+    return $ nub $ mapMaybe (extractPkg . second) (directoryEntries dir)+  where+    extractPkg path =+      if Path.takeExtension path == ".cabal" then+        (Just . takeWhile (/= '/') . Path.toUnrootedFilePath . Path.unrootPath) path+        else Nothing++    second (_,b,_) = b