Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ghc/head.hackage
  • RyanGlScott/head.hackage
  • vaibhavsagar/head.hackage
  • phadej/head.hackage
  • jessoune29/head.hackage
  • alanz/head.hackage
  • clint/head.hackage
  • osa1/head.hackage
  • supersven/head.hackage
  • fendor/head.hackage
  • hsyl20/head.hackage
  • adinapoli/head.hackage
  • alexbiehl/head.hackage
  • mimi.vx/head.hackage
  • Kleidukos/head.hackage
  • wz1000/head.hackage
  • alinab/head.hackage
  • teo/head.hackage
  • duog/head.hackage
  • sheaf/head.hackage
  • expipiplus1/head.hackage
  • drsooch/head.hackage
  • tobias/head.hackage
  • brandonchinn178/head.hackage
  • mpickering/hooks-setup-testing
  • Mikolaj/head.hackage
  • RandomMoonwalker/head.hackage
  • facundominguez/head.hackage
  • trac-fizzixnerd/head.hackage
  • neil.mayhew/head.hackage
  • jappeace/head.hackage
31 results
Show changes
Commits on Source (1023)
patches/* -text
......@@ -2,150 +2,94 @@
# ===========================
#
# This is the GitLab CI automation that drives GHC's head.hackage testing.
# The goal is to be able to test GHC by building a (small) subset of Hackage.
# The goal is to be able to test GHC by building a subset of Hackage.
# Moreover, we want to be able to collect logs of failed builds as well as
# performance metrics from builds that succeed.
#
# To accomplish this we use head.hackage's native Nix support and the
# ghc-artefact-nix expression to make GHC binary distributions usable from
# within Nix. These components are tied together by ./scripts/build-all.nix,
# which contains the list of packages which we build as well as some simple
# configuration to minimize the cost of the builds.
# To accomplish this we use the ci executable in ./ci. This drives a set of
# cabal v2-build builds and preserves their results.
#
# The execution flow looks something like:
#
# - Gitlab runner
# - (nix run)
# - run-ci
# - ./run-ci (the Nix package just wraps the script)
# - (nix run) (when USE_NIX=1)
# - head-hackage-ci $EXTRA_OPTS (a Cabal project in ci/)
# - ci/Main.hs
# - TestPatches.testPatches <$> TestPatches.config
# - option '--test-package'
# - <something similar for building the packages>
#
# EXTRA_OPTS are injected into the execution flow inside ./run-ci, which in turn
# sources them from ci/config.sh.
#
# The compiler to be tested can be taken from a number of sources.
# head.hackage's own validation pipeline runs against GHC HEAD and the three
# supported major versions. In addition, other GitLab projects (e.g. ghc/ghc>)
# can trigger a multi-project pipeline, specifying a GHC binary distribution
# via either the GHC_TARBALL or UPSTREAM_* variables.
#
stages:
- test
- update-repo
- deploy
variables:
# Commit of ghc/ci-images repository from which to pull Docker images
DOCKER_REV: 6d19c3adc1f5c28c82aed8c5b1ac40931ac60f3f
# Which nixos/nix Docker image tag to use
DOCKER_TAG: "2.13.1"
# Default GHC bindist
GHC_TARBALL: "https://gitlab.haskell.org/api/v4/projects/1/jobs/artifacts/master/raw/ghc-x86_64-fedora27-linux.tar.xz?job=validate-x86_64-linux-fedora27"
# Default this to ghc/ghc> to make it more convenient to run from the web
# interface.
UPSTREAM_PROJECT_ID: 1
UPSTREAM_PROJECT_PATH: "ghc/ghc"
# Project ID of ghc/ghc
GHC_PROJECT_ID: "1"
GIT_SUBMODULE_STRATEGY: recursive
# ACCESS_TOKEN provided via protected environment variable
# CPUS is set by the runner, as usual.
# EXTRA_HC_OPTS provided by GHC job. These are passed to via --ghc-options to
# GHC during the package builds. This is instantiated with, e.g., -dcore-lint
# during GHC validation builds.
# EXTRA_HC_OPTS are passed to via --ghc-options to GHC during the package
# builds. This is instantiated with, e.g., -dcore-lint during GHC validation
# builds.
build:
stage: test
# ONLY_PACKAGES can be passed to restrict set of packages that are built.
tags:
- x86_64-linux
- head.hackage
# EXTRA_OPTS are passed directly to test-patches.
image: nixos/nix
# Multi-project pipeline variables:
#
# These are set by the "upstream" pipeline for downstream pipelines:
#
# UPSTREAM_PROJECT_PATH: The path of the upstream project (e.g. `ghc/ghc`)
# UPSTREAM_PIPELINE_ID: The ID of the upstream pipeline
#
# Instead of UPSTREAM_PIPELINE_ID you can also pass:
cache:
key: build-all
paths:
- store.nar
before_script:
- |
if [ -e store.nar ]; then
echo "Extracting cached Nix store..."
nix-store --import -vv < store.nar || echo "invalid cache"
else
echo "No cache found"
fi
script:
- |
if [ -n "$GHC_PIPELINE_ID" ]; then
job_name="validate-x86_64-linux-fedora27"
job_id=$(nix run -f scripts/build-all.nix find-job \
--arg bindistTarball $GHC_TARBALL \
-c find-job.sh $GHC_PROJECT_ID $GHC_PIPELINE_ID $job_name)
echo "Pulling ${job_name} binary distribution from Pipeline $GHC_PIPELINE_ID (job $job_id)..."
fi
# UPSTREAM_COMMIT_SHA: The ref or commit SHA of the GHC build to be tested
#
- echo "Bindist tarball is $GHC_TARBALL"
- nix-build scripts/build-all.nix -j$CPUS
--no-build-output
-A buildDepends
--arg bindistTarball "$GHC_TARBALL"
--arg extraHcOpts "\"$EXTRA_HC_OPTS\""
- nix-store --export $(nix-store -qR --include-outputs $(nix-instantiate --quiet scripts/build-all.nix --arg bindistTarball $GHC_TARBALL -A buildDepends -A ghc)) > store.nar
- ret=0
- nix-build scripts/build-all.nix
-j$CPUS --no-build-output
-A testedPackages
--keep-going
--arg bindistTarball $GHC_TARBALL
|| { echo "Build failed!"; ret=1; }
- scripts/summarize.py || echo "summarize script failed"
- exit $ret
# We explictly set the locale to avoid happy choking up on UTF-8 source code. See #31
LANG: "C.UTF-8"
after_script:
- nix run -f '<nixpkgs>' gnutar -c tar -zcf logs.tar.gz logs
- nix run -f '<nixpkgs>' graphviz -c scripts/render-graph.sh < summary.dot > summary.dot.svg
- ls -lh
artifacts:
when: always
paths:
- logs.tar.gz
- summary.json
- summary.dot
- summary.dot.svg
# Build and deploy a Hackage repository
update-repo:
stage: update-repo
tags:
- x86_64-linux
- head.hackage
stages:
- generate
- dispatch
image: nixos/nix
generate-pipeline:
variables:
#KEYS_TARBALL: https://downloads.haskell.org/ghc/head.hackage-keys.tar.enc
KEYS_TARBALL: http://home.smart-cactus.org/~ben/head.hackage-keys.tar.enc
# KEYS_TARBALL_KEY provided by protected variable
only:
- master
script:
- nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
- nix-channel --update
- nix build -f scripts/build-repo.nix
- nix run -f scripts/build-repo.nix -c build-repo.sh build-repo
- nix run -f '<nixpkgs>' gnutar -c tar -zxf logs.tar.gz
- mv logs repo
- cp summary.dot.svg repo
dependencies:
- build
after_script:
- rm -Rf keys
artifacts:
paths:
- repo
pages:
stage: deploy
tags:
- x86_64-linux
- head.hackage
image: nixos/nix
script:
- mv repo public
dependencies:
- update-repo
only:
- master
GIT_SUBMODULE_STRATEGY: none
image: alpine:latest
tags: [x86_64-linux]
stage: generate
script: ./ci/generate-pipeline.sh
artifacts:
paths:
- public
- gitlab-generated-pipeline.yml
run-pipeline:
stage: dispatch
trigger:
strategy: depend
forward:
pipeline_variables: true
include:
- artifact: gitlab-generated-pipeline.yml
job: generate-pipeline
[submodule "tests/ghc-debug"]
path = tests/ghc-debug
url = https://gitlab.haskell.org/ghc/ghc-debug.git
[submodule "tests/text"]
path = tests/text
url = https://github.com/haskell/text.git
[submodule "tests/bytestring"]
path = tests/bytestring
url = https://github.com/haskell/bytestring.git
[submodule "tests/containers"]
path = tests/containers
url = https://github.com/haskell/containers.git
Copyright 2023 The GHC Team
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
......@@ -6,35 +6,33 @@ Submit PRs with patch(es) relative to the source tarball(s) of
existing Hackage package(s).
- The patches MUST apply cleanly by `patch -p1` when inside the
original unpacked source-tarball. (Travis CI will verify this when
original unpacked source-tarball. (CI will verify this when
you submit a PR).
- The patches SHOULD work with at least GHC HEAD and the most recent
stable released GHC version (currently this means with GHC 8.6.1 and
GHC 8.7).
- The patches SHOULD work with at least GHC HEAD and a set of recent stable
released GHC versions (currently this means with GHC 9.6, 9.8, 9.10, 9.12 and 9.13).
- The patches SHOULD ideally result in the same code being compiled,
as one of the main purposes of these patches is to make regression
testing possible. I.e. try to avoid conditional compilation.
- If only the `.cabal` file needs to be modified, a `.cabal` file
SHOULD be used instead of a `.patch` file. If the changes to the
`.cabal` file are too invasive (e.g. removing modules, changing the
structure of the package etc), a `.patch` file must be used.
## How this works
This repo contains `<pkg-id>.patch` and `<pkg-id>.cabal` files in the
This repo contains `<pkg-id>.patch` files in the
[`patches/`](./patches/) folder (where `<pkg-id>` refers to a specific
release of a package, e.g. `lens-4.15.3`).
Adding a patch forces the system to use that specific version,
so empty patch files may exist to force the system to use that a
newer version, instead of a previous patch if available.
For example consider a patched `th-abstraction-0.5.0`, and an empty patch `th-abstraction-0.6.0`,
if we were to remove the empty patch, `0.6.0`, certain libraries such
as `generics-sop` fail to build, because it's forced to use `0.5.0`.
Once merged to `master`, all package releases whose `<pkg-id>` is
mentioned will enter the *HEAD.hackage* package index; if there is a
`.patch` file, the respective releases tarballs are patched
(i.e. mutated!). If there is a `.cabal` file, it is included as a
revision in the package index. Consequently, if there is only a
`.cabal` file and no `.patch` file, the original source `.tar.gz` is
included verbatimely (i.e. *not* mutated).
(i.e. mutated!).
If this operation succeeds, the `HEAD.hackage` package index at
http://HEAD.hackage.haskell.org/ is updated to contain the new index
......@@ -49,6 +47,35 @@ allowing to maximise sharing via the nix-style package-db cache store.
## How to use
If you know what you are looking for, here it is:
```cabal
repository head.hackage.ghc.haskell.org
url: https://ghc.gitlab.haskell.org/head.hackage/
secure: True
key-threshold: 3
root-keys:
f76d08be13e9a61a377a85e2fb63f4c5435d40f8feb3e12eb05905edb8cdea89
26021a13b401500c8eb2761ca95c61f2d625bfef951b939a8124ed12ecf07329
7541f32a4ccca4f97aea3b22f5e593ba2c0267546016b992dfadcd2fe944e55d
active-repositories: hackage.haskell.org, head.hackage.ghc.haskell.org:override
```
The use of `:override` forces cabal's constraint solver to pick versions of
libraries that have corresponding patches in head.hackage whenever possible.
This may or may not be what you want depending on your use case. If you wish
to permit cabal to choose build plans that include different versions of
libraries than what are patched in head.hackage, skip the `:override`:
```cabal
active-repositories: hackage.haskell.org, head.hackage.ghc.haskell.org
```
Also see
https://cabal.readthedocs.io/en/3.12/cabal-project-description-file.html#cfg-field-active-repositories.
`HEAD.hackage` doesn't bump the bounds of boot packages + certain other packages to avoid the busywork of bumping them. When using `HEAD.hackage`, you should use `--allow-newer` for these packages. The full list is [here](https://gitlab.haskell.org/ghc/head.hackage/-/blob/90570e1c4606c1d7d3d41797ec1b32d1b984067b/ci/MakeConstraints.hs#L40-49).
### As an add-on remote repository
......@@ -115,70 +142,60 @@ initialize it as a git repository, and the patch.
### Adding a patch
The `scripts/patch-tool` script is a tool for conveniently authoring and updating
patches. For instance, if you find that the `doctest` package needs to be
patched first run:
```
$ scripts/patch-tool unpack doctest
```
This will extract a `doctest` source tree to `packages/doctest-$version` and
initialize it as a git repository. You can now proceed to edit the tree as
necessary and run
```
$ scripts/patch-tool update-patches
```
This will create an appropriately-named patch in `patches/` from the edits in
the `doctest` tree.
The `scripts/patch-tool` script is a tool for conveniently authoring and updating patches. For example, to patch the `doctest` package, you can run the following steps:
### Usage with `nix`
1. `scripts/patch-tool unpack doctest`
1. Modify files in `packages/doctest-$version/` as necessary
1. Build/test as normal, e.g. `cabal build doctest`
1. `scripts/patch-tool update-patches`
1. Commit the patch
When contributing a patch, one needs to be mindful of [Hackage revisions].
head.hackage doesn't combine patches with the revisions of a package. Instead,
a patch is applied on the unrevised package (also called revision 0). This
implies that when contributing patches, it might be necessary to additionally
include the changes that are already in some revision. Moreover, this also
implies that if a patch only contains changes that are already present in
revisions, then contributing the patch to head.hackage is useless as the changes
are already available for building.
[Hackage revisions]: https://github.com/haskell-infra/hackage-trustees/blob/master/revisions-information.md
`default.nix` is a [Nix](https://nixos.org/nix/) expression which can be used to
build `head.hackage` packages using GHC 8.6.1-alpha2:
```
$ nix build -f ./. haskellPackages.servant
```
It can also be used to build a compiler from a local source tree and use this to
build `head.hackage` packages:
```
$ nix build -f ./. --arg ghc "(import ghc-from-source.nix {ghc-path=$GHC_TREE;})"
```
### GitLab CI
[GHC's GitLab instance](https://gitlab.haskell.org/ghc/head.hackage) uses
GitLab CI and `nix` to build a subset of the head.hackage package set using GHC
snapshots.
GHC's GitLab instance uses GitLab CI and the `head-hackage-ci` tool (contained
in the `ci/` directory) to test the `head.hackage` patchset against GHC releases
and snapshots. It can also compile head.hackage using a patch to GHC; just add
the `user-facing` label to a GHC MR, and the existing CI infrastructure will
invoke head.hackage.
To run a similar build locally simply download a binary distribution from a
`x86_64-fedora27-linux` CI job and run:
To run a similar build locally start by downloading and installing a binary
distribution appropriate for your distribution and then call the `run-ci` script:
```
$ export GHC_TARBALL=./ghc-x86_64-fedora27-linux.tar.xz
# for extra correctness assurance...
$ export GHC=/path/to/my/ghc
# enable Core Linting for extra correctness assurance...
$ export EXTRA_HC_OPTS=-dcore-lint
$ scripts/build-nix.sh
$ ./run-ci
```
This will build the set of packages defined by the `testedPackages` list in
`scripts/build-all.nix`.
After building `testedPackages` (allowing for failures) the script job runs
`scripts/summarize.py`, which produces a few artifacts:
This will build all packages having patches and produce a textual summary, as
well as a JSON file (`result.json`) describing the outcome.
* a JSON summary (`summary.json`) which includes the full dependency graph as
well as which package builds failed
* a DOT graph (`summary.dot`) showing the package depedencies and their build
success. This can be rendered with `scripts/render-graph.sh`.
* a directory (`./logs`) of build logs
If you are using nix you can run:
Note that `build-nix.sh` can also be used to build packages not included in
`testedPackages`:
```
$ scripts/build-nix.sh pandoc
nix-shell ci/ --command run-ci
```
Note that we currently rely on IOG's Hydra instance for caching of flake
outputs to ensure that they aren't rebuilt with every job.
### Hackage repository
[GHC's GitLab instance](https://gitlab.haskell.org/ghc/head.hackage) uses
GitLab CI to deploy a Hackage repository with the patches provided by
`head.hackage`. See the [repository]() for usage instructions.
GHC's GitLab instance uses GitLab CI to deploy a Hackage repository with the
patches provided by `head.hackage`. See the
[repository](http://ghc.gitlab.haskell.org/head.hackage/) for usage
instructions.
### Travis CI
......
let rev = "e8e76bc26a994aee313e571dc4e6701398d17a42";
in
{
url = "https://github.com/commercialhaskell/all-cabal-hashes/archive/${rev}.tar.gz";
sha256 = "16rnyxqmr93ahml0fjfa6hmjpmx8sbpfdr52krd2sd6ic9n5p5ix";
}
-- Need an empty file for cabal.project.local to work
-- https://github.com/haskell/cabal/issues/9168
run
dist
dist-newstyle
Copyright (c) 2019, Ben Gamari
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Ben Gamari nor the names of other
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
{-# LANGUAGE OverloadedStrings #-}
-- | Logic to find the most recent released version of a package.
module LatestVersion (getNewestVersion) where
import Distribution.Types.Version hiding (showVersion)
import Distribution.Text
import qualified Distribution.Package as Cabal
import qualified Data.Text as T
import Network.Wreq
import Data.Aeson
import Control.Monad.Fail
import Control.Lens
import Prelude hiding (fail)
data Resp = Resp { versions :: [Version] }
instance FromJSON Resp where
parseJSON = withObject "response" $ \o -> do
versions <- o .: "normal-version"
versions' <- mapM parseVersion versions
return $ Resp versions'
where
parseVersion :: MonadFail m => String -> m Version
parseVersion = maybe (fail "failed to parse version") pure . simpleParse
getNewestVersion :: Cabal.PackageName -> IO Version
getNewestVersion pname = do
resp <- getWith opts url >>= asJSON
return $ head $ versions (resp ^. responseBody)
where
opts = defaults & header "Accept" .~ ["application/json"]
url = "https://hackage.haskell.org/package/"++(display pname)++"/preferred"
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad
import Options.Applicative
import qualified TestPatches
import qualified MakeConstraints
mode :: Parser (IO ())
mode = hsubparser $ mconcat
[ command "test-patches" $ info testPatches (progDesc "build patched packages")
, command "make-constraints" $ info makeConstraints (progDesc "generate a cabal.constraints file")
]
where
testPatches = TestPatches.testPatches <$> TestPatches.config
makeConstraints =
(MakeConstraints.makeConstraints >=> print)
<$> argument str (metavar "DIR" <> help "patches directory")
main :: IO ()
main = do
theMode <- execParser $ info (helper <*> mode) mempty
theMode
{-# LANGUAGE OverloadedStrings #-}
module MakeConstraints where
import qualified Distribution.Package as Cabal
import Distribution.Text
import Distribution.Types.Version hiding (showVersion)
import qualified Data.Set as S
import qualified Data.Map.Strict as M
import qualified Text.PrettyPrint.ANSI.Leijen as PP
import Text.PrettyPrint.ANSI.Leijen (Doc, vcat, (<+>))
import Utils
-- These dependencies cause issues when testing boot libraries because the test-suites
-- introduce circular dependencies. One way to solve the circularity is to select
-- older version of packages (namely unix) which doesn't have the bytestring dependency (<= 2.5)
-- but we want to use the newer version of unix and just not use the optional
-- features of optparse-applicative nor tasty.
extraConstraints :: [String]
extraConstraints = [
"optparse-applicative -process"
, "tasty -unix"
]
-- These packages we must use the installed version, because there's no way to upgrade
-- them
bootPkgs :: S.Set Cabal.PackageName
bootPkgs = S.fromList
[ "base"
, "template-haskell"
, "ghc"
, "ghc-prim"
, "integer-gmp"
, "ghc-bignum"
]
-- These packages are installed, but we can install newer versions if the build plan
-- allows.. so we --allow-newer them in order to help find more build plans.
allowNewerPkgs :: S.Set Cabal.PackageName
allowNewerPkgs = S.fromList
[ "time"
, "binary"
, "bytestring"
, "Cabal"
, "containers"
, "deepseq"
, "text"
, "ghc-boot"
, "ghc-boot-th" ] `S.union` bootPkgs
constraints :: [String] -> Doc
constraints constraints =
"constraints:" PP.<$$> PP.indent 2 constraintsDoc
where
constraintsDoc = PP.vcat $ PP.punctuate "," (map PP.text constraints)
allowNewer :: S.Set Cabal.PackageName -> Doc
allowNewer pkgs =
"allow-newer:" PP.<$$> PP.indent 2 pkgsDoc
where
pkgsDoc = PP.vcat $ PP.punctuate "," $ map prettyPackageName $ S.toList pkgs
installedConstraints :: S.Set Cabal.PackageName -> S.Set Cabal.PackageName -> Doc
installedConstraints bootPkgs patchedPkgs =
"constraints:" PP.<$$> PP.indent 2 pkgsDoc
where
pkgsDoc = PP.vcat $ PP.punctuate ","
[ prettyPackageName bootPkg <+> "installed"
| bootPkg <- S.toList bootPkgs
, bootPkg `S.notMember` patchedPkgs
]
versionConstraints :: [(Cabal.PackageName, Version)] -> Doc
versionConstraints pkgs =
"constraints:" PP.<$$> PP.indent 2 body
where
body :: Doc
body = vcat $ PP.punctuate ","
[ prettyPackageName pkg <+> versionConstraints vers
| (pkg, vers) <- M.toList pkgVersions
]
versionConstraints :: S.Set Version -> Doc
versionConstraints vers =
PP.hcat $ PP.punctuate " || "
[ "==" <> prettyVersion ver
| ver <- S.toAscList vers
]
pkgVersions :: M.Map Cabal.PackageName (S.Set Version)
pkgVersions = M.fromListWith (<>)
[ (pkg, S.singleton ver)
| (pkg, ver) <- pkgs
]
makeConstraints :: FilePath -- ^ patch directory
-> IO Doc
makeConstraints patchDir = do
patches <- findPatchedPackages patchDir
let patchedPkgs = S.fromList $ map fst patches
doc = PP.vcat
[ allowNewer allowNewerPkgs
, ""
, installedConstraints bootPkgs patchedPkgs
, ""
, versionConstraints patches
, ""
, constraints extraConstraints
]
return doc
# head.hackage CI driver
This is the application which drives GHC's `head.hackage` continuous
integration infrastructure, namely `head-hackage-ci`. The general goals here
are three-fold:
1. test that the patches in the `head.hackage` repository build
2. use these patches to smoke-test GHC pre-releases and nightly builds
3. provide the patches as a Hackage repository
(<https://ghc.gitlab.haskell.org/head-hackage>) for consumption by end-users
We accomplish this via the `head-hackage-ci` executable. This executable has
two modes:
* `test-patches` is the primary driver of the CI testing process
* `make-constraints` is a utility for producing `cabal-install` constraints
to ensure that only patched versions of packages are used in install plans
Naturally, many Haskell packages have dependencies on native libraries.
`head-hackage-ci` supports two ways of providing these libraries:
* *from the host system*: Here we just rely on the host system to provide
native libraries; it is up to the user to ensure that the necessary packages
are installed.
* *from nixpkgs*: Here we use [nix][nix] and the [nixpkgs][nixpkgs] package set
to provide native libraries. These dependencies are defined in
`ci/build-deps.nix`. This mode is
[nix]: https://nixos.org/nix/
[nixpkgs]: https://github.com/NixOS/nixpkgs
## Test procedure
The testing part of the CI process (goals (1) and (2) above) uses `head-hackage-ci`'s
`test-patches` mode and some shell scripts (namely `ci/config.sh` and
`ci/build-repo.sh`) (and in the case of a Nix-based build, `ci/build-deps.nix`).
The below is all orchestrated by `run-ci.sh`:
1. Call `ci/config.sh` to determine the configuration of the run. This does
the following:
1. Identify the version of the compiler being tested (provided by the user
via the `GHC` environment variable)
1. Use the compiler version to find the set of packages
that we expected to be broken.
1. Build a set of command-line arguments destined for `head-hackage-ci`
from the broken-package set above and a set of "extra" packages
defined in `config.sh`
1. If we are using `nixpkgs` to get native libraries: compute a
`cabal.project` fragment from the dependency information in
`ci/build-deps.nix` (this logic lives in `ci/default.nix`).
1. Call `head-hackage-ci test-patches` with the computed arguments. This does the following:
1. Determine the set of packages to test (determined by the contents of the
`patches/` directory and additional packages provided via the
`--extra-package` flag)
1. Build a local Hackage repository of patched packages using the
`build-repo.sh` script (which itself depends upon
`hackage-repo-overlay-tool` and `hackage-overlay-tool`)
1. Build a `cabal.project` file containing configuration (e.g. the location
of the local repository, the location of the compiler, where native
dependencies are found, etc.)
1. Call `cabal v2-update` to inform `cabal` of the patched package repository
1. For each package to test:
1. Create a new working directory
1. Copy the previously constructed `cabal.project` into our working directory
1. Construct a dummy Cabal package depending only on the package under test
1. Call `cabal new-build` to build the package
1. Examine the state of `plan.json` and the cabal `logs` directory to work out
the outcome of the build
1. Write a JSON report (of type `Types.RunResult ()`) to `result.json`
1. Examine the failed packages and determine whether there were any unexpected failures.
### Build plans and empty patches
When testing a package, the CI driver will construct a build plan that favors
versions of Hackage libraries with `head.hackage` patches over versions of the same
library that lack patches. For example, if CI tests a library that depends on
library `foo` that has two Hackage releases, 0.1 and 0.2, then if `foo-0.1` has
a patch but `foo-0.2` does not, then the driver will include `foo-0.1` in the
build plan even though `foo-0.2` has a more recent version number. This is done
to reduce the likelihood of subsequent Hackage releases of `foo` breaking the
CI due to API changes.
Sometimes, this approach can work against you. Suppose that another library
`bar` also depends on `foo`. Moreover, `bar` requires the use of `foo-0.2` and
excludes `foo-0.1` in its version bounds. Because `foo-0.1` has a patch but
`foo-0.2` does not, however, the CI driver will insist on using `foo-0.1` when
constructing build plans, which means that it will fail to find a valid build
plan for `bar`!
The simplest way to fix this sort of problem is to add a patch for `foo-0.2`.
If there are patches for both `foo-0.1` and `foo-0.2` present, then the CI
driver will admit build plans with either version of `foo`. In the event that
`foo-0.2` already compiles with all supported versions of GHC, you can simply
add an empty patch by running `touch patches/foo-0.2.patch`.
import Distribution.Simple
main = defaultMain
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ScopedTypeVariables #-}
module TestPatches
( testPatches
, Config(..), config
) where
import Control.Monad
import Data.Foldable
import Data.List (intercalate, partition)
import Data.Maybe
import Data.Text (Text)
import GHC.Generics
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Encoding.Error as TE
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BSL
import Data.Aeson
import qualified Data.Map.Strict as M
import qualified Data.Map.Merge.Strict as M
import qualified Data.Set as S
import qualified Distribution.Package as Cabal
import Distribution.Text
import Distribution.Types.Version hiding (showVersion)
import qualified Text.PrettyPrint.ANSI.Leijen as PP
import Text.PrettyPrint.ANSI.Leijen (Doc, vcat, (<+>))
import System.FilePath
import System.Directory
import System.Environment (getEnvironment)
import System.Exit
import System.Process.Typed
import System.IO.Temp
import System.IO
import Cabal.Plan
import NeatInterpolation
import Options.Applicative
import Types
import qualified MakeConstraints
import Utils
newtype BrokenPackages = BrokenPackages { getBrokenPackageNames :: S.Set PkgName }
deriving (Semigroup, Monoid)
failureExpected :: BrokenPackages -> PkgName -> Bool
failureExpected (BrokenPackages pkgs) name = name `S.member` pkgs
-- Packages that should be built by declaring a @build-tool-depends@
-- dependency, not a @build-depends@ dependency. This is necessary for packages
-- that do not have a library component (e.g., @alex@).
newtype BuildToolPackages = BuildToolPackages { getBuildToolPackageNames :: S.Set PkgName }
deriving (Semigroup, Monoid)
buildToolPackage :: BuildToolPackages -> PkgName -> Bool
buildToolPackage (BuildToolPackages pkgs) name = name `S.member` pkgs
data Config = Config { configPatchDir :: FilePath
, configCompiler :: FilePath
, configLoggingWrapper :: Maybe FilePath
, configGhcOptions :: [String]
, configCabalOptions :: [String]
, configOnlyPackages :: Maybe (S.Set Cabal.PackageName)
, configConcurrency :: Int
, configExtraCabalFragments :: [FilePath]
, configExtraPackages :: [(Cabal.PackageName, Version)]
, configTestPackages :: [(Cabal.PackageName, FilePath)]
, configExpectedBrokenPkgs :: BrokenPackages
, configBuildToolPkgs :: BuildToolPackages
}
cabalOptions :: Config -> [String]
cabalOptions cfg =
let
compilerOption =
maybe
[ "-w", configCompiler cfg ]
(\l -> [ "-w", l, "--with-hc-pkg", configCompiler cfg <> "-pkg" ])
(configLoggingWrapper cfg)
in
configCabalOptions cfg ++
compilerOption
config :: Parser TestPatches.Config
config =
TestPatches.Config
<$> patchDir
<*> compiler
<*> loggingWrapper
<*> ghcOptions
<*> cabalOptions
<*> onlyPackages
<*> concurrency
<*> extraCabalFragments
<*> extraPackages
<*> testPackages
<*> expectedBrokenPkgs
<*> buildToolPkgs
where
patchDir = option str (short 'p' <> long "patches" <> help "patch directory" <> value "./patches")
compiler = option str (short 'w' <> long "with-compiler" <> help "path of compiler")
loggingWrapper =
fmap Just (option str (long "logging-wrapper" <> help "path of compiler logging wrapper"))
<|> pure Nothing
ghcOptions = many $ option str (short 'f' <> long "ghc-option" <> help "flag to pass to compiler")
cabalOptions = many $ option str (short 'F' <> long "cabal-option" <> help "flag to pass to cabal-install")
onlyPackages =
fmap (Just . S.fromList) (some $ option pkgName (short 'o' <> long "only" <> help "filter packages"))
<|> pure Nothing
concurrency = option auto (short 'j' <> long "concurrency" <> value 1 <> help "number of concurrent builds")
extraCabalFragments = many $ option str (long "extra-cabal-fragment" <> help "path of extra configuration to include in cabal project files")
extraPackages = many $ option pkgVer (short 'P' <> long "extra-package" <> help "other, un-patched packages to test")
testPackages = many $ option pkgNamePath (short 'T' <> long "test-package" <> help "A package to run tests for")
expectedBrokenPkgs =
fmap (BrokenPackages . S.fromList) $ many
$ option
(fmap toPkgName pkgName)
(short 'b' <> long "expect-broken" <> metavar "PKGNAME" <> help "expect the given package to fail to build")
buildToolPkgs =
fmap (BuildToolPackages . S.fromList) $ many
$ option
(fmap toPkgName pkgName)
(long "build-tool-package" <> metavar "PKGNAME" <> help "declare the given package with build-tool-depends, not build-depends")
pkgVer :: ReadM (Cabal.PackageName, Version)
pkgVer = str >>= parse . T.pack
where
parse s
| [name, ver] <- T.splitOn "==" s
, Just ver' <- simpleParse $ T.unpack ver
= pure (Cabal.mkPackageName $ T.unpack name, ver')
| otherwise
= fail $ unlines
[ "Invalid extra package specified:"
, "expected to be in form of PKG_NAME==VERSION"
]
pkgNamePath :: ReadM (Cabal.PackageName, FilePath)
pkgNamePath = str >>= parse . T.pack
where
parse s
| [name, fp] <- T.splitOn "=" s
= pure (Cabal.mkPackageName $ T.unpack name, T.unpack fp)
| otherwise
= fail $ unlines
[ "Invalid test package specified:"
, "expected to be in form of PKG_NAME=FILEPATH"
]
pkgName :: ReadM Cabal.PackageName
pkgName = str >>= maybe (fail "invalid package name") pure . simpleParse
testPatches :: Config -> IO ()
testPatches cfg = do
setup cfg
compInfo <- getCompilerInfo cfg
packages <- findPatchedPackages (configPatchDir cfg)
packages <- return (packages ++ configExtraPackages cfg)
let packages' :: S.Set (Cabal.PackageName, Version)
packages'
| Just only <- configOnlyPackages cfg
= S.fromList $ filter (\(pname,_) -> pname `S.member` only) packages
| otherwise
= S.fromList packages
let build :: (Cabal.PackageName, Version) -> IO [TestedPatch LogOutput]
build (pname, ver) = do
res <- buildPackage cfg pname ver
let tpatch = TestedPatch { patchedPackageName = PkgName $ T.pack $ display pname
, patchedPackageVersion = Ver $ versionNumbers ver
, patchedPackageResult = res
}
return [tpatch]
testedPatches <- fold <$> mapConcurrentlyN (fromIntegral $ configConcurrency cfg) build (S.toList packages')
let test :: (Cabal.PackageName, FilePath) -> IO ([TestedPatch LogOutput])
test (pname, fpath) = do
res <- testPackage cfg (pname, fpath)
let tpatch = TestedPatch { patchedPackageName = PkgName $ T.pack $ display pname
, patchedPackageVersion = Ver $ []
, patchedPackageResult = res
}
return [tpatch]
testResults <- fold <$> mapM test (configTestPackages cfg)
let runResult = RunResult { testedPatches = testedPatches
, testedTests = testResults
, compilerInfo = compInfo
}
let (okay, msg) = resultSummary (configExpectedBrokenPkgs cfg) runResult
print msg
BSL.writeFile "results.json" . encode =<< writeLogs "logs" runResult
unless okay $ exitWith $ ExitFailure 1
writeLogs :: FilePath -> RunResult LogOutput -> IO (RunResult ())
writeLogs logDir runResult = do
createDirectoryIfMissing True logDir
let failedUnits = [ (unitId, log)
| (unitId, (buildInfo, result)) <- M.toList $ runResultUnits runResult
, Just log <- pure $
case result of
BuildSucceeded log -> Just log
BuildFailed log -> Just log
_ -> Nothing
]
mapM_ writeLog failedUnits
return (() <$ runResult)
where
writeLog (UnitId unitId, LogOutput log) = TIO.writeFile logPath log
where logPath = logDir </> T.unpack unitId
failedUnits :: BrokenPackages -> RunResult log
-> M.Map UnitId (BuildInfo, BuildResult log)
failedUnits broken = M.filter didFail . runResultUnits
where
didFail (buildInfo, result) =
case result of
BuildFailed _ -> not $ failureExpected broken (pkgName buildInfo)
_ -> False
planningErrors :: RunResult log -> [(PkgName, Ver)]
planningErrors runResult =
[ (patchedPackageName tpatch, patchedPackageVersion tpatch)
| tpatch <- testedPatches runResult ++ testedTests runResult
, PackagePlanningFailed _ <- pure $ patchedPackageResult tpatch
]
resultSummary :: forall log. BrokenPackages -> RunResult log -> (Bool, Doc)
resultSummary broken runResult = (ok, msg)
where
ok = null planningErrs
&& null failedTests
&& null failedTestsBuild
&& null failedUnits
msg = vcat
[ "Total packages built:" <+> pshow (length allUnits)
, ""
, pshow (length expectedPlanningErrs) <+> "had no valid install plan (expected):"
, PP.indent 4 $ vcat $ map (uncurry prettyPkgVer) expectedPlanningErrs
, ""
, pshow (length planningErrs) <+> "had no valid install plan:"
, PP.indent 4 $ vcat $ map (uncurry prettyPkgVer) planningErrs
, ""
, pshow (length failedUnits) <+> "packages failed to build:"
, PP.indent 4 $ vcat
[ prettyPkgVer (pkgName binfo) (version binfo)
| (binfo, _) <- M.elems failedUnits ]
, pshow (length expectedFailedUnits) <+> "packages failed to build (expected):"
, PP.indent 4 $ vcat
[ prettyPkgVer (pkgName binfo) (version binfo)
| (binfo, _) <- M.elems expectedFailedUnits ]
, pshow (length failedTargetUnits) <+> "target packages failed to build:"
, PP.indent 4 $ vcat
[ prettyPkgVer pkg ver
| (pkg, ver) <- failedTargetUnits ]
, ""
, pshow (length failedDependsUnits) <+> "packages failed to build due to unbuildable dependencies."
, ""
, pshow (length failedTestsBuild) <+> "testsuites failed build."
, PP.indent 4 $ vcat
[ prettyPkgName pkg_name | pkg_name <- failedTestsBuild ]
, pshow (length failedTests) <+> "testsuites failed."
, PP.indent 4 $ vcat
[ prettyPkgName pkg_name | pkg_name <- failedTests ]
]
allUnits = runResultUnits runResult
(expectedPlanningErrs, planningErrs) =
partition (failureExpected broken . fst) (planningErrors runResult)
failedTests = [ pkg_name | (TestedPatch pkg_name ver (PackageResult (PackageBuildSucceeded PackageTestsFailed) _)) <- testedTests runResult ]
failedTestsBuild = [ pkg_name | (TestedPatch pkg_name ver (PackageResult PackageBuildFailed _)) <- testedTests runResult ]
failedTargetUnits =
[ (patchedPackageName tp, patchedPackageVersion tp)
| tp <- testedPatches runResult
, not $ isSuccessfulPackageResult (patchedPackageResult tp)
]
failedUnits, expectedFailedUnits :: M.Map UnitId (BuildInfo, BuildResult log)
(expectedFailedUnits, failedUnits) = M.partition splitExpected (M.filter failed allUnits)
where failed (_, BuildFailed _) = True
failed _ = False
splitExpected (binfo, _) = failureExpected broken (pkgName binfo)
failedDependsUnits :: M.Map UnitId (S.Set UnitId)
failedDependsUnits = M.filter (not . S.null) (failedDeps allUnits)
toPkgName :: Cabal.PackageName -> PkgName
toPkgName = PkgName . T.pack . display
toVer :: Version -> Ver
toVer = Ver . versionNumbers
prettyPkgName :: PkgName -> Doc
prettyPkgName (PkgName pname) =
PP.blue (PP.text $ T.unpack pname)
-- | For @cabal-plan@ types.
prettyPkgVer :: PkgName -> Ver -> Doc
prettyPkgVer pname (Ver ver) =
prettyPkgName pname
<+> PP.green (PP.text $ intercalate "." $ map show ver)
-- | For @Cabal@ types.
prettyPackageVersion :: Cabal.PackageName -> Version -> Doc
prettyPackageVersion pname version =
prettyPkgVer (toPkgName pname) (toVer version)
buildPackage :: Config -> Cabal.PackageName -> Version -> IO (PackageResult LogOutput)
buildPackage cfg pname version = do
logMsg $ "=> Building" <+> prettyPackageVersion pname version
compilerId <- getCompilerId (configCompiler cfg)
-- prepare the test package
createDirectoryIfMissing True dirName
copyFile "cabal.project" (dirName </> "cabal.project")
appendFile (dirName </> "cabal.project") "packages: .\n"
appendFile (dirName </> "cabal.project") $ "package *\n ghc-options:" ++ unwords (configGhcOptions cfg)
TIO.writeFile
(dirName </> concat ["test-", display pname, ".cabal"])
(makeTestCabalFile cfg pname version)
-- run the build
code <- runProcess $ setWorkingDir dirName
$ proc "cabal"
$ ["new-build"] ++ cabalOptions cfg
whatHappened ("=> Build of" <+> prettyPackageVersion pname version) cfg pname dirName code Nothing
where
dirName = "test-" ++ display pname ++ "-" ++ display version
testPackage :: Config -> (Cabal.PackageName, FilePath) -> IO (PackageResult LogOutput)
testPackage cfg (pname, fpath) = do
logMsg $ "=> Testing" <+> prettyPackageName pname
-- prepare the test package
createDirectoryIfMissing True dirName
copyFile "cabal.project" (dirName </> "cabal.project")
appendFile (dirName </> "cabal.project") ("packages: " ++ fpath ++ "\n")
-- run the build
code <- runProcess $ setWorkingDir dirName
$ proc "cabal"
$ ["new-build", Cabal.unPackageName pname, "--enable-tests"] ++ cabalOptions cfg
case code of
ExitSuccess -> do
runCode <- runProcess $ setWorkingDir dirName
$ proc "cabal"
$ ["new-test", Cabal.unPackageName pname, "--enable-tests"] ++ cabalOptions cfg
whatHappened ("=> Test of" <+> prettyPackageName pname) cfg pname dirName code (Just runCode)
_ ->
whatHappened ("=> Test of" <+> prettyPackageName pname) cfg pname dirName code Nothing
where
dirName = "test-" ++ display pname
whatHappened herald cfg pname dirName code runCode = do
compilerId <- getCompilerId (configCompiler cfg)
let planPath = dirName </> "dist-newstyle" </> "cache" </> "plan.json"
planExists <- doesFileExist planPath
case planExists of
True -> do
Just plan <- decode <$> BSL.readFile planPath :: IO (Maybe PlanJson)
cabalDir <- getCabalDirectory
let logDir = cabalDir </> "logs" </> compilerId
results <- mapM (checkUnit logDir) (pjUnits plan)
logMsg $
let result = case fromMaybe code runCode of
ExitSuccess -> PP.cyan "succeeded"
ExitFailure n -> PP.red "failed" <+> PP.parens ("code" <+> pshow n)
in herald <+> result
-- N.B. we remove the build directory on failure to ensure
-- that we re-extract the source if the user re-runs after
-- modifying a patch.
unless (code == ExitSuccess) $ removeDirectoryRecursive dirName
return $ PackageResult codesToStatus (mergeInfoPlan (planToBuildInfo plan) results)
False -> do
logMsg $ PP.red $ "=> Planning for" <+> herald <+> "failed"
removeDirectoryRecursive dirName
return $ PackagePlanningFailed mempty
where
codesToStatus =
case code of
ExitSuccess -> PackageBuildSucceeded $
case runCode of
Nothing -> NoTests
Just rCode -> case rCode of
ExitSuccess -> PackageTestsSucceeded
_ -> PackageTestsFailed
_ -> PackageBuildFailed
planToBuildInfo :: PlanJson -> M.Map UnitId BuildInfo
planToBuildInfo plan = M.fromList
[ (uId unit, info)
| unit <- M.elems $ pjUnits plan
, let depends :: S.Set UnitId
depends = fold
[ ciLibDeps comp <> ciExeDeps comp
| comp <- M.elems $ uComps unit
]
, let PkgId pname pvers = uPId unit
, let info = BuildInfo { pkgName = pname
, version = pvers
, flags = uFlags unit
, dependencies = depends
}
]
checkUnit :: FilePath -> Unit -> IO (BuildResult LogOutput)
checkUnit logDir unit
| UnitTypeBuiltin <- uType unit = return BuildPreexisted
| UnitTypeLocal <- uType unit = return $ BuildSucceeded (LogOutput "<<inplace>>")
| otherwise = do
exists <- doesFileExist logPath
case exists of
True -> do
buildLog <- TE.decodeUtf8With TE.lenientDecode <$> BS.readFile logPath
let PkgId (PkgName unitPkgName) _pvers = uPId unit
if | T.null buildLog
-> return $ BuildFailed (LogOutput buildLog)
| any isInstallingLine $ take 20 $ reverse $ T.lines buildLog
-- Note that it's not enough to check for isInstallingLine, as
-- it's possible for packages with custom Setup.hs scripts to
-- fail even after installation has completed (e.g., Agda, as
-- reported in #47). But only apply this check to the package
-- being tested, as we only want to label the tested package as
-- failing, not any of its dependencies.
, not (Cabal.unPackageName pname == T.unpack unitPkgName) ||
isPackageBuildSucceeded codesToStatus
-> return $ BuildSucceeded (LogOutput buildLog)
| otherwise
-> return $ BuildFailed (LogOutput buildLog)
False -> return BuildNotAttempted
where
isInstallingLine line = "Installing" `T.isPrefixOf` line
logPath =
case uId unit of
UnitId uid -> logDir </> T.unpack uid <.> "log"
mergeInfoPlan :: Ord k
=> M.Map k BuildInfo
-> M.Map k (BuildResult log)
-> M.Map k (BuildInfo, BuildResult log)
mergeInfoPlan = M.merge err err (M.zipWithMatched $ \_ x y -> (x,y))
where
err = M.mapMissing $ \_ _ -> error "error merging"
makeTestCabalFile :: Config -> Cabal.PackageName -> Version -> T.Text
makeTestCabalFile cfg pname' ver' =
[text|
cabal-version: 2.2
name: test-$pname
version: 1.0
library
exposed-modules:
$depends
default-language: Haskell2010
|]
where
pname = T.pack $ display pname'
ver = T.pack $ display ver'
depends | buildToolPackage (configBuildToolPkgs cfg) (toPkgName pname')
= "build-tool-depends: " <>
pname <> ":" <> pname <> " == " <> ver
| otherwise
= "build-depends: " <> pname <> " == " <> ver
getCompilerInfo :: Config -> IO CompilerInfo
getCompilerInfo cfg = do
(out,err) <- readProcess_ $ proc (configCompiler cfg) ["--info"]
BSL.writeFile "compiler-info" out
return $ CompilerInfo $ read $ T.unpack $ TE.decodeUtf8 $ BSL.toStrict out
setup :: Config -> IO ()
setup cfg = do
keysExist <- doesDirectoryExist "keys"
unless keysExist $ do
cabalDir <- getCabalDirectory
-- Work around cabal-install bug; it seems to get confused by repository changes
removePathForcibly $ cabalDir </> "packages" </> repoName
createDirectoryIfMissing True $ cabalDir </> "packages" </> repoName
runProcess_ $ proc "build-repo.sh" ["gen-keys"]
cwd <- getCurrentDirectory
environ <- getEnvironment
let env = environ ++
[ ("REPO_NAME", repoName)
, ("REPO_URL", "file://" ++ (cwd </> "repo"))
, ("PATCHES", configPatchDir cfg)
]
removePathForcibly "cabal.project"
runProcess_
$ setEnv env
$ proc "build-repo.sh" ["build-repo"]
projectFile <- openFile "cabal.project" WriteMode
runProcess_
$ setStdout (useHandleClose projectFile)
$ setEnv env
$ proc "build-repo.sh" ["build-repository-blurb"]
extraFragments <- mapM readFile (configExtraCabalFragments cfg)
constraints <- MakeConstraints.makeConstraints (configPatchDir cfg)
appendFile "cabal.project" $ show $ vcat $
[ "with-compiler: " <> PP.text (configCompiler cfg)
, constraints
] ++ map PP.text extraFragments
runProcess_ $ proc "cabal" ["new-update"]
-- Force cabal to rebuild the index cache.
buildPackage cfg "acme-box" (mkVersion [0,0,0,0])
return ()
where
repoName = "local"
-- | Compute for each unit which of its dependencies failed to build.
failedDeps :: M.Map UnitId (BuildInfo, BuildResult log) -> M.Map UnitId (S.Set UnitId)
failedDeps pkgs =
let res = fmap f pkgs -- N.B. Knot-tied
f :: (BuildInfo, BuildResult log) -> S.Set UnitId
f (binfo, result) =
failedDirectDeps <> failedTransDeps
where
failedTransDeps = S.unions $ map (res M.!) (S.toList $ dependencies binfo)
failedDirectDeps = S.filter failed $ S.filter excludeSelf (dependencies binfo)
-- We don't want failures of units in the same package to count as
-- failed dependencies.
excludeSelf :: UnitId -> Bool
excludeSelf unitId = pkgName binfo /= pkgName binfo'
where (binfo', _) = pkgs M.! unitId
failed :: UnitId -> Bool
failed unitId =
case snd $ pkgs M.! unitId of
BuildFailed _ -> True
_ -> False
in res
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
module Types
( RunResult(..)
, PackageStatus(..)
, isPackageBuildSucceeded
, PackageTestStatus(..)
, runResultUnits
, TestedPatch(..)
, PackageResult(..)
, isSuccessfulPackageResult
, BuildInfo(..)
, BuildResult(..)
, LogOutput(..)
, CompilerInfo(..)
) where
import Cabal.Plan
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import Data.Aeson
import qualified Data.Text as T
import GHC.Generics
import Cabal.Plan
-- | Information about a unit which we attempted to build.
data BuildInfo
= BuildInfo { pkgName :: PkgName
, version :: Ver
, flags :: M.Map FlagName Bool
, dependencies :: S.Set UnitId
}
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
-- | The result of a unit build.
data BuildResult log
= BuildSucceeded { buildLog :: log }
-- ^ the build succeeded.
| BuildPreexisted
-- ^ the unit pre-existed in the global package database.
| BuildFailed { buildLog :: log }
-- ^ the build failed
| BuildNotAttempted
-- ^ the build was not attempted either because a dependency failed or it
-- is an executable or testsuite component
deriving stock (Show, Generic, Functor, Foldable, Traversable)
deriving anyclass (ToJSON, FromJSON)
data PackageTestStatus = NoTests | PackageTestsFailed | PackageTestsSucceeded
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
data PackageStatus = PackageBuildFailed | PackageBuildSucceeded PackageTestStatus
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
isPackageBuildSucceeded :: PackageStatus -> Bool
isPackageBuildSucceeded PackageBuildSucceeded{} = True
isPackageBuildSucceeded PackageBuildFailed = False
-- | The result of an attempt to tested a patch
data PackageResult log
= PackagePlanningFailed { planningError :: T.Text }
-- ^ Our attempt to build the package resulting in no viable install plan.
| PackageResult { packageStatus :: PackageStatus
, units :: M.Map UnitId (BuildInfo, BuildResult log)
}
-- ^ We attempted to build the package.
deriving stock (Show, Generic, Functor, Foldable, Traversable)
deriving anyclass (ToJSON, FromJSON)
isSuccessfulPackageResult :: PackageResult log -> Bool
isSuccessfulPackageResult PackagePlanningFailed{} = False
isSuccessfulPackageResult PackageResult{packageStatus} = isPackageBuildSucceeded packageStatus
-- | Information about a patch which we tested.
data TestedPatch log
= TestedPatch { patchedPackageName :: PkgName
, patchedPackageVersion :: Ver
, patchedPackageResult :: PackageResult log
}
deriving stock (Show, Generic, Functor, Foldable, Traversable)
deriving anyclass (ToJSON, FromJSON)
-- | The result of a CI run.
data RunResult log
= RunResult { testedPatches :: [TestedPatch log]
, testedTests :: [TestedPatch log]
, compilerInfo :: CompilerInfo
}
deriving stock (Show, Generic, Functor, Foldable, Traversable)
deriving anyclass (ToJSON, FromJSON)
runResultUnits :: RunResult log -> M.Map UnitId (BuildInfo, BuildResult log)
runResultUnits runResult = M.unions
[ units
| tpatch <- testedPatches runResult ++ testedTests runResult
, PackageResult _ units <- pure $ patchedPackageResult tpatch
]
-- | Logged output from a build.
newtype LogOutput = LogOutput { getLogOutput :: T.Text }
deriving stock (Eq, Ord, Show)
deriving newtype (ToJSON, FromJSON)
newtype CompilerInfo = CompilerInfo [(String, String)]
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
module Utils where
import Control.Monad
import qualified Distribution.Package as Cabal
import Distribution.Text
import Distribution.Types.Version hiding (showVersion)
import qualified Data.ByteString.Lazy.Char8 as BSL
import System.Directory
import System.FilePath
import System.Process.Typed
import Control.Exception (bracket_)
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Concurrent.STM.TSem
import Text.PrettyPrint.ANSI.Leijen (Doc)
import qualified Text.PrettyPrint.ANSI.Leijen as PP
parsePatchName :: FilePath -> Maybe (Cabal.PackageName, Version)
parsePatchName fname = do
pid <- simpleParse (takeBaseName fname) :: Maybe Cabal.PackageId
let pname = Cabal.packageName pid
ver = Cabal.packageVersion pid
guard $ not $ null $ versionNumbers ver
return (pname, ver)
findPatchedPackages :: FilePath -- ^ patch directory
-> IO [(Cabal.PackageName, Version)]
findPatchedPackages patchDir = do
patchFiles <- listDirectory patchDir
return [ (pname, ver)
| fname <- patchFiles
, let err = error $ "Invalid patch file name: " ++ fname
, (pname, ver) <- maybe err pure $ parsePatchName fname
]
getCompilerId :: FilePath -> IO String
getCompilerId ghcPath = do
(_, out) <- readProcessStdout $ proc ghcPath ["--info"]
let parsed = read $ BSL.unpack out :: [(String, String)]
case lookup "Project version" parsed of
Just val -> return ("ghc-" ++ val)
Nothing -> fail "error fetching compiler id"
getCabalDirectory :: IO FilePath
getCabalDirectory = getAppUserDataDirectory "cabal"
mapConcurrentlyN :: Integer -> (a -> IO b) -> [a] -> IO [b]
mapConcurrentlyN n f xs = do
sem <- atomically $ newTSem n
let g x = bracket_ (atomically $ waitTSem sem) (atomically $ signalTSem sem) (f x)
xs' <- mapM (async . g) xs
mapM wait xs'
logMsg :: Doc -> IO ()
logMsg msg = print msg
pshow :: Show a => a -> Doc
pshow = PP.text . show
prettyVersion :: Version -> Doc
prettyVersion =
PP.hcat . PP.punctuate (PP.text ".") . map pshow . versionNumbers
prettyPackageName :: Cabal.PackageName -> Doc
prettyPackageName =
PP.text . Cabal.unPackageName
{ pkgs }:
# Maps Haskell package names to a list of the Nixpkgs attributes corresponding
# to their native library dependencies.
with pkgs;
{
zlib = [ zlib ];
digest = [ zlib ];
regex-pcre = [ pcre ];
bzlib = [ bzip2 ];
hmatrix = [ blas liblapack ];
hexpat = [ expat ];
hgmp = [ gmp ];
posix-api = [ systemd ];
lame = [ lame ];
}
......@@ -9,13 +9,19 @@
set -e
cipher=aes-256-cbc
if [ -z "$PATCHES" ]; then PATCHES=./patches; fi
log() {
echo "$1"
}
# For use by administrator.
gen_keys_tarball() {
hackage-repo-tool create-keys --keys=./keys
pass="$(pwgen 32 1)"
tar -c keys | openssl enc -$cipher -e -k "$pass" > keys.tar.enc
tar -c keys | openssl enc -$cipher -pbkdf2 -e -k "$pass" > keys.tar.enc
echo "Wrote ./keys.tar.enc"
echo "$pass" > keys.key
echo "KEYS_TARBALL_KEY = $pass"
}
......@@ -30,26 +36,39 @@ extract_keys_tarball() {
exit 1
fi
curl $KEYS_TARBALL | openssl enc -$cipher -d -k "$KEYS_TARBALL_KEY" | tar -x
curl $KEYS_TARBALL | openssl enc -$cipher -pbkdf2 -d -k "$KEYS_TARBALL_KEY" | tar -x
if [ ! -d ./keys ]; then
echo "Key tarball extraction failed"
exit 1
fi
}
build_index() {
build_repository_blurb() {
local keys="$(find keys/root -type f -printf "%f")"
local commit="$CI_COMMIT_SHA"
local commit_url="https://gitlab.haskell.org/ghc/head.hackage/commit/$commit"
local newline=$'\n'
sed -e 's/ \+$//' >repo/cabal.project.local <<EOF
repository head.hackage.ghc.haskell.org
url: https://ghc.gitlab.haskell.org/head.hackage/
if [ -z "$REPO_NAME" ]; then
REPO_NAME="head.hackage.ghc.haskell.org"
fi
if [ -z "$REPO_URL" ]; then
REPO_URL="https://ghc.gitlab.haskell.org/head.hackage/"
fi
sed -e 's/ \+$//' <<EOF
repository $REPO_NAME
url: $REPO_URL
secure: True
key-threshold: 3
root-keys:
${keys//.private/$newline }
EOF
}
build_index_page() {
local commit="$CI_COMMIT_SHA"
local commit_url="https://gitlab.haskell.org/ghc/head.hackage/commit/$commit"
build_repository_blurb >repo/repo.cabal.project
cat repo/repo.cabal.project > repo/cabal.project
build_constraints >> repo/cabal.project
cat >repo/ci.html <<EOF
<!DOCTYPE html>
......@@ -106,23 +125,25 @@ EOF
<p>The source of this package repository is at <a href="https://gitlab.haskell.org/ghc/head.hackage">gitlab.haskell.org/ghc/head.hackage</a>.
<p>To use package repository with <code>cabal-install</code> add the following
to your project's <code>cabal.project.local</code> and run
<code>cabal v2-update</code>: (consider using <code>scripts/head.hackage.sh update</code> as <code>v2-update</code> is broken, <a href="https://github.com/haskell/cabal/issues/5952">Cabal bug #5952</a>)
<p>To use package repository with <code>cabal-install >= 3.6</code> simply run the following:
<pre><code>
$(cat repo/cabal.project.local)
$ curl https://ghc.gitlab.haskell.org/head.hackage/cabal.project >> cabal.project.local
$ cabal update
</code></pre>
<p>Finally, you may want to add the <a
href="cabal.constraints">constraints</a> to your project to ensure that
cabal chooses the patched releases.
<p>This will add the following <code>source-repository</code> stanza to your project's <code>cabal.project.local</code>:
<pre><code>
$(cat repo/repo.cabal.project)
</code></pre>
as well as the version constraints in <a
href="cabal.constraints"><code>cabal.constraints</code></a>.
<p>If you find a package that doesn't build with a recent GHC
pre-release see the <a
href="https://gitlab.haskell.org/ghc/head.hackage#adding-a-patch">contributor
href="https://gitlab.haskell.org/ghc/head.hackage/-/blob/master/README.md#adding-a-patch">contributor
documentation</a> for instructions on how to contribute a patch.
<p>If you encounter other trouble refer to the
<p>If you encounter other trouble refer to the
<a href="https://gitlab.haskell.org/ghc/head.hackage">head.hackage
documentation</a> or
<a href="https://gitlab.haskell.org/ghc/head.hackage/issues">let us know</a>.
......@@ -138,61 +159,72 @@ $(cat repo/cabal.project.local)
EOF
}
split_pkg_version() {
package=$(echo $1 | sed 's/\(.\+\)-\([0-9]\+\(\.[0-9]\+\)*\)/\1/')
version=$(echo $1 | sed 's/\(.\+\)-\([0-9]\+\(\.[0-9]\+\)*\)/\2/')
}
build_constraints() {
cat <<EOF
allow-newer: *:base
allow-newer: *:template-haskell
allow-newer: *:time
allow-newer: *:Cabal
allow-newer: *:ghc
EOF
echo "constraints:"
for f in $(ls patches); do
split_pkg_version $(basename $(basename $f .patch) .cabal)
echo " $package ==$version"
done
head-hackage-ci make-constraints $PATCHES
}
# Build the hackage repository
build_repo() {
extract_keys_tarball
log "Building Hackage repository in $(pwd)/repo..."
# hackage-repo-tool bootstrap fails unless there is at least one package in the
# repo. Seed things with acme-box.
log "Fetching acme-box..."
cabal update
cabal fetch acme-box-0.0.0.0
mkdir -p repo/package
cp $HOME/.cabal/packages/hackage.haskell.org/acme-box/0.0.0.0/acme-box-0.0.0.0.tar.gz repo/package
# if ~/.cabal exists cabal-install will use that, otherwise packages go into $XDG_CACHE_HOME/cabal
if [ -d "$HOME/.cabal" ]; then
cp "$HOME/.cabal/packages/hackage.haskell.org/acme-box/0.0.0.0/acme-box-0.0.0.0.tar.gz" repo/package
else
cp "${XDG_CACHE_HOME:-$HOME/.cache}/cabal/packages/hackage.haskell.org/acme-box/0.0.0.0/acme-box-0.0.0.0.tar.gz" repo/package
fi
log "Bootstrapping repository..."
hackage-repo-tool bootstrap --keys=./keys --repo=./repo
mkdir -p template patches.cache
log "Patching packages..."
mkdir -p tmp template tmp/patches.cache
cp -R $PATCHES tmp/patches
tool \
--patches=./patches \
--patches=./tmp/patches \
--repo-cache=./cache \
--keys=./keys \
--repo-name=head.hackage \
--repo-url=http://hackage.haskell.org/ \
--template=template \
./repo
log "Building constraints..."
build_constraints > repo/cabal.constraints
build_index
log "Building index page..."
build_index_page
rm -R tmp
}
case $1 in
gen-keys) gen_keys_tarball ;;
extract-keys) extract_keys_tarball ;;
build-repo) build_repo ;;
build-constraints) build_constraints ;;
build-repository-blurb) build_repository_blurb ;;
build-index)
build_constraints > repo/cabal.constraints
build_index ;;
build_index_page ;;
*)
echo "Unknown command $1"
echo "error: Unknown command $1."
echo
echo "Usage: $0 [command]"
echo
echo "Commands:"
echo " gen-keys"
echo " extract-keys"
echo " build-repo"
echo " build-constraints"
echo " build-repository-blurb"
echo " build-index"
exit 1
;;
esac
packages: .
source-repository-package
type: git
location: https://gitlab.haskell.org/ghc/hackage-overlay-repo-tool
tag: 52f54229b08c6e86dd163dd42a78b22c10ffb099