packages feed

cachix-1.13.0: src/Cachix/Client/Command/List.hs

module Cachix.Client.Command.List (list, cachixCacheName, Entry (..), configuredEntries) where

import Cachix.Client.Env (Env, effectiveHost)
import Cachix.Client.Exception (CachixException)
import Cachix.Client.NixConf qualified as NixConf
import Cachix.Client.OptionsParser (ListOptions (..))
import Cachix.Client.URI (URI)
import Cachix.Client.URI qualified as URI
import Control.Exception.Safe qualified as Safe
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as T
import Protolude hiding (hPutStrLn, list, toS)
import Protolude.Conv (toS)
import System.IO (hPutStrLn)

data Entry = Entry
  { entrySource :: FilePath,
    entryUri :: Text,
    entryKey :: Text,
    entryCacheName :: Maybe Text
  }

list :: Env -> ListOptions -> IO ()
list env ListOptions {listAll} = do
  let cachixHost = effectiveHost env
  globalSources <- readSources NixConf.Global
  localSources <- readSources NixConf.Local
  let filtered = configuredEntries cachixHost ListOptions {listAll} (globalSources <> localSources)
  if null filtered
    then
      if listAll
        then putStrLn ("No substituters configured in nix.conf." :: Text)
        else putStrLn ("No Cachix caches configured. Try `cachix use <name>` first." :: Text)
    else printEntries filtered

-- | Select and deduplicate entries before rendering, retaining their source.
configuredEntries :: URI -> ListOptions -> [NixConf.NixConfSource] -> [Entry]
configuredEntries cachixHost ListOptions {listAll} sources =
  let allSources = dedupBy NixConf.nixConfPath sources
      entries = dedupBy entryKey (concatMap (toEntries cachixHost) allSources)
   in if listAll then entries else filter (isJust . entryCacheName) entries

readSources :: NixConf.NixConfLoc -> IO [NixConf.NixConfSource]
readSources loc =
  NixConf.read loc >>= \case
    Nothing -> pure []
    Just s ->
      Safe.catch (NixConf.resolveIncludes s) $ \(e :: CachixException) -> do
        hPutStrLn stderr $
          "Warning: skipping includes from "
            <> NixConf.nixConfPath s
            <> ": "
            <> show e
        pure [s]

toEntries :: URI -> NixConf.NixConfSource -> [Entry]
toEntries cachixHost src =
  [ Entry
      { entrySource = NixConf.nixConfPath src,
        entryUri = u,
        entryKey = canonicalKey u,
        entryCacheName = cachixCacheName cachixHost u
      }
  | u <- NixConf.readLines NixConf.isAnySubstituter src
  ]

-- | Keep the first occurrence of each @key x@, preserving input order.
dedupBy :: (Ord k) => (a -> k) -> [a] -> [a]
dedupBy key = go Set.empty
  where
    go _ [] = []
    go seen (x : xs)
      | Set.member (key x) seen = go seen xs
      | otherwise = x : go (Set.insert (key x) seen) xs

-- | Canonical form used only for deduplication (lowercased scheme + host,
-- default ports dropped). The original
-- substituter text is preserved for display.
canonicalKey :: Text -> Text
canonicalKey raw =
  case URI.parseURI (toS raw) of
    Right parsed | Just k <- URI.canonicalForm parsed -> k
    _ -> raw

cachixCacheName :: URI -> Text -> Maybe Text
cachixCacheName cachixHost rawUri = do
  parsed <- rightToMaybe $ URI.parseURI (toS rawUri)
  cHost <- hostText cachixHost
  sHost <- hostText parsed
  let suffix = "." <> cHost
  guard (suffix `T.isSuffixOf` sHost && T.length sHost > T.length suffix)
  pure $ T.dropEnd (T.length suffix) sHost
  where
    hostText u = T.toLower . toS . URI.hostBS <$> URI.getHostnameMay u

printEntries :: [Entry] -> IO ()
printEntries entries =
  sequence_ $ intersperse (putStrLn ("" :: Text)) (map printGroup grouped)
  where
    grouped :: [(FilePath, [Entry])]
    grouped =
      let bucket =
            foldl'
              (\m e -> Map.insertWith (flip (<>)) (entrySource e) [e] m)
              Map.empty
              entries
          order = dedupBy identity (map entrySource entries)
       in [(p, Map.findWithDefault [] p bucket) | p <- order]

    nameWidth = maximum (0 : [T.length n | e <- entries, Just n <- [entryCacheName e]])

    printGroup (path, es) = do
      putStrLn $ (toS path :: Text) <> ":"
      forM_ es printEntry

    printEntry e =
      case entryCacheName e of
        Just name ->
          putStrLn $ "  " <> T.justifyLeft nameWidth ' ' name <> "  " <> entryUri e
        Nothing ->
          putStrLn $ "  " <> T.justifyLeft nameWidth ' ' "-" <> "  " <> entryUri e