Skip to content

Don't re-run the solver unnecessarily (#12119) - #12227

Draft
tdammers wants to merge 17 commits into
haskell:masterfrom
tdammers:wip/tobias-12119
Draft

tdammers wants to merge 17 commits into
haskell:masterfrom
tdammers:wip/tobias-12119

Conversation

@tdammers

Copy link
Copy Markdown
Collaborator

Previously, the solver would be re-run whenever anything about the package description changed. This changes that so that only the relevant bits (roughly: the information found in PInfo) is taken into account, ignoring other changes to the package description.

@sheaf

sheaf commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

I think the problem is that Distribution.Solver.Modular.modularResolver.postprocess re-injects a bunch of outside information into what the solver has computed beyond the input we use as the recompilation cache key. Specifically, Distribution.Solver.Modular.ConfiguredConversion.convCP re-injects things like SourcePackage which leaks things like local directory names etc.

I think we need to be slightly more ambitious in how we update Distribution.Client.ProjectPlanning.rebuildInstallPlan.phaseRunSolver (man aren't these a mouthful).

Here is a possible roadmap (still very sketchy):

  • remove precisely depResolverInstalledPkgIndex and depResolverSourcePkgIndex from DepResolverParams, and add on the local packages but as [SourcePackage ()] so that it contains no filesystem-local information
  • update resolveDependencies so that it takes the two fields you removed from DepResolverParams as extra arguments
  • update phaseRunSolver so that it uses rerunIfChanged on (platform, compilerInfo, depResolverParams) and move it to a top-level function so that it can't possibly capture other local data that it isn't supposed to.
  • The convCP logic that re-injects data moves to the outside of the solver's rerunIfChanged.

So the code in rebuildInstallPlan around the call to phaseRunSolver would become something like:

(plan0, ...) <- phaseRunSolver solverEnv depResolverParams -- has the rerunIfChanged described above
let solverPlan = addSourceLocations ... plan0 
(elaboratedPlan, elaboratedShared) <-
  phaseElaboratePlan projectConfig compilerEtc pkgConfigDB solverPlan localPackages

where addSourceLocations is a new function that does the "add back source locations" parts of convCP.

@sheaf

sheaf commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

I think the easiest way forward would be to introduce a new datatype for the solver inputs without changing anything else, as a first step. Then we can think about which datatypes can be removed later.

------------------------------
-- cabal-install-solver 

-- Copy of PackageDescription based on what `convGPD` reads off it,
-- replacing 'BuildInfo' with a separate new 'SolverComponent'.
data SolverPackageDescription = SolverPackageDescription
  { spdPackageId      :: PackageId
  , spdSpecVersion    :: CabalSpecVersion
  , spdScannedVersion :: Maybe Version
  , spdBuildType      :: BuildType
  , spdSetupBuildInfo :: Maybe SetupBuildInfo
  , spdFlags          :: [PackageFlag]
  , spdLibrary        :: Maybe (CondTree ConfVar [Dependency] SolverComponent)
  , spdComponents :: [(ComponentName, CondTree ConfVar [Dependency] SolverComponent)]
  }

data SolverComponent = SolverComponent
  { scBuildable :: Bool, scVisibility :: LibraryVisibility
  , scExtensions :: [Extension], scLanguages :: [Language]
  , scPkgconfigDeps :: [PkgconfigDependency]
  , scToolDeps :: [ExeDependency] }

projectOutSolverPackageDescription :: SourcePackage loc -> SolverPackageDescription

---------------------------------
-- cabal-install

-- The recompilation cache key (input to `solveProject`)
data SolverInputs = SolverInputs
  { siCompiler        :: Compiler
  , siPlatform        :: Platform
  , siPackageDbs      :: PackageDBStackCWD
  , siRepoConfig      :: SolverRepoConfig 
  , siSettings        :: SolverSettings
  , siProjectPackages :: [PackageSpecifier SolverPackageDescription]
  , siEnabledStanzas  :: Map PackageName (Map OptionalStanza Bool)
  , siExtraInstalled  :: InstalledPackageIndex
  }

and then update convSP, convGPD, condCondTree, condBranch to use these types instead.

For the return type of the solver, delete ResolverPackage, SolverPackage, InstSolverPackage and DependencyResolver, replacing them by introducing

---------------------------
-- cabal-install-solver

data SolverPlanPackage ipkg srcpkg
  = PreExisting PackageId UnitId ipkg SolverDeps
  | Configured  PackageId srcpkg FlagAssignment OptionalStanzaSet SolverDeps

Then generalise SolverInstallPlan to GenericSolverPlan ipkg srcpkg, defining type SolverInstallPlan = GenericSolverPlan InstalledPackageInfo UnresolvedSourcePackage.

----------------------
-- cabal-install
data SolverOutput = SolverOutput
  { soPlan         :: GenericSolverPlan () ()
  , soInstalled    :: InstalledPackageIndex       -- only units the plan references
  , soRepoPackages :: [UnresolvedSourcePackage]   -- only non-project packages the plan references
  , soPkgConfigDb  :: Maybe PkgConfigDb
  , soIndexState   :: TotalIndexState, soActiveRepos :: ActiveRepos }

Then:

  • The solver returns GenericSolverPlan () (), and that's what gets cached.
  • convCP becomes a pure function which adds back on the InstalledPackageInfo and UnresolvedSourcePackage from the outside.

I retract any prior suggestions to use DepResolverParams directly as the cache key because making that actually work brings in unneeded changes (e.g. figuring out what to do with goalOrder, basicInstallPolicy etc).

zlonast and others added 14 commits October 12, 2026 15:32
We now don't store the specific Cabal library version used to provide
the Setup CLI, which allows us to re-use an install plan made with
another Cabal library version that is still compatible (e.g. across a
minor version bump).

Fixes haskell#11416
`dependOnWiredIns` adds two kinds of constraint: one installed-unit-id
constraint per wired-in unit of the compiler, and one `base >= 4.22`
version constraint.

The second only makes sense next to the first. The unit-id constraints
pin the new, reinstallable `base`; the version constraint then excludes
the old, non-reinstallable one. But it was added unconditionally, while
the unit-id constraints are empty for a compiler that reports no wired-in
units. Such a compiler has no installed `base` satisfying the bound, so
plans that reach this code fail, and `--allow-boot-library-installs`
alone is enough to reach it.

Fixes haskell#12328.
This commit includes a series of refactorings that are necessary to
solve haskell#11411:

- Abstract the Rebuild monad into the MonadRebuild typeclass,
  introducing the `withRunRebuildInIO` method of MonadRebuild. This
  allows us to interleave plain `IO` with `MonadRebuild` without
  necessarily having to use `Rebuild` itself. This is especially
  important because `Rebuild` also encodes the "current working
  directory" concern, which is relevant within `cabal-install`, but
  something we don't want to require within the `Cabal` library. Since
  the `MonadRebuild` typeclass allows for arbitrary custom monadic state
  per instance, it can live in the `Cabal` subproject, together with
  operations defined in terms of the typeclass, while `Rebuild` itself
  remains in `cabal-install`, using the custom monadic state to cover
  the "current working directory" concern.
- Change the `StateT` part of `Rebuild` into `WriterT`, since we only
  ever emit additional files from within a Rebuild context; consuming
  them happens on the outside, so Writer is sufficient.
- Provide `MonadRebuild` instances for the existing `Rebuild` type, and
  for the most minimal type that can satisfy the typeclass, `WriterT
  [MonitorFilePath] IO`.
- Implement most file monitoring / Rebuild actions in terms of
  `MonadRebuild`, so they can live in `Cabal`
  (`Distribution.Simple.RebuildMonad`), leaving only
  `cabal-install` specific things in the existing `cabal-install`
  module (`Distribution.Client.RebuildMonad`). Specifically, we
  implement `rerunIfChanged'` in terms of `MonadRebuild` and a custom
  function for chaining `IO` actions (serially or in parallel, as
  needed); the existing `rerunIfChanged` and
  `rerunConcurrentlyIfChanged` functions, which live in `cabal-install`,
  are now implemented in terms of `rerunIfChanged'`, which lives in
  `Cabal`).
