Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New freeze command #3503

Merged
merged 4 commits into from
Jun 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions cabal-install/Distribution/Client/CmdFreeze.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{-# LANGUAGE NamedFieldPuns, RecordWildCards #-}

-- | cabal-install CLI command: freeze
--
module Distribution.Client.CmdFreeze (
freezeAction,
) where

import Distribution.Client.ProjectPlanning
( ElaboratedInstallPlan, rebuildInstallPlan )
import Distribution.Client.ProjectConfig
( ProjectConfig(..), ProjectConfigShared(..)
, commandLineFlagsToProjectConfig, writeProjectLocalFreezeConfig
, findProjectRoot )
import Distribution.Client.ProjectPlanning.Types
( ElaboratedConfiguredPackage(..) )
import Distribution.Client.Targets
( UserConstraint(..) )
import Distribution.Solver.Types.ConstraintSource
( ConstraintSource(..) )
import Distribution.Client.DistDirLayout
( defaultDistDirLayout, defaultCabalDirLayout )
import Distribution.Client.Config
( defaultCabalDir )
import qualified Distribution.Client.InstallPlan as InstallPlan


import Distribution.Package
( PackageName, packageName, packageVersion )
import Distribution.Version
( VersionRange, thisVersion, unionVersionRanges )
import Distribution.PackageDescription
( FlagAssignment )
import Distribution.Client.Setup
( GlobalFlags, ConfigFlags(..), ConfigExFlags, InstallFlags )
import Distribution.Simple.Setup
( HaddockFlags, fromFlagOrDefault )
import Distribution.Simple.Utils
( die, notice )
import Distribution.Verbosity
( normal )

import Data.Monoid as Monoid
import qualified Data.Map as Map
import Data.Map (Map)
import Control.Monad (unless)
import System.FilePath


-- | 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 :: (ConfigFlags, ConfigExFlags, InstallFlags, HaddockFlags)
-> [String] -> GlobalFlags -> IO ()
freezeAction (configFlags, configExFlags, installFlags, haddockFlags)
extraArgs globalFlags = do

unless (null extraArgs) $
die $ "'freeze' doesn't take any extra arguments: "
++ unwords extraArgs

cabalDir <- defaultCabalDir
let cabalDirLayout = defaultCabalDirLayout cabalDir

projectRootDir <- findProjectRoot
let distDirLayout = defaultDistDirLayout projectRootDir

let cliConfig = commandLineFlagsToProjectConfig
globalFlags configFlags configExFlags
installFlags haddockFlags


(_, elaboratedPlan, _, _) <-
rebuildInstallPlan verbosity
projectRootDir distDirLayout cabalDirLayout
cliConfig

let freezeConfig = projectFreezeConfig elaboratedPlan
writeProjectLocalFreezeConfig projectRootDir freezeConfig
notice verbosity $
"Wrote freeze file: " ++ projectRootDir </> "cabal.project.freeze"

where
verbosity = fromFlagOrDefault normal (configVerbosity configFlags)



-- | Given the install plan, produce a config value with constraints that
-- freezes the versions of packages used in the plan.
--
projectFreezeConfig :: ElaboratedInstallPlan -> ProjectConfig
projectFreezeConfig elaboratedPlan =
Monoid.mempty {
projectConfigShared = Monoid.mempty {
projectConfigConstraints =
concat (Map.elems (projectFreezeConstraints elaboratedPlan))
}
}

-- | Given the install plan, produce solver constraints that will ensure the
-- solver picks the same solution again in future in different environments.
--
projectFreezeConstraints :: ElaboratedInstallPlan
-> Map PackageName [(UserConstraint, ConstraintSource)]
projectFreezeConstraints plan =
--
-- TODO: [required eventually] this is currently an underapproximation
-- since the constraints language is not expressive enough to specify the
-- precise solution. See https://github.com/haskell/cabal/issues/3502.
--
-- For the moment we deal with multiple versions in the solution by using
-- constraints that allow either version. Also, we do not include any
-- constraints for packages that are local to the project (e.g. if the
-- solution has two instances of Cabal, one from the local project and one
-- pulled in as a setup deps then we exclude all constraints on Cabal, not
-- just the constraint for the local instance since any constraint would
-- apply to both instances).
--
Map.unionWith (++) versionConstraints flagConstraints
`Map.difference` localPackages
where
versionConstraints :: Map PackageName [(UserConstraint, ConstraintSource)]
versionConstraints =
Map.mapWithKey
(\p v -> [(UserConstraintVersion p v, ConstraintSourceFreeze)])
versionRanges

versionRanges :: Map PackageName VersionRange
versionRanges =
Map.fromListWith unionVersionRanges $
[ (packageName pkg, thisVersion (packageVersion pkg))
| InstallPlan.PreExisting pkg <- InstallPlan.toList plan
]
++ [ (packageName pkg, thisVersion (packageVersion pkg))
| InstallPlan.Configured pkg <- InstallPlan.toList plan
]

flagConstraints :: Map PackageName [(UserConstraint, ConstraintSource)]
flagConstraints =
Map.mapWithKey
(\p f -> [(UserConstraintFlags p f, ConstraintSourceFreeze)])
flagAssignments

flagAssignments :: Map PackageName FlagAssignment
flagAssignments =
Map.fromList
[ (pkgname, flags)
| InstallPlan.Configured pkg <- InstallPlan.toList plan
, let flags = pkgFlagAssignment pkg
pkgname = packageName pkg
, not (null flags) ]

localPackages :: Map PackageName ()
localPackages =
Map.fromList
[ (packageName pkg, ())
| InstallPlan.Configured pkg <- InstallPlan.toList plan
, pkgLocalToProject pkg
]

60 changes: 44 additions & 16 deletions cabal-install/Distribution/Client/ProjectConfig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module Distribution.Client.ProjectConfig (
findProjectRoot,
readProjectConfig,
writeProjectLocalExtraConfig,
writeProjectLocalFreezeConfig,
writeProjectConfigFile,
commandLineFlagsToProjectConfig,

Expand Down Expand Up @@ -362,9 +363,10 @@ findProjectRoot = do
readProjectConfig :: Verbosity -> FilePath -> Rebuild ProjectConfig
readProjectConfig verbosity projectRootDir = do
global <- readGlobalConfig verbosity
local <- readProjectLocalConfig verbosity projectRootDir
extra <- readProjectLocalExtraConfig verbosity projectRootDir
return (global <> local <> extra)
local <- readProjectLocalConfig verbosity projectRootDir
freeze <- readProjectLocalFreezeConfig verbosity projectRootDir
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC @tibbe was quite vehemently against having a separate .freeze file, that's why we only have cabal.config and not cabal.freeze.config in the standard path. See the discussion here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if any of the new-* commands is supposed to modify project.config. I always assumed that project.config is a purely manually edited file, as opposed tocabal.project.* of which cabal.project.local and cabal.project.freeze are instances...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, cabal.config is also supposed to be manually editable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So here's my theory that tries to unify what is otherwise an ad-hoc list of config files: the cabal.project file would contain a include cabal.project.* directive, and certain commands like configure, freeze etc manage files that can be included. By default they would be included but we'd make that all completely explicit.

So in future, a cabal project init might make a default cabal.project file like:

include $global/config
include cabal.project.*
packages: ./
optional-packages: ./*/

Having them as separate files resolves the problem of automatically editing manually managed files. It's not that it's technically a problem to edit the main cabal.project file, e.g. we could have commands to help do that like adding a new package into the list, but the problem that we don't know how to edit things because we cannot distinguish between manually specified things and generated things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@23Skidoo so looking at the long discussion in that ticket, I'm not sure the objections there apply. It doesn't seem from that ticket that @tibbe really objects to having a separate file for freeze, but he's more concerned with UI complexity. Issues like, how do users know what files are involved, which ones do they edit etc. I think we can answer those questions quite clearly by using an explicit scheme like the above, where you can see how the top level project file includes others. Also, having comments in the relevant files will help make it clear.

It's true we have to make sure we're clear about the freeze workflow, but for the moment I think it's pretty much the same as before. We may want to add an unfreeze that just rm's the cabal.project.freeze file. I also don't foresee any problems with checking in the freeze file into source control.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dcoutts Personally I'm not against having a separate freeze file, just wanted to make sure that @tibbe's objections from that discussion were taken into consideration.

Also, if the freeze file is not supposed to be manually editable, I guess it should include a top-level comment with a warning, just like cabal.sandbox.config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reading that ticket I think @tibbe's complains are handled, here are the main ones:

I think this complicates things:

  • Users will need to understand what they should edit when and understand how the plethora of config files (.cabal, cabal.sandbox.config, cabal.config, cabal.freeze) interact.
  • The implementation is similarly complicated. Just to give an example among many: configure now needs to check if cabal.freeze has been updated and if so reconfigure.

By making the includes explicit it will be clear how the files interact: there's really just the cabal.project file but it can explicitly include other things. The new implementation already deals properly with tracking changes to the project files and reconfiguring correctly, so this isn't a problem.

I was just thinking about what the comment should be, e.g. we might want to explain that you can manage these manually by coping into the main file, but at the moment there's then no way to not include the generated one if it exists. So we may want to move forward on the explicit include approach so that we can provide a coherent story (including putting appropriate comments in the generated files).

extra <- readProjectLocalExtraConfig verbosity projectRootDir
return (global <> local <> freeze <> extra)


-- | Reads an explicit @cabal.project@ file in the given project root dir,
Expand Down Expand Up @@ -399,26 +401,43 @@ readProjectLocalConfig verbosity projectRootDir = do
}


-- | Reads a @cabal.project.extra@ file in the given project root dir,
-- | Reads a @cabal.project.local@ file in the given project root dir,
-- or returns empty. This file gets written by @cabal configure@, or in
-- principle can be edited manually or by other tools.
--
readProjectLocalExtraConfig :: Verbosity -> FilePath -> Rebuild ProjectConfig
readProjectLocalExtraConfig verbosity projectRootDir = do
hasExtraConfig <- liftIO $ doesFileExist projectExtraConfigFile
if hasExtraConfig
then do monitorFiles [monitorFileHashed projectExtraConfigFile]
liftIO readProjectExtraConfigFile
else do monitorFiles [monitorNonExistentFile projectExtraConfigFile]
readProjectLocalExtraConfig verbosity =
readProjectExtensionFile verbosity "local"
"project local configuration file"

-- | Reads a @cabal.project.freeze@ file in the given project root dir,
-- or returns empty. This file gets written by @cabal freeze@, or in
-- principle can be edited manually or by other tools.
--
readProjectLocalFreezeConfig :: Verbosity -> FilePath -> Rebuild ProjectConfig
readProjectLocalFreezeConfig verbosity =
readProjectExtensionFile verbosity "freeze"
"project freeze file"

-- | Reads a named config file in the given project root dir, or returns empty.
--
readProjectExtensionFile :: Verbosity -> String -> FilePath
-> FilePath -> Rebuild ProjectConfig
readProjectExtensionFile verbosity extensionName extensionDescription
projectRootDir = do
exists <- liftIO $ doesFileExist extensionFile
if exists
then do monitorFiles [monitorFileHashed extensionFile]
liftIO readExtensionFile
else do monitorFiles [monitorNonExistentFile extensionFile]
return mempty
where
projectExtraConfigFile = projectRootDir </> "cabal.project.local"
extensionFile = projectRootDir </> "cabal.project" <.> extensionName

readProjectExtraConfigFile =
reportParseResult verbosity "project local configuration file"
projectExtraConfigFile
readExtensionFile =
reportParseResult verbosity extensionDescription extensionFile
. parseProjectConfig
=<< readFile projectExtraConfigFile
=<< readFile extensionFile


-- | Parse the 'ProjectConfig' format.
Expand All @@ -442,7 +461,7 @@ showProjectConfig =
showLegacyProjectConfig . convertToLegacyProjectConfig


-- | Write a @cabal.project.extra@ file in the given project root dir.
-- | Write a @cabal.project.local@ file in the given project root dir.
--
writeProjectLocalExtraConfig :: FilePath -> ProjectConfig -> IO ()
writeProjectLocalExtraConfig projectRootDir =
Expand All @@ -451,6 +470,15 @@ writeProjectLocalExtraConfig projectRootDir =
projectExtraConfigFile = projectRootDir </> "cabal.project.local"


-- | Write a @cabal.project.freeze@ file in the given project root dir.
--
writeProjectLocalFreezeConfig :: FilePath -> ProjectConfig -> IO ()
writeProjectLocalFreezeConfig projectRootDir =
writeProjectConfigFile projectFreezeConfigFile
where
projectFreezeConfigFile = projectRootDir </> "cabal.project.freeze"


-- | Write in the @cabal.project@ format to the given file.
--
writeProjectConfigFile :: FilePath -> ProjectConfig -> IO ()
Expand Down
2 changes: 1 addition & 1 deletion cabal-install/Distribution/Client/ProjectOrchestration.hs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ runProjectPreBuildPhase
-- everything in the project. This is independent of any specific targets
-- the user has asked for.
--
(elaboratedPlan, elaboratedShared, projectConfig) <-
(elaboratedPlan, _, elaboratedShared, projectConfig) <-
rebuildInstallPlan verbosity
projectRootDir distDirLayout cabalDirLayout
cliConfig
Expand Down
31 changes: 24 additions & 7 deletions cabal-install/Distribution/Client/ProjectPlanning.hs
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,27 @@ sanityCheckElaboratedConfiguredPackage sharedConfig
-- * Deciding what to do: making an 'ElaboratedInstallPlan'
------------------------------------------------------------------------------

-- | Return an up-to-date elaborated install plan and associated config.
--
-- Two variants of the install plan are returned: with and without packages
-- from the store. That is, the \"improved\" plan where source packages are
-- replaced by pre-existing installed packages from the store (when their ids
-- match), and also the original elaborated plan which uses primarily source
-- packages.

-- The improved plan is what we use for building, but the original elaborated
-- plan is useful for reporting and configuration. For example the @freeze@
-- command needs the source package info to know about flag choices and
-- dependencies of executables and setup scripts.
--
rebuildInstallPlan :: Verbosity
-> FilePath -> DistDirLayout -> CabalDirLayout
-> ProjectConfig
-> IO ( ElaboratedInstallPlan
-> IO ( ElaboratedInstallPlan -- with store packages
, ElaboratedInstallPlan -- with source packages
, ElaboratedSharedConfig
, ProjectConfig )
-- ^ @(improvedPlan, elaboratedPlan, _, _)@
rebuildInstallPlan verbosity
projectRootDir
distDirLayout@DistDirLayout {
Expand Down Expand Up @@ -275,16 +290,16 @@ rebuildInstallPlan verbosity
elaboratedShared) <- phaseElaboratePlan projectConfigTransient
compilerEtc
solverPlan localPackages
phaseMaintainPlanOutputs elaboratedPlan elaboratedShared

return (elaboratedPlan, elaboratedShared,
projectConfig)
return (elaboratedPlan, elaboratedShared, projectConfig)

-- The improved plan changes each time we install something, whereas
-- the underlying elaborated plan only changes when input config
-- changes, so it's worth caching them separately.
improvedPlan <- phaseImprovePlan elaboratedPlan elaboratedShared
return (improvedPlan, elaboratedShared, projectConfig)

phaseMaintainPlanOutputs improvedPlan elaboratedPlan elaboratedShared

return (improvedPlan, elaboratedPlan, elaboratedShared, projectConfig)

where
fileMonitorCompiler = newFileMonitorInCacheDir "compiler"
Expand Down Expand Up @@ -537,9 +552,10 @@ rebuildInstallPlan verbosity
-- the libs available. This will need to be after plan improvement phase.
--
phaseMaintainPlanOutputs :: ElaboratedInstallPlan
-> ElaboratedInstallPlan
-> ElaboratedSharedConfig
-> Rebuild ()
phaseMaintainPlanOutputs elaboratedPlan elaboratedShared = do
phaseMaintainPlanOutputs _improvedPlan elaboratedPlan elaboratedShared = do
liftIO $ debug verbosity "Updating plan.json"
liftIO $ writePlanExternalRepresentation
distDirLayout
Expand Down Expand Up @@ -1090,6 +1106,7 @@ elaborateInstallPlan platform compiler compilerprogdb

pkgSourceLocation = srcloc
pkgSourceHash = Map.lookup pkgid sourcePackageHashes
pkgLocalToProject = isLocalToProject pkg
pkgBuildStyle = if shouldBuildInplaceOnly pkg
then BuildInplaceOnly else BuildAndInstall
pkgBuildPackageDBStack = buildAndRegisterDbs
Expand Down
7 changes: 7 additions & 0 deletions cabal-install/Distribution/Client/ProjectPlanning/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ data ElaboratedConfiguredPackage
--pkgSourceDir ? -- currently passed in later because they can use temp locations
--pkgBuildDir ? -- but could in principle still have it here, with optional instr to use temp loc

-- | Is this package one of the ones specified by location in the
-- project file? (As opposed to a dependency, or a named package pulled
-- in)
pkgLocalToProject :: Bool,

-- | Are we going to build and install this package to the store, or are
-- we going to build it and register it locally.
pkgBuildStyle :: BuildStyle,

pkgSetupPackageDBStack :: PackageDBStack,
Expand Down
3 changes: 3 additions & 0 deletions cabal-install/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import qualified Distribution.Client.List as List
import qualified Distribution.Client.CmdConfigure as CmdConfigure
import qualified Distribution.Client.CmdBuild as CmdBuild
import qualified Distribution.Client.CmdRepl as CmdRepl
import qualified Distribution.Client.CmdFreeze as CmdFreeze

import Distribution.Client.Install (install)
import Distribution.Client.Configure (configure)
Expand Down Expand Up @@ -283,6 +284,8 @@ mainWorker args = topHandler $
CmdBuild.buildAction
, hiddenCmd installCommand { commandName = "new-repl" }
CmdRepl.replAction
, hiddenCmd installCommand { commandName = "new-freeze" }
CmdFreeze.freezeAction
]

type Action = GlobalFlags -> IO ()
Expand Down
1 change: 1 addition & 0 deletions cabal-install/cabal-install.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ executable cabal
Distribution.Client.Check
Distribution.Client.CmdBuild
Distribution.Client.CmdConfigure
Distribution.Client.CmdFreeze
Distribution.Client.CmdRepl
Distribution.Client.Config
Distribution.Client.Configure
Expand Down
2 changes: 1 addition & 1 deletion cabal-install/tests/IntegrationTests2.hs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ planProject testdir cliConfig = do
-- ended in an exception (as we leave the files to help with debugging).
cleanProject testdir

(elaboratedPlan, elaboratedShared, projectConfig) <-
(elaboratedPlan, _, elaboratedShared, projectConfig) <-
rebuildInstallPlan verbosity
projectRootDir distDirLayout cabalDirLayout
cliConfig
Expand Down