diff --git a/cabal-install/src/Distribution/Client/CmdFreeze.hs b/cabal-install/src/Distribution/Client/CmdFreeze.hs index 29718b5d441..5122f782f41 100644 --- a/cabal-install/src/Distribution/Client/CmdFreeze.hs +++ b/cabal-install/src/Distribution/Client/CmdFreeze.hs @@ -5,6 +5,7 @@ module Distribution.Client.CmdFreeze ( freezeCommand , freezeAction + , ClientFreezeFlags (..) ) where import Distribution.Client.Compat.Prelude @@ -27,23 +28,16 @@ import Distribution.Client.ProjectConfig ) import Distribution.Client.ProjectOrchestration import Distribution.Client.ProjectPlanning -import Distribution.Client.Targets - ( UserConstraint (..) - , UserConstraintScope (..) - , UserQualifier (..) - ) -import Distribution.Solver.Types.ConstraintSource - ( ConstraintSource (..) - ) -import Distribution.Solver.Types.PackageConstraint - ( PackageProperty (..) - ) - import Distribution.Client.Setup ( CommonSetupFlags (setupVerbosity) , ConfigFlags (..) , GlobalFlags ) +import Distribution.Client.Targets + ( UserConstraint (..) + , UserConstraintScope (..) + , UserQualifier (..) + ) import Distribution.Package ( PackageName , packageName @@ -54,11 +48,19 @@ import Distribution.PackageDescription , nullFlagAssignment ) import Distribution.Simple.Flag (Flag (..), fromFlagOrDefault) +import Distribution.Simple.Setup (trueArg) import Distribution.Simple.Utils ( dieWithException , notice , wrapText ) +import Distribution.Solver.Types.ConstraintSource + ( ConstraintSource (..) + ) +import Distribution.Solver.Types.PackageConstraint + ( PackageProperty (..) + ) +import Distribution.Solver.Types.Settings (OnlyConstrained (..)) import Distribution.Verbosity ( normal ) @@ -74,10 +76,15 @@ import qualified Data.Map as Map import Distribution.Client.Errors import Distribution.Simple.Command ( CommandUI (..) + , OptionField + , ShowOrParseArgs + , option , usageAlternatives ) -freezeCommand :: CommandUI (NixStyleFlags ()) +newtype ClientFreezeFlags = ClientFreezeFlags {lockDependencies :: Flag Bool} + +freezeCommand :: CommandUI (NixStyleFlags ClientFreezeFlags) freezeCommand = CommandUI { commandName = "v2-freeze" @@ -99,7 +106,10 @@ freezeCommand = ++ "one approach is to try variations using 'v2-build --dry-run' with " ++ "solver flags such as '--constraint=\"pkg < 1.2\"' and once you have " ++ "a satisfactory solution to freeze it using the 'v2-freeze' command " - ++ "with the same set of flags." + ++ "with the same set of flags.\n\n" + ++ "By default, a freeze file is a set of constaints; unconstrained packages " + ++ "can still be included in the build plan. If you wish to restrict dependencies " + ++ "to those included in the freeze file, use the '--lock' flag." , commandNotes = Just $ \pname -> "Examples:\n" ++ " " @@ -108,23 +118,38 @@ freezeCommand = ++ " Freeze the configuration of the current project\n\n" ++ " " ++ pname + ++ " v2-freeze --lock\n" + ++ " Freeze the configuration of the current project and only allow frozen dependencies in future builds\n\n" + ++ " " + ++ pname ++ " v2-build --dry-run --constraint=\"aeson < 1\"\n" ++ " Check what a solution with the given constraints would look like\n" ++ " " ++ pname ++ " v2-freeze --constraint=\"aeson < 1\"\n" ++ " Freeze a solution using the given constraints\n" - , commandDefaultFlags = defaultNixStyleFlags () - , commandOptions = nixStyleOptions (const []) + , commandDefaultFlags = defaultNixStyleFlags (ClientFreezeFlags (Flag False)) + , commandOptions = nixStyleOptions freezeOptions } +freezeOptions :: ShowOrParseArgs -> [OptionField ClientFreezeFlags] +freezeOptions _ = + [ option + [] + ["lock"] + "Promote the resulting freeze file to a lock file" + lockDependencies + (\v f -> f{lockDependencies = v}) + trueArg + ] + -- | To a first approximation, the @freeze@ command runs the first phase of -- the @build@ command where we bring the install plan up to date, and then -- based on the install plan we write out a @cabal.project.freeze@ config file. -- -- For more details on how this works, see the module -- "Distribution.Client.ProjectOrchestration" -freezeAction :: NixStyleFlags () -> [String] -> GlobalFlags -> IO () +freezeAction :: NixStyleFlags ClientFreezeFlags -> [String] -> GlobalFlags -> IO () freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do unless (null extraArgs) $ dieWithException verbosity $ @@ -148,7 +173,7 @@ freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do localPackages Nothing - let freezeConfig = projectFreezeConfig elaboratedPlan totalIndexState activeRepos + let freezeConfig = projectFreezeConfig extraFlags elaboratedPlan totalIndexState activeRepos dryRun = buildSettingDryRun buildSettings || buildSettingOnlyDownload buildSettings @@ -170,11 +195,12 @@ freezeAction flags@NixStyleFlags{..} extraArgs globalFlags = do -- | Given the install plan, produce a config value with constraints that -- freezes the versions of packages used in the plan. projectFreezeConfig - :: ElaboratedInstallPlan + :: ClientFreezeFlags + -> ElaboratedInstallPlan -> TotalIndexState -> ActiveRepos -> ProjectConfig -projectFreezeConfig elaboratedPlan totalIndexState activeRepos0 = +projectFreezeConfig freezeFlags elaboratedPlan totalIndexState activeRepos0 = mempty { projectConfigShared = mempty @@ -182,12 +208,17 @@ projectFreezeConfig elaboratedPlan totalIndexState activeRepos0 = concat (Map.elems (projectFreezeConstraints elaboratedPlan)) , projectConfigIndexState = Flag totalIndexState , projectConfigActiveRepos = Flag activeRepos + , projectConfigOnlyConstrained = onlyConstrainedFlag freezeFlags } } where activeRepos :: ActiveRepos activeRepos = filterSkippedActiveRepos activeRepos0 + onlyConstrainedFlag :: ClientFreezeFlags -> Flag OnlyConstrained + onlyConstrainedFlag ClientFreezeFlags{lockDependencies = Flag True} = Flag OnlyConstrainedAll + onlyConstrainedFlag ClientFreezeFlags{lockDependencies = _} = NoFlag + -- | Given the install plan, produce solver constraints that will ensure the -- solver picks the same solution again in future in different environments. projectFreezeConstraints diff --git a/cabal-testsuite/PackageTests/Freeze/freeze-lock.out b/cabal-testsuite/PackageTests/Freeze/freeze-lock.out new file mode 100644 index 00000000000..e330b8a640b --- /dev/null +++ b/cabal-testsuite/PackageTests/Freeze/freeze-lock.out @@ -0,0 +1,5 @@ +# cabal v2-update +Downloading the latest package list from test-local-repo +# cabal v2-freeze +Resolving dependencies... +Wrote freeze file: /cabal.project.freeze diff --git a/cabal-testsuite/PackageTests/Freeze/freeze-lock.test.hs b/cabal-testsuite/PackageTests/Freeze/freeze-lock.test.hs new file mode 100644 index 00000000000..54c27258b83 --- /dev/null +++ b/cabal-testsuite/PackageTests/Freeze/freeze-lock.test.hs @@ -0,0 +1,6 @@ +import Test.Cabal.Prelude +main = cabalTest $ do + withRepo "repo" $ do + cabal "v2-freeze" ["--lock"] + cwd <- fmap testCurrentDir getTestEnv + assertFileDoesContain (cwd "cabal.project.freeze") "reject-unconstrained-dependencies: all" diff --git a/changelog.d/pr-10785.md b/changelog.d/pr-10785.md new file mode 100644 index 00000000000..e9ca4290b23 --- /dev/null +++ b/changelog.d/pr-10785.md @@ -0,0 +1,10 @@ +--- +synopsis: Added a `--lock` flag to `cabal freeze` to promote a freeze file to a lock file +packages: [cabal-install] +prs: 10785 +issues: 10784 +--- + +Added a `--lock` flag to `cabal freeze`, to promote a freeze file to a lock file. By calling `cabal freeze --lock`, the resulting freeze file will ensure that only dependencies whose constraints are specified, will be accepted by future build plans. This flag can be used to ensure that no unaudited packages are added to the build plan. + +This new `--lock` flag reuses the mechanism behind `--reject-unconstrained-dependencies`, by writing the equivalent of `--reject-unconstrained-dependencies=all` to the freeze file. diff --git a/doc/cabal-commands.rst b/doc/cabal-commands.rst index 7f0b37ac48f..9c0ffdd5b7a 100644 --- a/doc/cabal-commands.rst +++ b/doc/cabal-commands.rst @@ -532,6 +532,16 @@ users see a consistent set of dependencies. For libraries, this is not recommended: users often need to build against different versions of libraries than what you developed against. +A freeze file is really a set of constraint; by default, such files do not +prevent new dependencies from being included in the build plan. In this sense, +a freeze file is not, by default, a **lockfile**. To turn a freeze file into a lockfile, +use the ``--lock`` flag when invocating ``cabal freeze``. This will prevent future +builds from including new dependencies. This can be helpful in situations where +every dependency must be explicitly audited and approved, for example. +Under the hood, the ``--lock`` flag reuses the mechanism behind +``--reject-unconstrained-dependencies``, by writing the equivalent of +``--reject-unconstrained-dependencies=all`` to the freeze file. + cabal gen-bounds ^^^^^^^^^^^^^^^^