- Move large parts of the `Distribution.Client.FileMonitor` module from
  `cabal-install` to `Cabal` (as `Distribution.Simple.FileMonitor`);
  we will be needing these parts to solve haskell#11411
  (`Distribution.Client.FileMonitor` still re-exports all of them
  though).
- Move most of the `Distribution.Client.HashValue` module over to
  `Cabal` (with the exception of TUF-related functions, which are
  specific to `cabal-install`; again we re-export everything in
  `cabal-install` to maintain API compatibility).
- Move `getFilePathRootDirectory` from `Distribution.Client.Glob` into
  `Distribution.Simple.FileMonitor.Types` - it has little to do with
  actual globs, and is only used for file monitoring purposes.
- Shuffle some utility functions around to make them accessible where we
  need them.

The big picture with all this is that we want to reuse the existing file
monitoring machinery to keep track of build-tool versions alongside
generated source files. However, the tracking needs to happen in the
`Cabal` library, while file monitoring has so far only been available in
`cabal-install`; hence we need to move the required file monitoring code
and its dependencies into the `Cabal` library, but without introducing
any `cabal-install` specific concerns into `Cabal`, and without breaking
API compatibility on the `cabal-install` side.
This replaces the previous preprocessor re-run logic ("if the
preprocessor output exists and is newer than the input, don't re-run the
preprocessor") with a file monitor, which takes both the input file and
the preprocessor itself into account, solving haskell#11411.

To make this possible, each preprocessor is now tagged with a hash that
uniquely identifies the specific preprocessor version.

The process of finding the actual preprocessor (`runPreProcessor`) to
run has been split off from the actual running; this was necessary so
that we can get a unique hash of the preprocessor (including version)
without having to actually run it, while also avoiding duplicate work
and a potential mismatch between the hash and the actual preprocessor.
Hence, we now have `configurePreProcessor`, which returns a pair of the
former `runPreProcessor` action and the preprocessor hash.

Other changes:

- Use GHC.Fingerprint instead of SHA256 in FileMonitor (avoiding a
  dependency on cryptohash-sha256, which is not a boot library).
- Clean up the ProgramDb API, so that we can be explicit about whether
  we want to configure preprocessors on the fly as needed or not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants