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

[WIP] stack freeze command #4220

Merged
merged 6 commits into from
Aug 21, 2018
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
3 changes: 3 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ Major changes:
changes, existing cache files will in general be invalidated,
resulting in Stack needing to rebuild many previously cached
builds in the new version. Sorry :(.
* A new command, `stack freeze` has been added which outputs
project and snapshot definitions with dependencies pinned to
their exact versions.

Behavior changes:

Expand Down
38 changes: 38 additions & 0 deletions doc/setting_up_dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Dependency freezing

To make builds reproducible it makes sense to pin project dependencies to some
exact versions and this is what is stack's `freeze` command is about.

## Project freezing

The default mode of its invocation:

```
$ stack freeze
```
freezes the following fields from the project's `stack.yaml`

* packages in `extra-deps` which do not include sha256 of their cabal files and
which do not specify pantry tree pointer of the package archive
* `resolver` if it references a remote snapshot and if it does not specify
pantry tree pointer of its contents

The command outputs to standard output new project's `stack.yaml` with these
changes included.

If a project is specified precisely enough stack tells about it and exits.

## Snapshot freezing

When a project uses some custom snapshot freezing dependencies defined in
the project is not enough as a snapshot could also contain not precisely
specified package references. To prevent this from happening `--snapshot` flag
(or `-s` in its short form) of the `freeze` command could be used:

```
$ stack freeze --snapshot
```

In this mode `freeze` command works almost like in the default mode, the main
differenc is that it works with the projects snapshot definition and thus it
pins packages from its `packages` field and not from the project's `extra-deps`.
2 changes: 2 additions & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ library:
- Stack.Docker.GlobalDB
- Stack.Dot
- Stack.FileWatch
- Stack.Freeze
- Stack.GhcPkg
- Stack.Ghci
- Stack.Ghci.Script
Expand All @@ -198,6 +199,7 @@ library:
- Stack.Options.DockerParser
- Stack.Options.DotParser
- Stack.Options.ExecParser
- Stack.Options.FreezeParser
- Stack.Options.GhcBuildParser
- Stack.Options.GhciParser
- Stack.Options.GhcVariantParser
Expand Down
55 changes: 55 additions & 0 deletions src/Stack/Freeze.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}

module Stack.Freeze
( freeze
, FreezeOpts (..)
, FreezeMode (..)
) where

import qualified Data.Yaml as Yaml
import qualified RIO.ByteString as B
import Stack.Prelude
import Stack.Types.BuildPlan
import Stack.Types.Config

data FreezeMode = FreezeProject | FreezeSnapshot

data FreezeOpts = FreezeOpts
{ freezeMode :: FreezeMode
}

freeze :: HasEnvConfig env => FreezeOpts -> RIO env ()
freeze (FreezeOpts FreezeProject) = do
mproject <- view $ configL.to configMaybeProject
case mproject of
Just (p, _) -> do
let deps = projectDependencies p
resolver = projectResolver p
completePackageLocation' pl =
case pl of
PLImmutable pli -> PLImmutable <$> completePackageLocation pli
plm@(PLMutable _) -> pure plm
resolver' <- completeSnapshotLocation resolver
deps' <- mapM completePackageLocation' deps
if deps' == deps && resolver' == resolver
then
logInfo "No freezing is required for this project"
else
liftIO $ B.putStr $ Yaml.encode p{ projectDependencies = deps'
, projectResolver = resolver'
}
Nothing -> logWarn "No project was found: nothing to freeze"

