{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Rewrite
( Args (..),
runAll,
golangModuleVersion,
quotedUrls,
quotedUrlsET,
rustCrateVersion,
version,
redirectedUrls,
)
where
import qualified Data.Text as T
import Data.Text.Encoding as T
import Data.Text.Encoding.Error as T
import Data.Text.IO as T
import qualified File
import qualified Network.HTTP.Client as HTTP
import Network.HTTP.Types.Status (statusCode)
import qualified Nix
import OurPrelude
import qualified Polysemy.Error as Error
import Polysemy.Output (Output, output)
import qualified Process
import System.Exit
import Utils (UpdateEnv (..))
import qualified Utils
( runLog,
stripQuotes,
)
import Prelude hiding (log)
{-
This module contains rewrite functions that make some modification to the
nix derivation. These are in the IO monad so that they can do things like
re-run nix-build to recompute hashes, but morally they should just stick to
editing the derivationFile for their one stated purpose.
The return contract is:
- If it makes a modification, it should return a simple message to attach to
the pull request description to provide context or justification for code
reviewers (e.g., a GitHub issue or RFC).
- If it makes no modification, return None
- If it throws an exception, nixpkgs-update will be aborted for the package and
no other rewrite functions will run.
TODO: Setup some unit tests for these!
-}
data Args = Args
{ updateEnv :: Utils.UpdateEnv,
attrPath :: Text,
derivationFile :: FilePath,
derivationContents :: Text,
hasUpdateScript :: Bool
}
type Rewriter = (Text -> IO ()) -> Args -> ExceptT Text IO (Maybe Text)
type Plan = [(Text, Rewriter)]
plan :: Plan
plan =
[ ("version", version),
("rustCrateVersion", rustCrateVersion),
("golangModuleVersion", golangModuleVersion),
("updateScript", updateScript),
("", quotedUrlsET) -- Don't change the logger
--("redirectedUrl", Rewrite.redirectedUrls)
]
runAll :: (Text -> IO ()) -> Args -> ExceptT Text IO [Text]
runAll log args = do
msgs <- forM plan $ \(name, f) -> do
let log' msg =
if T.null name
then log msg
else log $ ("[" <> name <> "] ") <> msg
lift $ log' "" -- Print initial empty message to signal start of rewriter
f log' args
return $ catMaybes msgs
--------------------------------------------------------------------------------
-- The canonical updater: updates the src attribute and recomputes the sha256
version :: MonadIO m => (Text -> m ()) -> Args -> ExceptT Text m (Maybe Text)
version log args@Args {..} = do
if
| Nix.numberOfFetchers derivationContents > 1 || Nix.numberOfHashes derivationContents > 1 -> do
lift $ log "generic version rewriter does not support multiple hashes"
return Nothing
| hasUpdateScript -> do
lift $ log "skipping because derivation has updateScript"
return Nothing
| otherwise -> do
srcVersionFix args
lift $ log "updated version and sha256"
return $ Just "Version update"
--------------------------------------------------------------------------------
-- Rewrite meta.homepage (and eventually other URLs) to be quoted if not
-- already, as per https://github.com/NixOS/rfcs/pull/45
quotedUrls ::
Members '[Process.Process, File.File, Error Text, Output Text] r =>
Args ->
Sem r (Maybe Text)
quotedUrls Args {..} = do
output "[quotedUrls]"
homepage <- Nix.getHomepage attrPath
stripped <- case Utils.stripQuotes homepage of
Nothing -> throw "Could not strip url! This should never happen!"
Just x -> pure x
let goodHomepage = "homepage = " <> homepage <> ";"
let replacer = \target -> File.replace target goodHomepage derivationFile
urlReplaced1 <- replacer ("homepage = " <> stripped <> ";")
urlReplaced2 <- replacer ("homepage = " <> stripped <> " ;")
urlReplaced3 <- replacer ("homepage =" <> stripped <> ";")
urlReplaced4 <- replacer ("homepage =" <> stripped <> "; ")
if urlReplaced1 || urlReplaced2 || urlReplaced3 || urlReplaced4
then do
output "[quotedUrls]: added quotes to meta.homepage"
return $ Just "Quoted meta.homepage for [RFC 45](https://github.com/NixOS/rfcs/pull/45)"
else do
output "[quotedUrls] nothing found to replace"
return Nothing
quotedUrlsET :: MonadIO m => (Text -> IO ()) -> Args -> ExceptT Text m (Maybe Text)
quotedUrlsET log rwArgs =
ExceptT $
liftIO
. runFinal
. embedToFinal
. Error.runError
. Process.runIO
. File.runIO
. Utils.runLog log
$ quotedUrls rwArgs
--------------------------------------------------------------------------------
-- Redirect homepage when moved.
redirectedUrls :: MonadIO m => (Text -> m ()) -> Args -> ExceptT Text m (Maybe Text)
redirectedUrls log Args {..} = do
unstripped <- Nix.getHomepageET attrPath
homepage <- case Utils.stripQuotes unstripped of
Nothing -> throwE "Could not strip homepage! This should never happen!"
Just x -> pure x
response <- liftIO $ do
manager <- HTTP.newManager HTTP.defaultManagerSettings
request <- HTTP.parseRequest (T.unpack homepage)
HTTP.httpLbs request manager
let status = statusCode $ HTTP.responseStatus response
if status `elem` [301, 308]
then do
lift $ log "Redirecting URL"
let headers = HTTP.responseHeaders response
location = lookup "Location" headers
case location of
Nothing -> do
lift $ log "Server did not return a location"
return Nothing
Just ((T.decodeUtf8With T.lenientDecode) -> newHomepage) -> do
_ <- File.replaceIO homepage newHomepage derivationFile
lift $ log "Replaced homepage"
return $
Just $
"Replaced homepage by "
<> newHomepage
<> " due http "
<> (T.pack . show) status
else do
lift $ log "URL not redirected"
return Nothing
--------------------------------------------------------------------------------
-- Rewrite Rust on rustPlatform.buildRustPackage
-- This is basically `version` above, but with a second pass to also update the
-- cargoSha256 vendor hash.
rustCrateVersion :: MonadIO m => (Text -> m ()) -> Args -> ExceptT Text m (Maybe Text)
rustCrateVersion log args@Args {..} = do
if
| not (T.isInfixOf "cargoSha256" derivationContents) -> do
lift $ log "No cargoSha256 found"
return Nothing
| hasUpdateScript -> do
lift $ log "skipping because derivation has updateScript"
return Nothing
| otherwise -> do
-- This starts the same way `version` does, minus the assert
srcVersionFix args
-- But then from there we need to do this a second time for the cargoSha256!
oldCargoSha256 <- Nix.getAttr Nix.Raw "cargoSha256" attrPath
_ <- lift $ File.replaceIO oldCargoSha256 Nix.sha256Zero derivationFile
newCargoSha256 <- Nix.getHashFromBuild attrPath
when (oldCargoSha256 == newCargoSha256) $ throwE "cargoSha256 hashes equal; no update necessary"
lift . log $ "Replacing cargoSha256 with " <> newCargoSha256
_ <- lift $ File.replaceIO Nix.sha256Zero newCargoSha256 derivationFile
-- Ensure the package actually builds and passes its tests
Nix.build attrPath
lift $ log "Finished updating Crate version and replacing hashes"
return $ Just "Rust version update"
--------------------------------------------------------------------------------
-- Rewrite Golang packages with buildGoModule
-- This is basically `version` above, but with a second pass to also update the
-- vendorSha256 go vendor hash.
golangModuleVersion :: MonadIO m => (Text -> m ()) -> Args -> ExceptT Text m (Maybe Text)
golangModuleVersion log args@Args {..} = do
if
| not (T.isInfixOf "buildGoModule" derivationContents && T.isInfixOf "vendorSha256" derivationContents) -> do
lift $ log "Not a buildGoModule package with vendorSha256"
return Nothing
| hasUpdateScript -> do
lift $ log "skipping because derivation has updateScript"
return Nothing
| otherwise -> do
-- This starts the same way `version` does, minus the assert
srcVersionFix args
-- But then from there we need to do this a second time for the vendorSha256!
-- Note that explicit `null` cannot be coerced to a string by nix eval --raw
oldVendorSha256 <- (Nix.getAttr Nix.Raw "vendorSha256" attrPath <|> Nix.getAttr Nix.NoRaw "vendorSha256" attrPath)
lift . log $ "Found old vendorSha256 = " <> oldVendorSha256
original <- liftIO $ T.readFile derivationFile
_ <- lift $ File.replaceIO ("\"" <> oldVendorSha256 <> "\"") "null" derivationFile
ok <- runExceptT $ Nix.build attrPath
_ <-
if isLeft ok
then do
_ <- liftIO $ T.writeFile derivationFile original
_ <- lift $ File.replaceIO oldVendorSha256 Nix.sha256Zero derivationFile
newVendorSha256 <- Nix.getHashFromBuild attrPath
_ <- lift $ File.replaceIO Nix.sha256Zero newVendorSha256 derivationFile
-- Note that on some small bumps, this may not actually change if go.sum did not
lift . log $ "Replaced vendorSha256 with " <> newVendorSha256
else do
lift . log $ "Set vendorSha256 to null"
-- Ensure the package actually builds and passes its tests
Nix.build attrPath
lift $ log "Finished updating vendorSha256"
return $ Just "Golang update"
--------------------------------------------------------------------------------
-- Calls passthru.updateScript
updateScript :: MonadIO m => (Text -> m ()) -> Args -> ExceptT Text m (Maybe Text)
updateScript log Args {..} = do
if hasUpdateScript
then do
(exitCode, msg) <- Nix.runUpdateScript attrPath
case exitCode of
ExitSuccess -> do
lift $ log "Success"
lift $ log msg
return $ Just "Ran passthru.UpdateScript"
ExitFailure num -> do
throwE $ "[updateScript] Failed with exit code " <> tshow num <> "\n" <> msg
else do
lift $ log "skipping because derivation has no updateScript"
return Nothing
--------------------------------------------------------------------------------
-- Common helper functions and utilities
-- Helper to update version and src attributes, re-computing the sha256.
-- This is done by the generic version upgrader, but is also a sub-component of some of the others.
srcVersionFix :: MonadIO m => Args -> ExceptT Text m ()
srcVersionFix Args {..} = do
let UpdateEnv {..} = updateEnv
oldHash <- Nix.getOldHash attrPath
_ <- lift $ File.replaceIO oldVersion newVersion derivationFile
_ <- lift $ File.replaceIO oldHash Nix.sha256Zero derivationFile
newHash <- Nix.getHashFromBuild attrPath
when (oldHash == newHash) $ throwE "Hashes equal; no update necessary"
_ <- lift $ File.replaceIO Nix.sha256Zero newHash derivationFile
return ()