{-# LANGUAGE QuasiQuotes #-} module Cachix.Client.InstallationMode ( InstallationMode (..), NixEnv (..), getInstallationMode, addBinaryCache, isTrustedUser, getUser, fromString, toString, UseOptions (..) ) where import Cachix.Api as Api import Cachix.Client.Config (Config) import Cachix.Client.Exception (CachixException (..)) import qualified Cachix.Client.NetRc as NetRc import qualified Cachix.Client.NixConf as NixConf import Data.String.Here import qualified Data.Text as T import Protolude import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable) import System.Environment (lookupEnv) import System.FilePath ((), replaceFileName) import Prelude (String) data NixEnv = NixEnv { isTrusted :: Bool, isRoot :: Bool, isNixOS :: Bool } -- NOTE: update the list of options for --mode argument in OptionsParser.hs data InstallationMode = Install NixConf.NixConfLoc | WriteNixOS | UntrustedRequiresSudo deriving (Show, Eq) data UseOptions = UseOptions { useMode :: Maybe InstallationMode, useNixOSFolder :: FilePath } deriving (Show) fromString :: String -> Maybe InstallationMode fromString "root-nixconf" = Just $ Install NixConf.Global fromString "user-nixconf" = Just $ Install NixConf.Local fromString "nixos" = Just WriteNixOS fromString "untrusted-requires-sudo" = Just UntrustedRequiresSudo fromString _ = Nothing toString :: InstallationMode -> String toString (Install NixConf.Global) = "root-nixconf" toString (Install NixConf.Local) = "user-nixconf" toString WriteNixOS = "nixos" toString UntrustedRequiresSudo = "untrusted-requires-sudo" getInstallationMode :: NixEnv -> InstallationMode getInstallationMode NixEnv {..} | isNixOS && isRoot = WriteNixOS | not isNixOS && isRoot = Install NixConf.Global | isTrusted = Install NixConf.Local | otherwise = UntrustedRequiresSudo -- | Add a Binary cache to nix.conf, print nixos config or fail addBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> InstallationMode -> IO () addBinaryCache _ _ _ UntrustedRequiresSudo = throwIO $ MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon" addBinaryCache maybeConfig bc useOptions WriteNixOS = nixosBinaryCache maybeConfig bc useOptions addBinaryCache maybeConfig bc@Api.BinaryCache {..} _ (Install ncl) = do -- TODO: might need locking one day gnc <- NixConf.read NixConf.Global (input, output) <- case ncl of NixConf.Global -> return ([gnc], gnc) NixConf.Local -> do lnc <- NixConf.read NixConf.Local return ([gnc, lnc], lnc) let nixconf = fromMaybe (NixConf.NixConf []) output netrcLocMaybe <- forM (guard $ not isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf addNetRCLine = fromMaybe identity $ do netrcLoc <- netrcLocMaybe :: Maybe FilePath -- We only add the netrc line for local user configs for now. -- On NixOS we assume it will be picked up from the default location. guard (ncl == NixConf.Local) pure (NixConf.setNetRC $ toS netrcLoc) NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf filename <- NixConf.getFilename ncl putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename nixosBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> IO () nixosBinaryCache maybeConfig bc@Api.BinaryCache {..} UseOptions {useNixOSFolder = baseDirectory} = do eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions) case eitherPermissions of Left _ -> throwIO $ NixOSInstructions noEtcPermissionInstructions Right permissions | writable permissions -> installFiles | otherwise -> throwIO $ NixOSInstructions noEtcPermissionInstructions where installFiles = do createDirectoryIfMissing True $ toS toplevel writeFile (toS glueModuleFile) glueModule writeFile (toS cacheModuleFile) cacheModule unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global putText instructions configurationNix :: Text configurationNix = toS $ toS baseDirectory "configuration.nix" namespace :: Text namespace = "cachix" toplevel :: Text toplevel = toS $ toS baseDirectory toS namespace glueModuleFile :: Text glueModuleFile = toplevel <> ".nix" cacheModuleFile :: Text cacheModuleFile = toplevel <> "/" <> toS name <> ".nix" noEtcPermissionInstructions :: Text noEtcPermissionInstructions = [iTrim| Could not install NixOS configuration to /etc/nixos/ due to lack of write permissions. Pass `--nixos-folder /etc/mynixos/` as an alternative location with write permissions. |] instructions :: Text instructions = [iTrim| Cachix configuration written to ${glueModuleFile}. Binary cache ${name} configuration written to ${cacheModuleFile}. To start using cachix add the following to your ${configurationNix}: imports = [ ./cachix.nix ]; Then run: $ nixos-rebuild switch |] glueModule :: Text glueModule = [i| # WARN: this file will get overwritten by $ cachix use { pkgs, lib, ... }: let folder = ./cachix; toImport = name: value: folder + ("/" + name); filterCaches = key: value: value == "regular" && lib.hasSuffix ".nix" key; imports = lib.mapAttrsToList toImport (lib.filterAttrs filterCaches (builtins.readDir folder)); in { inherit imports; nix.binaryCaches = ["https://cache.nixos.org/"]; } |] cacheModule :: Text cacheModule = [i| { nix = { binaryCaches = [ "${uri}" ]; binaryCachePublicKeys = [ ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)} ]; }; } |] -- TODO: allow overriding netrc location addPrivateBinaryCacheNetRC :: Maybe Config -> Api.BinaryCache -> NixConf.NixConfLoc -> IO FilePath addPrivateBinaryCacheNetRC maybeConfig bc nixconf = do filename <- (`replaceFileName` "netrc") <$> NixConf.getFilename nixconf case maybeConfig of Nothing -> panic "Run $ cachix authtoken " Just config -> do let netrcfile = fromMaybe filename Nothing -- TODO: get netrc from nixconf NetRc.add config [bc] netrcfile putErrText $ "Configured private read access credentials in " <> toS filename pure filename isTrustedUser :: [Text] -> IO Bool isTrustedUser users = do user <- getUser -- to detect single user installations permissions <- getPermissions "/nix/store" let isTrusted = writable permissions || user `elem` users when (not (null groups) && not isTrusted) $ do -- TODO: support Nix group syntax putText "Warn: cachix doesn't yet support checking if user is trusted via groups, so it will recommend sudo" putStrLn $ "Warn: groups found " <> T.intercalate "," groups return isTrusted where groups = filter (\u -> T.head u == '@') users getUser :: IO Text getUser = do maybeUser <- lookupEnv "USER" case maybeUser of Nothing -> throwIO $ UserEnvNotSet "$USER must be set" Just user -> return $ toS user