freeze (FreezeOpts FreezeSnapshot) = do
msnapshot <- view $ buildConfigL.to bcSnapshotDef.to sdSnapshot
case msnapshot of
Just (snap, _) -> do
snap' <- completeSnapshot snap
if snap' == snap
then
logInfo "No freezing is required for the snapshot of this project"
else
liftIO $ B.putStr $ Yaml.encode snap'
Nothing ->
logWarn "No snapshot was found: nothing to freeze"
16 changes: 16 additions & 0 deletions src/Stack/Options/FreezeParser.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{-# LANGUAGE NoImplicitPrelude #-}

module Stack.Options.FreezeParser where

import Data.Semigroup ((<>))
import Options.Applicative
import Stack.Freeze


-- | Parser for arguments to `stack freeze`
freezeOptsParser :: Parser FreezeOpts
freezeOptsParser =
FreezeOpts <$> flag FreezeProject FreezeSnapshot
( long "snapshot"
<> short 's'
<> help "Freeze snapshot definition instead of project's stack.yaml" )
10 changes: 10 additions & 0 deletions src/main/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import Stack.Dot
import Stack.GhcPkg (findGhcPkgField)
import qualified Stack.Nix as Nix
import Stack.FileWatch
import Stack.Freeze
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you forgot to git add the new modules.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ouch, forgot the main part of it

import Stack.Ghci
import Stack.Hoogle
import Stack.Ls
Expand All @@ -77,6 +78,7 @@ import Stack.Options.DotParser
import Stack.Options.ExecParser
import Stack.Options.GhciParser
import Stack.Options.GlobalParser
import Stack.Options.FreezeParser

import Stack.Options.HpcReportParser
import Stack.Options.NewParser
Expand Down Expand Up @@ -388,6 +390,10 @@ commandLineHandler currentDir progName isInterpreter = complicatedOptions
"Run a Stack Script"
scriptCmd
scriptOptsParser
addCommand' "freeze"
"Show project or snapshot with pinned dependencies if there are any such"
freezeCmd
freezeOptsParser

unless isInterpreter (do
addCommand' "eval"
Expand Down Expand Up @@ -1004,6 +1010,10 @@ queryCmd selectors go = withBuildConfig go $ queryBuildInfo $ map T.pack selecto
hpcReportCmd :: HpcReportOpts -> GlobalOpts -> IO ()
hpcReportCmd hropts go = withBuildConfig go $ generateHpcReportForTargets hropts

freezeCmd :: FreezeOpts -> GlobalOpts -> IO ()
freezeCmd freezeOpts go =
withBuildConfig go $ freeze freezeOpts

data MainException = InvalidReExecVersion String String
| UpgradeCabalUnusable
| InvalidPathForExec FilePath
Expand Down
25 changes: 25 additions & 0 deletions test/integration/tests/4220-freeze-command/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Control.Monad (unless)
import StackTest

main :: IO ()
main = do
stackCheckStdout ["freeze"] $ \stdOut -> do
let expected = unlines
[ "packages:"
Copy link
Contributor

Choose a reason for hiding this comment

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

No problem with this approach, but you can sort of get multiline strings in Haskell with:

"foo\n\
\bar\n\
\baz"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know but unlines looks a bit more readable in this case imo

, "- ."
, "extra-deps:"
, "- hackage: a50-0.5@sha256:b8dfcc13dcbb12e444128bb0e17527a2a7a9bd74ca9450d6f6862c4b394ac054,1491"
, " pantry-tree:"
, " size: 409"
, " sha256: a7c6151a18b04afe1f13637627cad4deff91af51d336c4f33e95fc98c64c40d3"
, "resolver:"
, " size: 527165"
, " url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/11/19.yaml"
, " sha256: 0116ad1779b20ad2c9d6620f172531f13b12bb69867e78f4277157e28865dfd4"
]
unless (stdOut == expected) $
error $ concat [ "Expected: "
, show expected
, "\nActual: "
, show stdOut
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: freeze-command
version: 0.1.0.0
build-type: Simple
cabal-version: >= 2.0

library
exposed-modules: Src
hs-source-dirs: src
build-depends: base
, rio
, vector
default-language: Haskell2010
5 changes: 5 additions & 0 deletions test/integration/tests/4220-freeze-command/files/src/Src.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module Src where

-- | A function of the main library
funMainLib :: Int -> Int
funMainLib = succ
5 changes: 5 additions & 0 deletions test/integration/tests/4220-freeze-command/files/stack.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resolver: lts-11.19
packages:
- .
extra-deps:
- a50-0.5@rev:0