diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for cabal-pkg-config-version-hook
+
+## 0.1.0.0 -- 2022-03-07
+
+* First version. Main feature is room for improvement.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+
+# A Cabal setup hook for pkg-config version info
+
+Cabal doesn't natively give access to version information from `pkg-config` (yet?). This setup hook gets the version and makes it available via Haskell CPP and C/C++ macros, as well as setting Cabal flags automatically.
diff --git a/cabal-pkg-config-version-hook.cabal b/cabal-pkg-config-version-hook.cabal
new file mode 100644
--- /dev/null
+++ b/cabal-pkg-config-version-hook.cabal
@@ -0,0 +1,27 @@
+cabal-version:      2.4
+name:               cabal-pkg-config-version-hook
+version:            0.1.0.0
+synopsis:           Make Cabal aware of pkg-config package versions
+description:        A setup hook for Cabal that determines the compile-time version of a pkg-config package and adds CPP macros and enables flags.
+-- A URL where users can report bugs.
+-- bug-reports:
+homepage:           https://github.com/hercules-ci/hercules-ci-agent/tree/master/cabal-pkg-config-version-hook#readme
+license:            BSD-3-Clause
+author:             Robert Hensing
+maintainer:         info@hercules-ci.com
+copyright:          cabal-pkg-config-version-hook contributors
+category:           Distribution
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+library
+    exposed-modules:
+      Distribution.PkgConfigVersionHook
+    build-depends:
+      base ==4.*
+      , Cabal >= 2.2.0.0
+      , process
+      , lens
+    hs-source-dirs:   src
+    default-language: Haskell2010
diff --git a/src/Distribution/PkgConfigVersionHook.hs b/src/Distribution/PkgConfigVersionHook.hs
new file mode 100644
--- /dev/null
+++ b/src/Distribution/PkgConfigVersionHook.hs
@@ -0,0 +1,164 @@
+-- |
+--
+--  Example:
+--
+--  @
+--      main :: IO ()
+--      main =
+--        defaultMainWithHooks $
+--          simpleUserHooks
+--            & addHook
+--              (mkSettings "nix-store")
+--                { macroName = "NIX",
+--                  flagPrefixName = "nix"
+--                }
+--  @
+--
+--  The above will look for a pkg-config package @nix-store@, and then
+--
+--    * Define CPP, C and C++ macros
+--
+--        * @NIX_MAJOR@, an integer
+--        * @NIX_MINOR@, an integer
+--        * @NIX_PATCH@, an integer; 0 if missing
+--        * @NIX_IS_AT_LEAST(major,minor,patch)@, returning true when the discovered version @>=@ the specified version
+--
+--    * Set or unset flags like `nix-2_4` so that the flag is true when the
+--      discovered version is at least the version in the flag's name.
+module Distribution.PkgConfigVersionHook
+  ( addHook,
+    mkSettings,
+    Settings (..),
+    composeConfHook,
+  )
+where
+
+import Control.Lens ((%~), (^.))
+import Control.Monad (when)
+import qualified Data.Char as C
+import Data.Foldable (toList)
+import Data.Function ((&))
+import qualified Data.List as L
+import Distribution.Simple
+import Distribution.Simple.Setup (configConfigurationsFlags)
+import Distribution.Types.BuildInfo.Lens (ccOptions, cppOptions, cxxOptions)
+import Distribution.Types.Flag (flagName, mkFlagAssignment, mkFlagName, unFlagName)
+import Distribution.Types.GenericPackageDescription.Lens
+  ( allCondTrees,
+    condBenchmarks,
+    condExecutables,
+    condForeignLibs,
+    condLibrary,
+    condSubLibraries,
+    condTestSuites,
+    genPackageFlags,
+  )
+import System.IO (hPutStrLn, stderr)
+import System.Process (readProcess)
+import qualified Text.ParserCombinators.ReadP as P
+import Prelude hiding (log)
+
+-- | Hook into Cabal to provide pkg-config metadata. Can be applied multiple
+-- times to support multiple packages.
+addHook :: Settings -> UserHooks -> UserHooks
+addHook settings hooks = hooks {confHook = composeConfHook settings (confHook hooks)}
+
+-- | How the metadata for a pkg-config package should be made available to the
+-- cabal file.
+data Settings = Settings
+  { -- | Name of the package; used for querying pkg-config.
+    pkgConfigName :: String,
+    -- | Name to use in the Haskell CPP and C/C++ preprocessor macros.
+    --
+    -- For example, `pkgConfigName = "FOO"` will set the macros
+    --
+    --  * @FOO_MAJOR@
+    --
+    --  * @FOO_MINOR@
+    --
+    --  * @FOO_PATCH@
+    --
+    --  * @FOO_IS_AT_LEAST(major, minor, patch)@
+    macroName :: String,
+    -- | Name to use when setting flag values in the cabal file.
+    --
+    -- Flags named with this prefix, followed by a dash, followed by a major version number, an underscore and a minor version number will be set when the detected package is at least that version.
+    flagPrefixName :: String
+  }
+
+-- | Derive a default 'Settings' value from just a pkg-config package name.
+mkSettings :: String -> Settings
+mkSettings name =
+  Settings
+    { pkgConfigName = name,
+      macroName = map (\x -> case x of '-' -> '_') name,
+      flagPrefixName = name
+    }
+
+-- | Extend the value of 'confHook'. It's what powers 'addHook'.
+composeConfHook settings origHook = \(genericPackageDescription, hookedBuildInfo) confFlags -> do
+  (actualMajor, actualMinor, actualPatch) <- getPkgConfigPackageVersion (pkgConfigName settings)
+
+  let defines =
+        [ "-D" <> macroName settings <> "_MAJOR=" <> show actualMajor,
+          "-D" <> macroName settings <> "_MINOR=" <> show actualMinor,
+          "-D" <> macroName settings <> "_PATCH=" <> show actualPatch,
+          "-D" <> macroName settings <> "_IS_AT_LEAST(a,b,c)=(" <> show actualMajor <> ">a||(" <> show actualMajor <> "==a&&(" <> show actualMinor <> ">b||(" <> show actualMinor <> "==b&&" <> show actualPatch <> ">=c))))"
+        ]
+      extraFlags =
+        [ (mkFlagName (flagPrefixName settings ++ "-" ++ show major ++ "_" ++ show minor), (actualMajor, actualMinor) >= (major, minor))
+          | declaredFlag <- genericPackageDescription ^. genPackageFlags,
+            let rawName = unFlagName $ flagName declaredFlag,
+            rawVersion <- L.stripPrefix (flagPrefixName settings ++ "-") rawName & toList,
+            [major, minor] <- unambiguously parseFlagVersion rawVersion & toList
+        ]
+      setDefines comp x =
+        x
+          & comp . cppOptions %~ (<> defines)
+          & comp . ccOptions %~ (<> defines)
+          & comp . cxxOptions %~ (<> defines)
+      genericPackageDescription' =
+        genericPackageDescription
+          & setDefines (condLibrary . traverse . traverse)
+          & setDefines (condSubLibraries . traverse . traverse . traverse)
+          & setDefines (condForeignLibs . traverse . traverse . traverse)
+          & setDefines (condExecutables . traverse . traverse . traverse)
+          & setDefines (condTestSuites . traverse . traverse . traverse)
+          & setDefines (condBenchmarks . traverse . traverse . traverse)
+
+      configConfigurationsFlags' = configConfigurationsFlags confFlags `mappend` mkFlagAssignment extraFlags
+      confFlags' =
+        confFlags
+          { configConfigurationsFlags = configConfigurationsFlags'
+          }
+  origHook (genericPackageDescription', hookedBuildInfo) confFlags'
+
+parseVersion :: P.ReadP [Int]
+parseVersion = do
+  map read <$> do
+    P.many1 (P.satisfy C.isDigit) `P.sepBy` P.char '.'
+
+parseFlagVersion :: P.ReadP [Int]
+parseFlagVersion =
+  map read <$> do
+    P.many1 (P.satisfy C.isDigit) `P.sepBy` P.char '_'
+
+unambiguously :: P.ReadP a -> String -> Maybe a
+unambiguously p s =
+  case filter (\(a, x) -> x == "") $ P.readP_to_S p s of
+    [(v, _)] -> Just v
+    _ -> Nothing
+
+getPkgConfigPackageVersion :: String -> IO (Int, Int, Int)
+getPkgConfigPackageVersion pkgName = do
+  s <- readProcess "pkg-config" ["--modversion", pkgName] ""
+  case L.sortOn (\(_, s) -> length s) $ P.readP_to_S parseVersion s of
+    [] -> error ("Could not parse version " ++ show s ++ " returned by pkg-config for package " ++ pkgName)
+    (v, r) : _ -> do
+      when (L.dropWhile C.isSpace r /= "") $ do
+        log ("ignoring trailing text " ++ show r ++ " in version " ++ show s ++ " of pkg-config package " ++ pkgName)
+      let v' = v ++ L.repeat 0
+      pure (v' L.!! 0, v' L.!! 1, v' L.!! 2)
+
+-- Should probably use a Cabal function?
+log = hPutStrLn stderr
