packages feed

hix-0.9.0: lib/Hix/Managed/Bump/Optimize.hs

module Hix.Managed.Bump.Optimize where

import Exon (exon)

import Hix.Class.Map (nMap, nTransformMaybe, (!?))
import Hix.Data.Bounds (Ranges)
import Hix.Data.Monad (M)
import qualified Hix.Data.Overrides
import Hix.Data.VersionBounds (fromLower, majorRange)
import Hix.Managed.Build (processQuery)
import Hix.Managed.Bump.Candidates (candidatesBump)
import qualified Hix.Managed.Cabal.Data.SolverState
import Hix.Managed.Cabal.Data.SolverState (SolverFlags (SolverFlags), solverState)
import Hix.Managed.Constraints (fromVersions, preferInstalled, preferRanges, preferVersions)
import qualified Hix.Managed.Data.BuildConfig
import Hix.Managed.Data.BuildConfig (BuildConfig)
import Hix.Managed.Data.Bump (Bump (..))
import Hix.Managed.Data.Constraints (EnvConstraints)
import qualified Hix.Managed.Data.EnvContext
import Hix.Managed.Data.EnvContext (EnvDeps)
import Hix.Managed.Data.Initial (Initial (Initial))
import Hix.Managed.Data.Mutable (MutableBounds, MutableDep, MutableVersions, depName)
import qualified Hix.Managed.Data.MutableId
import Hix.Managed.Data.MutableId (MutableId (MutableId))
import Hix.Managed.Data.Mutation (DepMutation (..))
import qualified Hix.Managed.Data.MutationState
import Hix.Managed.Data.MutationState (MutationState)
import Hix.Managed.Data.ProjectContext (ProjectContext)
import Hix.Managed.Data.ProjectResult (ProjectResult)
import Hix.Managed.Data.Query (Query (Query))
import qualified Hix.Managed.Data.StageContext
import Hix.Managed.Data.StageContext (StageContext (StageContext))
import Hix.Managed.Data.StageResult (StageResult, StageSummary (StageNoAction, StageReport))
import Hix.Managed.Data.StageState (BuildStatus, BuildSuccess)
import Hix.Managed.Flow (Flow, execStage, execStatelessStage)
import qualified Hix.Managed.Handlers.Build
import Hix.Managed.Handlers.Build (BuildHandlers)
import Hix.Managed.Handlers.Cabal (CabalHandlers, installedVersions)
import Hix.Managed.Handlers.Mutation.Bump (handlersBump)
import Hix.Managed.Process (processProjectSimple)
import Hix.Managed.Report (describeIterations)
import Hix.Managed.StageResult (stageResult)

-- TODO this is still janky because it uses reified bound changes (@initialBounds@), which means a loss of precision.
-- If the user specified <=1.2.3, we end up with <1.2.3.
-- Better to drag the actual ranges through from the start.
stateBoundsForNonInstalled :: MutableBounds -> MutableVersions -> Ranges
stateBoundsForNonInstalled initialBounds =
  nTransformMaybe \ package -> \case
    Nothing -> do
      bounds <- initialBounds !? package
      pure (depName package, majorRange bounds)
    Just _ -> Nothing

-- | Solver params for Bump consist of, in decreasing order of precedence:
--
-- - User-specified bounds (added by the constructor 'solverState').
--
-- - The existing overrides from previous runs, used as preferred versions.
--   This ensures that the solver selects the same versions as in the last run while still allowing newer versions for
--   transitive dependencies that are later in the candidate list.
--
-- - Installed versions from the package db, used as lower bounds.
--
-- - Initial bounds from the state (and potentially from the user config if @--read-upper-bounds@ was used) for packages
--   without an installed version.
--   This applies to dependencies on local packages that have not been published to central Hackage (but have versions
--   available on a custom Hackage) and pulled into the project's nixpkgs, and which are targets of a separate managed
--   env (which means that they are not included in the env's GHC package set as local derivations like targets are).
--   If this was the first run, so that no previously determined overrides are present in the state, then these deps
--   would not have installed versions that could be preferred for stability.
--   While the user could specify solver bounds to mitigate this, it's likely that an existing regular bound will be the
--   safest bet.
--   Otherwise the solver would choose the latest version from the custom Hackage, which may mean breakage.
--
-- - The preference for using the installed version for mutable deps, which is a fallback for packages without overrides
--   (since their chosen versions are installed).
bumpSolverParams ::
  EnvDeps ->
  CabalHandlers ->
  Initial MutationState ->
  MutableBounds ->
  EnvConstraints
bumpSolverParams deps cabal (Initial state) initialBounds =
  preferVersions (nMap (.version) state.overrides) <>
  fromVersions fromLower installed <>
  preferRanges (stateBoundsForNonInstalled initialBounds installed) <>
  preferInstalled deps.mutable
  where
    installed = installedVersions cabal deps.mutable

success :: Map MutableDep BuildSuccess -> Word -> Text
success _ iterations =
  [exon|Found working latest versions for all deps after #{iter}.|]
  where
    iter = describeIterations iterations

failure :: Word -> Text
failure iterations =
  [exon|Couldn't find working latest versions for some deps after #{describeIterations iterations}.|]

bumpBuild ::
  BuildHandlers ->
  BuildConfig ->
  StageContext ->
  M StageResult
bumpBuild handlers conf stage@StageContext {env, builder, state, initialBounds} = do
  result <- processQuery (candidatesBump handlers) handlersBump conf stage ext
  pure (stageResult success failure result)
  where
    ext = solverState env.solverBounds env.deps (bumpSolverParams env.deps builder.cabal state initialBounds) flags
    flags = SolverFlags {allowNewer = True, forceRevisions = False}

bumpBuildStage ::
  BuildHandlers ->
  BuildConfig ->
  Flow BuildStatus
bumpBuildStage handlers conf =
  execStage "bump" (bumpBuild handlers conf)

bumpReportStage ::
  BuildHandlers ->
  Flow BuildStatus
bumpReportStage handlers =
  execStatelessStage "bump-report" \ StageContext {query = Query query} ->
    toSummary . toCandidates <$> traverse (candidatesBump handlers) (toList query)
  where
    toSummary = \case
      Just cs -> StageReport "Found new versions:" cs
      Nothing -> StageNoAction (Just "All dependencies are up to date.")

    toCandidates = nonEmpty . fmap toCandidate . catMaybes

    toCandidate DepMutation {package = name, mutation = Bump {version}} =
      MutableId {..}

bumpStages ::
  BuildHandlers ->
  BuildConfig ->
  Flow BuildStatus
bumpStages handlers conf
  | conf.lookup
  = bumpReportStage handlers
  | otherwise
  = bumpBuildStage handlers conf

bumpOptimizeMain :: BuildHandlers -> ProjectContext -> M ProjectResult
bumpOptimizeMain = processProjectSimple bumpStages