{-# LANGUAGE ExtendedDefaultRules #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-type-defaults #-} module Update ( addPatched, assertNotUpdatedOn, cveAll, cveReport, prMessage, sourceGithubAll, updateAll, updatePackage, ) where import CVE (CVE, cveID, cveLI) import qualified Check import Control.Concurrent import qualified Data.ByteString.Lazy.Char8 as BSL import Data.IORef import Data.Maybe (fromJust) import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time.Calendar (showGregorian) import Data.Time.Clock (UTCTime, getCurrentTime, utctDay) import qualified GH import qualified Git import Language.Haskell.TH.Env (envQ) import NVD (getCVEs, withVulnDB) import qualified Nix import qualified NixpkgsReview import OurPrelude import Outpaths import qualified Rewrite import qualified Skiplist import qualified Time import Utils ( Options (..), URL, UpdateEnv (..), Version, branchName, logDir, parseUpdates, prTitle, whenBatch, ) import qualified Version import Prelude hiding (log) default (T.Text) data MergeBaseOutpathsInfo = MergeBaseOutpathsInfo { lastUpdated :: UTCTime, mergeBaseOutpaths :: Set ResultLine } log' :: MonadIO m => FilePath -> Text -> m () log' logFile msg = do runDate <- liftIO $ runM $ Time.runIO Time.runDate liftIO $ T.appendFile logFile (runDate <> " " <> msg <> "\n") logFileName :: IO String logFileName = do lDir <- logDir now <- getCurrentTime let logFile = lDir <> "/" <> showGregorian (utctDay now) <> ".log" putStrLn ("Using log file: " <> logFile) return logFile getLog :: Options -> IO (Text -> IO ()) getLog o = do if batchUpdate o then do logFile <- logFileName let log = log' logFile T.appendFile logFile "\n\n" return log else return T.putStrLn notifyOptions :: (Text -> IO ()) -> Options -> IO () notifyOptions log o = do let repr f = if f o then "YES" else "NO" let ghUser = GH.untagName . githubUser $ o let pr = repr doPR let outpaths = repr calculateOutpaths let cve = repr makeCVEReport let review = repr runNixpkgsReview npDir <- tshow <$> Git.nixpkgsDir log $ [interpolate| Configured Nixpkgs-Update Options: ---------------------------------- GitHub User: $ghUser Send pull request on success: $pr Calculate Outpaths: $outpaths CVE Security Report: $cve Run nixpkgs-review: $review Nixpkgs Dir: $npDir ----------------------------------|] updateAll :: Options -> Text -> IO () updateAll o updates = do log <- getLog o log "New run of nixpkgs-update" notifyOptions log o twoHoursAgo <- runM $ Time.runIO Time.twoHoursAgo mergeBaseOutpathSet <- liftIO $ newIORef (MergeBaseOutpathsInfo twoHoursAgo S.empty) updateLoop o log (parseUpdates updates) mergeBaseOutpathSet cveAll :: Options -> Text -> IO () cveAll o updates = do let u' = rights $ parseUpdates updates results <- mapM ( \(p, oldV, newV, url) -> do r <- cveReport (UpdateEnv p oldV newV url o) return $ p <> ": " <> oldV <> " -> " <> newV <> "\n" <> r ) u' T.putStrLn (T.unlines results) sourceGithubAll :: Options -> Text -> IO () sourceGithubAll o updates = do let u' = rights $ parseUpdates updates _ <- runExceptT $ do Git.fetchIfStale <|> liftIO (T.putStrLn "Failed to fetch.") Git.cleanAndResetTo "master" mapM_ ( \(p, oldV, newV, url) -> do let updateEnv = UpdateEnv p oldV newV url o runExceptT $ do attrPath <- Nix.lookupAttrPath updateEnv srcUrl <- Nix.getSrcUrl attrPath v <- GH.latestVersion updateEnv srcUrl if v /= newV then liftIO $ T.putStrLn $ p <> ": " <> oldV <> " -> " <> newV <> " -> " <> v else return () ) u' updateLoop :: Options -> (Text -> IO ()) -> [Either Text (Text, Version, Version, Maybe URL)] -> IORef MergeBaseOutpathsInfo -> IO () updateLoop _ log [] _ = log "nixpkgs-update finished" updateLoop o log (Left e : moreUpdates) mergeBaseOutpathsContext = do log e updateLoop o log moreUpdates mergeBaseOutpathsContext updateLoop o log (Right (pName, oldVer, newVer, url) : moreUpdates) mergeBaseOutpathsContext = do log (pName <> " " <> oldVer <> " -> " <> newVer <> fromMaybe "" (fmap (" " <>) url)) let updateEnv = UpdateEnv pName oldVer newVer url o updated <- updatePackageBatch log updateEnv mergeBaseOutpathsContext case updated of Left failure -> do log $ "FAIL " <> failure cleanupResult <- runExceptT $ Git.cleanup (branchName updateEnv) case cleanupResult of Left e -> liftIO $ print e _ -> if ".0" `T.isSuffixOf` newVer then let Just newNewVersion = ".0" `T.stripSuffix` newVer in updateLoop o log (Right (pName, oldVer, newNewVersion, url) : moreUpdates) mergeBaseOutpathsContext else updateLoop o log moreUpdates mergeBaseOutpathsContext Right _ -> do log "SUCCESS" updateLoop o log moreUpdates mergeBaseOutpathsContext -- Arguments this function should have to make it testable: -- - the merge base commit (should be updated externally to this function) -- - the merge base context should be updated externally to this function -- - the commit for branches: master, staging, staging-next updatePackageBatch :: (Text -> IO ()) -> UpdateEnv -> IORef MergeBaseOutpathsInfo -> IO (Either Text ()) updatePackageBatch log updateEnv@UpdateEnv {..} mergeBaseOutpathsContext = runExceptT $ do let pr = doPR options -- Filters that don't need git whenBatch updateEnv do Skiplist.packageName packageName -- Update our git checkout Git.fetchIfStale <|> liftIO (T.putStrLn "Failed to fetch.") Git.cleanAndResetTo "master" -- Filters: various cases where we shouldn't update the package attrPath <- Nix.lookupAttrPath updateEnv hasUpdateScript <- Nix.hasUpdateScript attrPath whenBatch updateEnv do Skiplist.attrPath attrPath when pr do Git.checkAutoUpdateBranchDoesntExist packageName GH.checkExistingUpdatePR updateEnv attrPath unless hasUpdateScript do Nix.assertNewerVersion updateEnv Version.assertCompatibleWithPathPin updateEnv attrPath derivationFile <- Nix.getDerivationFile attrPath unless hasUpdateScript do assertNotUpdatedOn updateEnv derivationFile "master" assertNotUpdatedOn updateEnv derivationFile "staging" assertNotUpdatedOn updateEnv derivationFile "staging-next" -- Calculate output paths for rebuilds and our merge base mergeBase <- if batchUpdate options then Git.checkoutAtMergeBase (branchName updateEnv) else pure "HEAD" let calcOutpaths = calculateOutpaths options oneHourAgo <- liftIO $ runM $ Time.runIO Time.oneHourAgo mergeBaseOutpathsInfo <- liftIO $ readIORef mergeBaseOutpathsContext mergeBaseOutpathSet <- if calcOutpaths && lastUpdated mergeBaseOutpathsInfo < oneHourAgo then do mbos <- currentOutpathSet now <- liftIO getCurrentTime liftIO $ writeIORef mergeBaseOutpathsContext (MergeBaseOutpathsInfo now mbos) return mbos else if calcOutpaths then return $ mergeBaseOutpaths mergeBaseOutpathsInfo else return $ dummyOutpathSetBefore attrPath -- Get the original values for diffing purposes derivationContents <- liftIO $ T.readFile derivationFile oldHash <- Nix.getOldHash attrPath oldSrcUrl <- Nix.getSrcUrl attrPath oldVerMay <- rightMay `fmapRT` (lift $ runExceptT $ Nix.getAttr Nix.Raw "version" attrPath) tryAssert "The derivation has no 'version' attribute, so do not know how to figure out the version while doing an updateScript update" (not hasUpdateScript || isJust oldVerMay) -- One final filter Skiplist.content derivationContents ---------------------------------------------------------------------------- -- UPDATES -- -- At this point, we've stashed the old derivation contents and -- validated that we actually should be rewriting something. Get -- to work processing the various rewrite functions! rewriteMsgs <- Rewrite.runAll log Rewrite.Args {..} ---------------------------------------------------------------------------- -- Compute the diff and get updated values diffAfterRewrites <- Git.diff mergeBase tryAssert "The diff was empty after rewrites." (diffAfterRewrites /= T.empty) lift . log $ "Diff after rewrites:\n" <> diffAfterRewrites updatedDerivationContents <- liftIO $ T.readFile derivationFile newSrcUrl <- Nix.getSrcUrl attrPath newHash <- Nix.getHash attrPath newVerMay <- rightMay `fmapRT` (lift $ runExceptT $ Nix.getAttr Nix.Raw "version" attrPath) tryAssert "The derivation has no 'version' attribute, so do not know how to figure out the version while doing an updateScript update" (not hasUpdateScript || isJust newVerMay) -- Sanity checks to make sure the PR is worth opening unless hasUpdateScript do when (derivationContents == updatedDerivationContents) $ throwE "No rewrites performed on derivation." when (oldSrcUrl == newSrcUrl) $ throwE "Source url did not change. " when (oldHash == newHash) $ throwE "Hashes equal; no update necessary" editedOutpathSet <- if calcOutpaths then currentOutpathSet else return $ dummyOutpathSetAfter attrPath let opDiff = S.difference mergeBaseOutpathSet editedOutpathSet let numPRebuilds = numPackageRebuilds opDiff whenBatch updateEnv do Skiplist.python numPRebuilds derivationContents when (numPRebuilds == 0) (throwE "Update edits cause no rebuilds.") Nix.build attrPath -- -- Update updateEnv if using updateScript updateEnv' <- if hasUpdateScript then do -- Already checked that these are Just above. let Just oldVer = oldVerMay let Just newVer = newVerMay return $ UpdateEnv packageName oldVer newVer (Just "passthru.updateScript") options else return updateEnv -- -- Publish the result lift . log $ "Successfully finished processing" result <- Nix.resultLink publishPackage log updateEnv' oldSrcUrl newSrcUrl attrPath result (Just opDiff) rewriteMsgs whenBatch updateEnv do Git.cleanAndResetTo "master" publishPackage :: (Text -> IO ()) -> UpdateEnv -> Text -> Text -> Text -> Text -> Maybe (Set ResultLine) -> [Text] -> ExceptT Text IO () publishPackage log updateEnv oldSrcUrl newSrcUrl attrPath result opDiff rewriteMsgs = do let prBase = if (isNothing opDiff || numPackageRebuilds (fromJust opDiff) < 100) then "master" else "staging" cachixTestInstructions <- doCachix log updateEnv result resultCheckReport <- case Skiplist.checkResult (packageName updateEnv) of Right () -> lift $ Check.result updateEnv (T.unpack result) Left msg -> pure msg metaDescription <- Nix.getDescription attrPath <|> return T.empty metaHomepage <- Nix.getHomepageET attrPath <|> return T.empty metaChangelog <- Nix.getChangelog attrPath <|> return T.empty cveRep <- liftIO $ cveReport updateEnv releaseUrl <- GH.releaseUrl updateEnv newSrcUrl <|> return "" compareUrl <- GH.compareUrl oldSrcUrl newSrcUrl <|> return "" maintainers <- Nix.getMaintainers attrPath let commitMsg = commitMessage updateEnv attrPath Git.commit commitMsg commitHash <- Git.headHash nixpkgsReviewMsg <- if prBase /= "staging" && (runNixpkgsReview . options $ updateEnv) then liftIO $ NixpkgsReview.runReport log commitHash else return "" -- Try to push it three times when (doPR . options $ updateEnv) (Git.push updateEnv <|> Git.push updateEnv <|> Git.push updateEnv) isBroken <- Nix.getIsBroken attrPath when (batchUpdate . options $ updateEnv) (lift untilOfBorgFree) let prMsg = prMessage updateEnv isBroken metaDescription metaHomepage metaChangelog rewriteMsgs releaseUrl compareUrl resultCheckReport commitHash attrPath maintainers result (fromMaybe "" (outpathReport <$> opDiff)) cveRep cachixTestInstructions nixpkgsReviewMsg liftIO $ log prMsg if (doPR . options $ updateEnv) then do let ghUser = GH.untagName . githubUser . options $ updateEnv pullRequestUrl <- GH.pr updateEnv (prTitle updateEnv attrPath) prMsg (ghUser <> ":" <> (branchName updateEnv)) prBase liftIO $ log pullRequestUrl else liftIO $ T.putStrLn prMsg commitMessage :: UpdateEnv -> Text -> Text commitMessage updateEnv attrPath = prTitle updateEnv attrPath brokenWarning :: Bool -> Text brokenWarning False = "" brokenWarning True = "- WARNING: Package has meta.broken=true; Please manually test this package update and remove the broken attribute." prMessage :: UpdateEnv -> Bool -> Text -> Text -> Text -> [Text] -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text -> Text prMessage updateEnv isBroken metaDescription metaHomepage metaChangelog rewriteMsgs releaseUrl compareUrl resultCheckReport commitHash attrPath maintainers resultPath opReport cveRep cachixTestInstructions nixpkgsReviewMsg = -- Some components of the PR description are pre-generated prior to calling -- because they require IO, but in general try to put as much as possible for -- the formatting into the pure function so that we can control the body -- formatting in one place and unit test it. let brokenMsg = brokenWarning isBroken metaHomepageLine = if metaHomepage == T.empty then "" else "meta.homepage for " <> attrPath <> " is: " <> metaHomepage metaDescriptionLine = if metaDescription == T.empty then "" else "meta.description for " <> attrPath <> " is: " <> metaDescription metaChangelogLine = if metaDescription == T.empty then "" else "meta.changelog for " <> attrPath <> " is: " <> metaChangelog rewriteMsgsLine = foldl (\ms m -> ms <> T.pack "\n- " <> m) "\n###### Updates performed" rewriteMsgs maintainersCc = if not (T.null maintainers) then "cc " <> maintainers <> " for [testing](https://github.com/ryantm/nixpkgs-update/blob/master/doc/nixpkgs-maintainer-faq.md#r-ryantm-opened-a-pr-for-my-package-what-do-i-do)." else "" releaseUrlMessage = if releaseUrl == T.empty then "" else "- [Release on GitHub](" <> releaseUrl <> ")" compareUrlMessage = if compareUrl == T.empty then "" else "- [Compare changes on GitHub](" <> compareUrl <> ")" nixpkgsReviewSection = if nixpkgsReviewMsg == T.empty then "NixPkgs review skipped" else [interpolate| We have automatically built all packages that will get rebuilt due to this change. This gives evidence on whether the upgrade will break dependent packages. Note sometimes packages show up as _failed to build_ independent of the change, simply because they are already broken on the target branch. $nixpkgsReviewMsg |] pat link = [interpolate|This update was made based on information from $link.|] sourceLinkInfo = maybe "" pat $ sourceURL updateEnv ghUser = GH.untagName . githubUser . options $ updateEnv batch = batchUpdate . options $ updateEnv automatic = if batch then "Automatic" else "Semi-automatic" in [interpolate| $automatic update generated by [nixpkgs-update](https://github.com/ryantm/nixpkgs-update) tools. $sourceLinkInfo $brokenMsg $metaDescriptionLine $metaHomepageLine $metaChangelogLine $rewriteMsgsLine ###### To inspect upstream changes $releaseUrlMessage $compareUrlMessage ###### Impact
Checks done (click to expand) --- - built on NixOS $resultCheckReport ---
Rebuild report (if merged into master) (click to expand) ``` $opReport ```
Instructions to test this update (click to expand) --- $cachixTestInstructions ``` nix-build -A $attrPath https://github.com/$ghUser/nixpkgs/archive/$commitHash.tar.gz ``` After you've downloaded or built it, look at the files and if there are any, run the binaries: ``` ls -la $resultPath ls -la $resultPath/bin ``` ---

$cveRep ### Pre-merge build results $nixpkgsReviewSection --- ###### Maintainer pings $maintainersCc |] jqBin :: String jqBin = fromJust ($$(envQ "JQ") :: Maybe String) <> "/bin/jq" untilOfBorgFree :: MonadIO m => m () untilOfBorgFree = do stats <- shell "curl -s https://events.nix.ci/stats.php" & readProcessInterleaved_ waiting <- shell (jqBin <> " .evaluator.messages.waiting") & setStdin (byteStringInput stats) & readProcessInterleaved_ & fmap (BSL.readInt >>> fmap fst >>> fromMaybe 0) when (waiting > 2) $ do liftIO $ threadDelay 60000000 untilOfBorgFree assertNotUpdatedOn :: MonadIO m => UpdateEnv -> FilePath -> Text -> ExceptT Text m () assertNotUpdatedOn updateEnv derivationFile branch = do npDir <- liftIO $ Git.nixpkgsDir let Just file = T.stripPrefix (T.pack npDir <> "/") (T.pack derivationFile) derivationContents <- Git.show branch file Nix.assertOldVersionOn updateEnv branch derivationContents addPatched :: Text -> Set CVE -> IO [(CVE, Bool)] addPatched attrPath set = do let list = S.toList set forM list ( \cve -> do patched <- runExceptT $ Nix.hasPatchNamed attrPath (cveID cve) let p = case patched of Left _ -> False Right r -> r return (cve, p) ) cveReport :: UpdateEnv -> IO Text cveReport updateEnv = if not (makeCVEReport . options $ updateEnv) then return "" else withVulnDB $ \conn -> do let pname1 = packageName updateEnv let pname2 = T.replace "-" "_" pname1 oldCVEs1 <- getCVEs conn pname1 (oldVersion updateEnv) oldCVEs2 <- getCVEs conn pname2 (oldVersion updateEnv) let oldCVEs = S.fromList (oldCVEs1 ++ oldCVEs2) newCVEs1 <- getCVEs conn pname1 (newVersion updateEnv) newCVEs2 <- getCVEs conn pname2 (newVersion updateEnv) let newCVEs = S.fromList (newCVEs1 ++ newCVEs2) let inOldButNotNew = S.difference oldCVEs newCVEs inNewButNotOld = S.difference newCVEs oldCVEs inBoth = S.intersection oldCVEs newCVEs ifEmptyNone t = if t == T.empty then "none" else t inOldButNotNew' <- addPatched (packageName updateEnv) inOldButNotNew inNewButNotOld' <- addPatched (packageName updateEnv) inNewButNotOld inBoth' <- addPatched (packageName updateEnv) inBoth let toMkdownList = fmap (uncurry cveLI) >>> T.unlines >>> ifEmptyNone fixedList = toMkdownList inOldButNotNew' newList = toMkdownList inNewButNotOld' unresolvedList = toMkdownList inBoth' if fixedList == "none" && unresolvedList == "none" && newList == "none" then return "" else return [interpolate| ###### Security vulnerability report
Security report (click to expand) CVEs resolved by this update: $fixedList CVEs introduced by this update: $newList CVEs present in both versions: $unresolvedList

|] doCachix :: MonadIO m => (Text -> m ()) -> UpdateEnv -> Text -> ExceptT Text m Text doCachix log updateEnv resultPath = let o = options updateEnv in if batchUpdate o && "r-ryantm" == (GH.untagName $ githubUser o) then do return [interpolate| Either **download from Cachix**: ``` nix-store -r $resultPath \ --option binary-caches 'https://cache.nixos.org/ https://nix-community.cachix.org/' \ --option trusted-public-keys ' nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= ' ``` (The Cachix cache is only trusted for this store-path realization.) For the Cachix download to work, your user must be in the `trusted-users` list or you can use `sudo` since root is effectively trusted. Or, **build yourself**: |] else do lift $ log "skipping cachix" return "Build yourself:" updatePackage :: Options -> Text -> IO (Either Text ()) updatePackage o updateInfo = do let (p, oldV, newV, url) = head (rights (parseUpdates updateInfo)) let updateEnv = UpdateEnv p oldV newV url o let log = T.putStrLn liftIO $ notifyOptions log o twoHoursAgo <- runM $ Time.runIO Time.twoHoursAgo mergeBaseOutpathSet <- liftIO $ newIORef (MergeBaseOutpathsInfo twoHoursAgo S.empty) updatePackageBatch log updateEnv mergeBaseOutpathSet