1 {-# LANGUAGE NamedFieldPuns #-}
2 {-# LANGUAGE RecordWildCards #-}
4 -- | cabal-install CLI command: freeze
5 module Distribution
.Client
.CmdFreeze
10 import Distribution
.Client
.Compat
.Prelude
13 import Distribution
.Client
.DistDirLayout
14 ( DistDirLayout
(distProjectFile
)
16 import Distribution
.Client
.IndexUtils
(ActiveRepos
, TotalIndexState
, filterSkippedActiveRepos
)
17 import qualified Distribution
.Client
.InstallPlan
as InstallPlan
18 import Distribution
.Client
.NixStyleOptions
20 , defaultNixStyleFlags
23 import Distribution
.Client
.ProjectConfig
25 , ProjectConfigShared
(..)
26 , writeProjectLocalFreezeConfig
28 import Distribution
.Client
.ProjectOrchestration
29 import Distribution
.Client
.ProjectPlanning
30 import Distribution
.Client
.Targets
32 , UserConstraintScope
(..)
35 import Distribution
.Solver
.Types
.ConstraintSource
36 ( ConstraintSource
(..)
38 import Distribution
.Solver
.Types
.PackageConstraint
39 ( PackageProperty
(..)
42 import Distribution
.Client
.Setup
46 import Distribution
.Package
51 import Distribution
.PackageDescription
55 import Distribution
.Simple
.Flag
(Flag
(..), fromFlagOrDefault
)
56 import Distribution
.Simple
.Utils
61 import Distribution
.Verbosity
64 import Distribution
.Version
66 , simplifyVersionRange
71 import qualified Data
.Map
as Map
73 import Distribution
.Client
.Errors
74 import Distribution
.Simple
.Command
79 freezeCommand
:: CommandUI
(NixStyleFlags
())
82 { commandName
= "v2-freeze"
83 , commandSynopsis
= "Freeze dependencies."
84 , commandUsage
= usageAlternatives
"v2-freeze" ["[FLAGS]"]
85 , commandDescription
= Just
$ \_
->
87 "The project configuration is frozen so that it will be reproducible "
89 ++ "The precise dependency configuration for the project is written to "
90 ++ "the 'cabal.project.freeze' file (or '$project_file.freeze' if "
91 ++ "'--project-file' is specified). This file extends the configuration "
92 ++ "from the 'cabal.project' file and thus is used as the project "
93 ++ "configuration for all other commands (such as 'v2-build', "
94 ++ "'v2-repl' etc).\n\n"
95 ++ "The freeze file can be kept in source control. To make small "
96 ++ "adjustments it may be edited manually, or to make bigger changes "
97 ++ "you may wish to delete the file and re-freeze. For more control, "
98 ++ "one approach is to try variations using 'v2-build --dry-run' with "
99 ++ "solver flags such as '--constraint=\"pkg < 1.2\"' and once you have "
100 ++ "a satisfactory solution to freeze it using the 'v2-freeze' command "
101 ++ "with the same set of flags."
102 , commandNotes
= Just
$ \pname
->
107 ++ " Freeze the configuration of the current project\n\n"
110 ++ " v2-build --dry-run --constraint=\"aeson < 1\"\n"
111 ++ " Check what a solution with the given constraints would look like\n"
114 ++ " v2-freeze --constraint=\"aeson < 1\"\n"
115 ++ " Freeze a solution using the given constraints\n"
116 , commandDefaultFlags
= defaultNixStyleFlags
()
117 , commandOptions
= nixStyleOptions
(const [])
120 -- | To a first approximation, the @freeze@ command runs the first phase of
121 -- the @build@ command where we bring the install plan up to date, and then
122 -- based on the install plan we write out a @cabal.project.freeze@ config file.
124 -- For more details on how this works, see the module
125 -- "Distribution.Client.ProjectOrchestration"
126 freezeAction
:: NixStyleFlags
() -> [String] -> GlobalFlags
-> IO ()
127 freezeAction flags
@NixStyleFlags
{..} extraArgs globalFlags
= do
128 unless (null extraArgs
) $
129 dieWithException verbosity
$
130 FreezeAction extraArgs
139 establishProjectBaseContext verbosity cliConfig OtherCommand
141 (_
, elaboratedPlan
, _
, totalIndexState
, activeRepos
) <-
150 let freezeConfig
= projectFreezeConfig elaboratedPlan totalIndexState activeRepos
152 buildSettingDryRun buildSettings
153 || buildSettingOnlyDownload buildSettings
156 then notice verbosity
"Freeze file not written due to flag(s)"
158 writeProjectLocalFreezeConfig distDirLayout freezeConfig
160 "Wrote freeze file: " ++ distProjectFile distDirLayout
"freeze"
162 verbosity
= fromFlagOrDefault normal
(configVerbosity configFlags
)
164 commandLineFlagsToProjectConfig
167 mempty
-- ClientInstallFlags, not needed here
169 -- | Given the install plan, produce a config value with constraints that
170 -- freezes the versions of packages used in the plan.
172 :: ElaboratedInstallPlan
176 projectFreezeConfig elaboratedPlan totalIndexState activeRepos0
=
178 { projectConfigShared
=
180 { projectConfigConstraints
=
181 concat (Map
.elems (projectFreezeConstraints elaboratedPlan
))
182 , projectConfigIndexState
= Flag totalIndexState
183 , projectConfigActiveRepos
= Flag activeRepos
187 activeRepos
:: ActiveRepos
188 activeRepos
= filterSkippedActiveRepos activeRepos0
190 -- | Given the install plan, produce solver constraints that will ensure the
191 -- solver picks the same solution again in future in different environments.
192 projectFreezeConstraints
193 :: ElaboratedInstallPlan
194 -> Map PackageName
[(UserConstraint
, ConstraintSource
)]
195 projectFreezeConstraints plan
=
197 -- TODO: [required eventually] this is currently an underapproximation
198 -- since the constraints language is not expressive enough to specify the
199 -- precise solution. See https://github.com/haskell/cabal/issues/3502.
201 -- For the moment we deal with multiple versions in the solution by using
202 -- constraints that allow either version. Also, we do not include any
203 -- /version/ constraints for packages that are local to the project (e.g.
204 -- if the solution has two instances of Cabal, one from the local project
205 -- and one pulled in as a setup deps then we exclude all constraints on
206 -- Cabal, not just the constraint for the local instance since any
207 -- constraint would apply to both instances). We do however keep flag
208 -- constraints of local packages.
210 deleteLocalPackagesVersionConstraints
211 (Map
.unionWith
(++) versionConstraints flagConstraints
)
213 versionConstraints
:: Map PackageName
[(UserConstraint
, ConstraintSource
)]
218 ( UserConstraint
(UserAnyQualifier p
) (PackagePropertyVersion v
)
219 , ConstraintSourceFreeze
225 versionRanges
:: Map PackageName VersionRange
227 Map
.map simplifyVersionRange
$
228 Map
.fromListWith unionVersionRanges
$
229 [ (packageName pkg
, thisVersion
(packageVersion pkg
))
230 | InstallPlan
.PreExisting pkg
<- InstallPlan
.toList plan
232 ++ [ (packageName pkg
, thisVersion
(packageVersion pkg
))
233 | InstallPlan
.Configured pkg
<- InstallPlan
.toList plan
236 flagConstraints
:: Map PackageName
[(UserConstraint
, ConstraintSource
)]
241 ( UserConstraint
(UserQualified UserQualToplevel p
) (PackagePropertyFlags f
)
242 , ConstraintSourceFreeze
248 flagAssignments
:: Map PackageName FlagAssignment
252 | InstallPlan
.Configured elab
<- InstallPlan
.toList plan
253 , let flags
= elabFlagAssignment elab
254 pkgname
= packageName elab
255 , not (nullFlagAssignment flags
)
258 -- As described above, remove the version constraints on local packages,
259 -- but leave any flag constraints.
260 deleteLocalPackagesVersionConstraints
261 :: Map PackageName
[(UserConstraint
, ConstraintSource
)]
262 -> Map PackageName
[(UserConstraint
, ConstraintSource
)]
263 deleteLocalPackagesVersionConstraints
=
265 ( \_pkgname
() constraints
->
266 case filter (not . isVersionConstraint
. fst) constraints
of
268 constraints
' -> Just constraints
'
274 isVersionConstraint
(UserConstraint _
(PackagePropertyVersion _
)) = True
275 isVersionConstraint _
= False
277 localPackages
:: Map PackageName
()
280 [ (packageName elab
, ())
281 | InstallPlan
.Configured elab
<- InstallPlan
.toList plan
282 , elabLocalToProject elab