Skip to content

Commit 83274a8

Browse files
committed
WIP: cache pkg-config results
1 parent 15a0010 commit 83274a8

File tree

1 file changed

+48
-17
lines changed

1 file changed

+48
-17
lines changed

cabal-install-solver/src/Distribution/Solver/Types/PkgConfigDb.hs

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{-# LANGUAGE DeriveDataTypeable #-}
22
{-# LANGUAGE DeriveGeneric #-}
3+
{-# LANGUAGE TupleSections #-}
4+
{-# LANGUAGE TypeApplications #-}
35
-----------------------------------------------------------------------------
46
-- |
57
-- Module : Distribution.Solver.Types.PkgConfigDb
@@ -21,14 +23,15 @@ module Distribution.Solver.Types.PkgConfigDb
2123
) where
2224

2325
import Distribution.Solver.Compat.Prelude
24-
import Prelude ()
26+
import Prelude (read)
2527

26-
import Control.Exception (handle)
27-
import Control.Monad (mapM)
28+
import Control.Exception (handle, handleJust)
2829
import qualified Data.Map as M
29-
import System.FilePath (splitSearchPath)
30+
import System.FilePath (splitSearchPath, takeBaseName, (</>))
3031

3132
import Distribution.Compat.Environment (lookupEnv)
33+
import Distribution.Compat.Directory
34+
import Distribution.Compat.Time (getModTime, ModTime)
3235
import Distribution.Package (PkgconfigName, mkPkgconfigName)
3336
import Distribution.Parsec
3437
import Distribution.Simple.Program
@@ -38,6 +41,7 @@ import Distribution.Simple.Utils (info)
3841
import Distribution.Types.PkgconfigVersion
3942
import Distribution.Types.PkgconfigVersionRange
4043
import Distribution.Verbosity (Verbosity)
44+
import System.IO.Error (isDoesNotExistError)
4145

4246
-- | The list of packages installed in the system visible to
4347
-- @pkg-config@. This is an opaque datatype, to be constructed with
@@ -63,19 +67,34 @@ readPkgConfigDb verbosity progdb = handle ioErrorHandler $ do
6367
case mpkgConfig of
6468
Nothing -> noPkgConfig "Cannot find pkg-config program"
6569
Just (pkgConfig, _) -> do
66-
pkgList <- lines <$> getProgramOutput verbosity pkgConfig ["--list-all"]
67-
-- The output of @pkg-config --list-all@ also includes a description
68-
-- for each package, which we do not need.
69-
let pkgNames = map (takeWhile (not . isSpace)) pkgList
70-
(pkgVersions, _errs, exitCode) <-
71-
getProgramInvocationOutputAndErrors verbosity
72-
(programInvocation pkgConfig ("--modversion" : pkgNames))
73-
case exitCode of
74-
ExitSuccess -> (return . pkgConfigDbFromList . zip pkgNames) (lines pkgVersions)
75-
-- if there's a single broken pc file the above fails, so we fall back into calling it individually
76-
_ -> do
77-
info verbosity ("call to pkg-config --modversion on all packages failed. Falling back to querying pkg-config individually on each package")
78-
pkgConfigDbFromList . catMaybes <$> mapM (getIndividualVersion pkgConfig) pkgNames
70+
-- TODO use a more sensible data structure
71+
-- should I just add a `ModTime` field to `PkgConfigDb` and serialise that directly?
72+
-- TODO don't hardcode path (how to get? does this need to be its own preference?)
73+
let cacheFile = "/home/gthomas/.local/state/cabal/pkg-config"
74+
writeCache = writeFile cacheFile . show @[(String, ModTime, String)]
75+
readCache = handleJust (guard . isDoesNotExistError) (\() -> pure []) $ read @[(String, ModTime, String)] <$> readFile cacheFile
76+
-- TODO more logging
77+
pcPaths <- splitOn ":" . dropWhileEnd isSpace
78+
-- TODO verbose logs imply we already call this elsewhere
79+
<$> getProgramOutput verbosity pkgConfig ["--variable", "pc_path", "pkg-config"]
80+
-- ["/usr/lib/pkgconfig", "/usr/share/pkgconfig"]
81+
ts <- traverse (\(d, f) -> (takeBaseName f,) <$> getModTime (d </> f))
82+
-- TODO why are there directories here (`personality`)?
83+
-- note that if we remove them, we get exactly the same list here that we'd get from `pkg-config --list-package-names`
84+
-- should we sanity-check that, or is it part of the `pkg-config` spec?
85+
-- =<< filterM (fmap not . doesFileExist)
86+
=<< foldMap (\p -> map (p,) <$> listDirectory p) pcPaths
87+
cache <- M.fromList . map (\(p, t, v) -> (p, (t, v))) <$> readCache
88+
r <- fmap catMaybes $ for ts $ \(p, t) -> do
89+
case M.lookup p cache of
90+
Just (storedTime, storedVersion) | t == storedTime -> pure $ Just (p, t, storedVersion)
91+
-- TODO the `Nothing` case is (on my machine) always for packages with missing dependencies
92+
-- since this call will always fail for them, for these never get cached, and thus get checked again on every iteration
93+
-- but maybe that's fine? since we'd hope there aren't many of them
94+
-- TODO inline `getIndividualVersion` and add better error reporting?
95+
_ -> fmap ((p, t,) . snd) <$> getIndividualVersion pkgConfig p
96+
writeCache r
97+
pure $ pkgConfigDbFromList $ map (\(p, _t, v) -> (p, v)) r
7998
where
8099
-- For when pkg-config invocation fails (possibly because of a
81100
-- too long command line).
@@ -85,6 +104,18 @@ readPkgConfigDb verbosity progdb = handle ioErrorHandler $ do
85104
++ extra)
86105
return NoPkgConfigDb
87106

107+
-- TODO vendored from `extra` - what should I do?
108+
splitOn :: (Eq a) => [a] -> [a] -> [[a]]
109+
splitOn [] _ = error "splitOn, needle may not be empty"
110+
splitOn _ [] = [[]]
111+
splitOn needle haystack = a : if null b then [] else splitOn needle $ drop (length needle) b
112+
where
113+
(a, b) = breakOn needle haystack
114+
breakOn :: Eq a => [a] -> [a] -> ([a], [a])
115+
breakOn needle haystack | needle `isPrefixOf` haystack = ([], haystack)
116+
breakOn _ [] = ([], [])
117+
breakOn needle (x:xs) = first (x:) $ breakOn needle xs
118+
88119
ioErrorHandler :: IOException -> IO PkgConfigDb
89120
ioErrorHandler e = noPkgConfig (show e)
90121

0 commit comments

Comments
 (0)