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
43 ( CommonSetupFlags
(setupVerbosity
)
47 import Distribution
.Package
52 import Distribution
.PackageDescription
56 import Distribution
.Simple
.Flag
(Flag
(..), fromFlagOrDefault
)
57 import Distribution
.Simple
.Utils
62 import Distribution
.Verbosity
65 import Distribution
.Version
67 , simplifyVersionRange
72 import qualified Data
.Map
as Map
74 import Distribution
.Client
.Errors
75 import Distribution
.Simple
.Command
80 freezeCommand
:: CommandUI
(NixStyleFlags
())
83 { commandName
= "v2-freeze"
84 , commandSynopsis
= "Freeze dependencies."
85 , commandUsage
= usageAlternatives
"v2-freeze" ["[FLAGS]"]
86 , commandDescription
= Just
$ \_
->
88 "The project configuration is frozen so that it will be reproducible "
90 ++ "The precise dependency configuration for the project is written to "
91 ++ "the 'cabal.project.freeze' file (or '$project_file.freeze' if "
92 ++ "'--project-file' is specified). This file extends the configuration "
93 ++ "from the 'cabal.project' file and thus is used as the project "
94 ++ "configuration for all other commands (such as 'v2-build', "
95 ++ "'v2-repl' etc).\n\n"
96 ++ "The freeze file can be kept in source control. To make small "
97 ++ "adjustments it may be edited manually, or to make bigger changes "
98 ++ "you may wish to delete the file and re-freeze. For more control, "
99 ++ "one approach is to try variations using 'v2-build --dry-run' with "
100 ++ "solver flags such as '--constraint=\"pkg < 1.2\"' and once you have "
101 ++ "a satisfactory solution to freeze it using the 'v2-freeze' command "
102 ++ "with the same set of flags."
103 , commandNotes
= Just
$ \pname
->
108 ++ " Freeze the configuration of the current project\n\n"
111 ++ " v2-build --dry-run --constraint=\"aeson < 1\"\n"
112 ++ " Check what a solution with the given constraints would look like\n"
115 ++ " v2-freeze --constraint=\"aeson < 1\"\n"
116 ++ " Freeze a solution using the given constraints\n"
117 , commandDefaultFlags
= defaultNixStyleFlags
()
118 , commandOptions
= nixStyleOptions
(const [])
121 -- | To a first approximation, the @freeze@ command runs the first phase of
122 -- the @build@ command where we bring the install plan up to date, and then
123 -- based on the install plan we write out a @cabal.project.freeze@ config file.
125 -- For more details on how this works, see the module
126 -- "Distribution.Client.ProjectOrchestration"
127 freezeAction
:: NixStyleFlags
() -> [String] -> GlobalFlags
-> IO ()
128 freezeAction flags
@NixStyleFlags
{..} extraArgs globalFlags
= do
129 unless (null extraArgs
) $
130 dieWithException verbosity
$
131 FreezeAction extraArgs
140 establishProjectBaseContext verbosity cliConfig OtherCommand
142 (_
, elaboratedPlan
, _
, totalIndexState
, activeRepos
) <-
151 let freezeConfig
= projectFreezeConfig elaboratedPlan totalIndexState activeRepos
153 buildSettingDryRun buildSettings
154 || buildSettingOnlyDownload buildSettings
157 then notice verbosity
"Freeze file not written due to flag(s)"
159 writeProjectLocalFreezeConfig distDirLayout freezeConfig
161 "Wrote freeze file: " ++ (distProjectFile distDirLayout
"freeze")
163 verbosity
= fromFlagOrDefault normal
(setupVerbosity
$ configCommonFlags configFlags
)
165 commandLineFlagsToProjectConfig
168 mempty
-- ClientInstallFlags, not needed here
170 -- | Given the install plan, produce a config value with constraints that
171 -- freezes the versions of packages used in the plan.
173 :: ElaboratedInstallPlan
177 projectFreezeConfig elaboratedPlan totalIndexState activeRepos0
=
179 { projectConfigShared
=
181 { projectConfigConstraints
=
182 concat (Map
.elems (projectFreezeConstraints elaboratedPlan
))
183 , projectConfigIndexState
= Flag totalIndexState
184 , projectConfigActiveRepos
= Flag activeRepos
188 activeRepos
:: ActiveRepos
189 activeRepos
= filterSkippedActiveRepos activeRepos0
191 -- | Given the install plan, produce solver constraints that will ensure the
192 -- solver picks the same solution again in future in different environments.
193 projectFreezeConstraints
194 :: ElaboratedInstallPlan
195 -> Map PackageName
[(UserConstraint
, ConstraintSource
)]
196 projectFreezeConstraints plan
=
198 -- TODO: [required eventually] this is currently an underapproximation
199 -- since the constraints language is not expressive enough to specify the
200 -- precise solution. See https://github.com/haskell/cabal/issues/3502.
202 -- For the moment we deal with multiple versions in the solution by using
203 -- constraints that allow either version. Also, we do not include any
204 -- /version/ constraints for packages that are local to the project (e.g.
205 -- if the solution has two instances of Cabal, one from the local project
206 -- and one pulled in as a setup deps then we exclude all constraints on
207 -- Cabal, not just the constraint for the local instance since any
208 -- constraint would apply to both instances). We do however keep flag
209 -- constraints of local packages.
211 deleteLocalPackagesVersionConstraints
212 (Map
.unionWith
(++) versionConstraints flagConstraints
)
214 versionConstraints
:: Map PackageName
[(UserConstraint
, ConstraintSource
)]
219 ( UserConstraint
(UserAnyQualifier p
) (PackagePropertyVersion v
)
220 , ConstraintSourceFreeze
226 versionRanges
:: Map PackageName VersionRange
228 Map
.map simplifyVersionRange
$
229 Map
.fromListWith unionVersionRanges
$
230 [ (packageName pkg
, thisVersion
(packageVersion pkg
))
231 | InstallPlan
.PreExisting pkg
<- InstallPlan
.toList plan
233 ++ [ (packageName pkg
, thisVersion
(packageVersion pkg
))
234 | InstallPlan
.Configured pkg
<- InstallPlan
.toList plan
237 flagConstraints
:: Map PackageName
[(UserConstraint
, ConstraintSource
)]
242 ( UserConstraint
(UserQualified UserQualToplevel p
) (PackagePropertyFlags f
)
243 , ConstraintSourceFreeze
249 flagAssignments
:: Map PackageName FlagAssignment
253 | InstallPlan
.Configured elab
<- InstallPlan
.toList plan
254 , let flags
= elabFlagAssignment elab
255 pkgname
= packageName elab
256 , not (nullFlagAssignment flags
)
259 -- As described above, remove the version constraints on local packages,
260 -- but leave any flag constraints.
261 deleteLocalPackagesVersionConstraints
262 :: Map PackageName
[(UserConstraint
, ConstraintSource
)]
263 -> Map PackageName
[(UserConstraint
, ConstraintSource
)]
264 deleteLocalPackagesVersionConstraints
=
266 ( \_pkgname
() constraints
->
267 case filter (not . isVersionConstraint
. fst) constraints
of
269 constraints
' -> Just constraints
'
275 isVersionConstraint
(UserConstraint _
(PackagePropertyVersion _
)) = True
276 isVersionConstraint _
= False
278 localPackages
:: Map PackageName
()
281 [ (packageName elab
, ())
282 | InstallPlan
.Configured elab
<- InstallPlan
.toList plan
283 , elabLocalToProject elab