Commit 1b616c6c authored by Ben Gamari's avatar Ben Gamari 🐢

A new CI story

The Problem
-----------

When I started looking at the problem of providing CI for `head.hackage`
I considered two possible designs:

 1. Build upon `cabal-install`
 2. Build upon Nix's Haskell infrastructure

While I preferred (1), I found that integrating with `cabal-install` was
quite difficult:

 * it [does not produce
   logs](https://github.com/haskell/cabal/issues/5901) for local packages,
   which was the obvious way to incorporate patched packages into the
   build plan

 * it is difficult to reconstruct why a package build failed (e.g. due
   to a planning failure, dependency failing to build, or an error in
   the package itself)

For these reasons it so happened that (2) ended up being a tad easier to
implement. However, it suffers from a number of problems:

 * Nix's Haskell infrastructure doesn't handle multiple versions of a
   single package at all, yet we now have patches for multiple package
   versions in `head.hackage`

 * Nix's Haskell infrastructure doesn't handle flags, which can
   complicate building some packages

 * The Nix expressions ended up being rather difficult to maintain

The Solution
------------

This MR moves the CI infrastructure back in the direction of (1),
facilitated by workarounds that I found for the two issues described
above.

The infrastructure revolves around the `head-hackage-ci` executable
which provides a `test-patches` mode which `gitlab-ci.yml` invokes
thusly:
```
head-hackage-ci test-patches --patches=./patches --with-compiler=$GHC
```
This mode does several things:

 1. Builds a local package repository (using the same script used to
    build `https://ghc.gitlab.haskell.org/head.hackage/`). (N.B. by
    pulling patched packages from a proper repository instead of using
    local packages we side-step the fact that `cabal-install` doesn't
    produce logs for local packages)
 2. Generate a `cabal.project` file containing:

    * a `remote-repository` stanza referring to this repository
    * constraints to ensure that we only choose patched package versions
    * some additional `package` stanzas to ensure that `Cabal` can find
      native library dependencies (these are defined in
      `ci/build-deps.nix`)

 3. Run `cabal new-update` (as well as perform a dummy build of the
    `acme-box` package to ensure that the package index cache is built,
    otherwise parallel builds can randomly fail)
 4. For each patched create a new working directory containing:

    * the previously generated `cabal.project` file
    * a `test-$PKGNAME.cabal` file defining a dummy package depending
      upon the library

    and perform the build. We use some heuristics depending upon:

    * the `plan.json` file
    * which log files exist
    * the contents of said log files

    to sort out what happened.
 6. After all the packages have been built produce a final report of the
    result.

While this is admittedly pretty hacky, in truth it's no worse than the
somersaults which we had to perform in the Nix infrastructure. Reliably
introspecting on failed builds seems to be messy business no matter
which build system you use.
parent 111b9172
......@@ -48,15 +48,6 @@ build:
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
......@@ -68,35 +59,33 @@ build:
fi
- 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
--arg extraHcOpts "\"$EXTRA_HC_OPTS\""
|| { echo "Build failed!"; ret=1; }
- scripts/summarize.py || echo "summarize script failed"
- exit $ret
- |
nix build \
-f https://github.com/mpickering/ghc-artefact-nix/archive/master.tar.gz \
--argstr url $GHC_TARBALL \
--out-link ghc \
ghcHEAD
- GHC=`pwd`/ghc/bin/ghc
- mkdir -p tmp; cd tmp
- |
if [ -n "$EXTRA_HC_OPTS" ]; then
EXTRA_OPTS="--ghc-option=\"$EXTRA_HC_OPTS\""
fi
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
nix eval --raw -f ../ci cabalDepsSrc > deps.cabal.project
nix run -f ../ci -c \
head-hackage-ci \
test-patches \
--extra-cabal-fragment=$(pwd)/deps.cabal.project \
--patches=../patches \
--with-compiler=$GHC \
$EXTRA_OPTS
- nix run -f ../ci -c xz results.json
artifacts:
when: always
paths:
- logs.tar.gz
- summary.json
- summary.dot
- summary.dot.svg
- results.json.xz
# Build and deploy a Hackage repository
update-repo:
......@@ -119,7 +108,7 @@ update-repo:
- 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 scripts/build-repo.nix -c build-repo.sh extract-build-repo
- nix run -f '<nixpkgs>' gnutar -c tar -zxf logs.tar.gz
- mv logs repo
- cp summary.dot.svg repo
......
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
bootPkgs :: S.Set Cabal.PackageName
bootPkgs = S.fromList
[ "base"
, "template-haskell"
, "time"
, "Cabal"
, "ghc"
, "ghc-prim"
]
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
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 doc = PP.vcat
[ allowNewer bootPkgs
, ""
, versionConstraints patches
]
return doc
import Distribution.Simple
main = defaultMain
This diff is collapsed.
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}
module Types
( RunResult(..)
, TestedPatch(..)
, PackageResult(..)
, BuildInfo(..)
, BuildResult(..)
) 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
= BuildSucceeded { buildLog :: T.Text }
-- ^ the build succeeded.
| BuildPreexisted
-- ^ the unit pre-existed in the global package database.
| BuildFailed { buildLog :: T.Text }
-- ^ 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)
deriving anyclass (ToJSON, FromJSON)
-- | The result of an attempt to tested a patch
data PackageResult
= PackagePlanningFailed { planningError :: T.Text }
-- ^ Our attempt to build the package resulting in no viable install plan.
| PackageResult { packageBuilt :: Bool
, units :: M.Map UnitId (BuildInfo, BuildResult)
}
-- ^ We attempted to build the package.
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
-- | Information about a patch which we tested.
data TestedPatch
= TestedPatch { patchedPackageName :: PkgName
, patchedPackageVersion :: Ver
, patchedPackageResult :: PackageResult
}
deriving stock (Show, Generic)
deriving anyclass (ToJSON, FromJSON)
-- | The result of a CI run.
data RunResult
= RunResult { testedPatches :: [TestedPatch] }
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
# To update nixpkgs bump rev and baseNixpkgs's sha256.
let
rev = "c2fd152c98dc618c9c06b1551faee17e79a03b7f";
in
fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/${rev}.tar.gz";
sha256 = "07cafslsgdymzcfcpgzw8fqv07r4wslsz7xijni1l1n2agcj2436";
}
{ pkgs }:
let
# Maps Haskell package names to a list of their native library dependency
# attributes.
pkgDeps= with pkgs;
{
zlib = [ zlib ];
digest = [ zlib ];
};
mkCabalFragment = pkgName: deps:
with pkgs.lib;
let
libDirs = concatStringsSep " " (map (dep: getOutput "lib" dep + "/lib") deps);
includeDirs = concatStringsSep " " (map (dep: getOutput "dev" dep + "/include") deps);
in ''
package ${pkgName}
extra-lib-dirs: ${libDirs}
extra-include-dirs: ${includeDirs}
'';
in
pkgs.lib.concatStringsSep "\n" (pkgs.lib.mapAttrsToList mkCabalFragment pkgDeps)
......@@ -9,6 +9,7 @@
set -e
cipher=aes-256-cbc
if [ -z "$PATCHES" ]; then PATCHES=./patches; fi
# For use by administrator.
gen_keys_tarball() {
......@@ -16,6 +17,7 @@ gen_keys_tarball() {
pass="$(pwgen 32 1)"
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"
}
......@@ -37,19 +39,30 @@ extract_keys_tarball() {
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() {
local commit="$CI_COMMIT_SHA"
local commit_url="https://gitlab.haskell.org/ghc/head.hackage/commit/$commit"
build_repository_blurb >repo/cabal.project.local
cat >repo/ci.html <<EOF
<!DOCTYPE html>
......@@ -122,7 +135,7 @@ $(cat repo/cabal.project.local)
href="https://gitlab.haskell.org/ghc/head.hackage#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>.
......@@ -139,13 +152,11 @@ EOF
}
build_constraints() {
gen-constraints.py
head-hackage-ci make-constraints $PATCHES
}
# Build the hackage repository
build_repo() {
extract_keys_tarball
# hackage-repo-tool bootstrap fails unless there is at least one package in the
# repo. Seed things with acme-box.
cabal update
......@@ -153,6 +164,7 @@ build_repo() {
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
cp -R $PATCHES patches
hackage-repo-tool bootstrap --keys=./keys --repo=./repo
mkdir -p template patches.cache
......@@ -172,11 +184,26 @@ case $1 in
gen-keys) gen_keys_tarball ;;
extract-keys) extract_keys_tarball ;;
build-repo) build_repo ;;
extract-build-repo)
extract_keys_tarball
build_repo ;;
build-constraints) build_constraints ;;
build-repository-blurb) build_repository_blurb ;;
build-index)
build_constraints > repo/cabal.constraints
build_index ;;
*)
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
{ nixpkgs ? (import <nixpkgs> {}) }:
{ nixpkgs ? (import (import ./base-nixpkgs.nix) {}) }:
with nixpkgs;
let
......@@ -17,27 +17,38 @@ let
src = fetchFromGitHub {
owner = "bgamari";
repo = "hackage-overlay-repo-tool";
rev = "304023e541d4a417c1ec9a22610b806d5505228d";
sha256 = "1vnb1c6w5b6snz09c1avzs5wkw8l794b0fn7zpmy42hlh4nhdp0d";
rev = "18eb61c830ad908d36d343f400a1588af6b9a03a";
sha256 = "1y1fw5x9lyd533lm67s7iyzb4640y8lya11sdjia0yd1j5if6s40";
};
in haskellPackages.callCabal2nix "hackage-overlay-repo-tool" src {};
head-hackage-ci =
haskellPackages.callCabal2nix "head-hackage-ci" ./. {};
buildDeps = import ./build-deps.nix { pkgs = nixpkgs; };
buildDepsFile = pkgs.writeText "deps.cabal.project" buildDeps;
build-repo =
let
deps = [
bash curl gnutar findutils patch rsync openssl
cabal-install ghc
cabal-install ghc gcc binutils-unwrapped pwgen gnused
hackage-repo-tool overlay-tool python3
];
in
runCommand "repo" {
nativeBuildInputs = [ makeWrapper ];
cabalDepsSrc = buildDeps;
} ''
mkdir -p $out/bin
makeWrapper ${./build-repo.sh} $out/bin/build-repo.sh \
makeWrapper ${head-hackage-ci}/bin/head-hackage-ci $out/bin/head-hackage-ci \
--prefix PATH : ${stdenv.lib.makeBinPath deps}:$out/bin
makeWrapper ${./gen-constraints.py} $out/bin/gen-constraints.py \
makeWrapper ${./build-repo.sh} $out/bin/build-repo.sh \
--prefix PATH : ${stdenv.lib.makeBinPath deps}:$out/bin
makeWrapper ${xz}/bin/xz $out/bin/xz
'';
in
build-repo
cabal-version: >=1.10
name: head-hackage-ci
version: 0.1.0.0
synopsis: Test driver for head.hackage
-- description:
-- bug-reports:
license: BSD3
license-file: LICENSE
author: Ben Gamari
maintainer: ben@smart-cactus.org
copyright: (c) 2019 Ben Gamari
build-type: Simple
executable head-hackage-ci
main-is: Main.hs
other-modules: Types, Utils, TestPatches, MakeConstraints, LatestVersion
build-depends:
Cabal,
aeson,
ansi-wl-pprint,
async,
base >=4.11 && <4.13,
bytestring,
cabal-plan,
containers,
directory,
filepath,
lens,
neat-interpolation,
optparse-applicative,
stm,
temporary,
text,
typed-process,
wreq
ghc-options: -threaded
default-language: Haskell2010
......@@ -2,7 +2,7 @@
"owner": "commercialhaskell",
"repo": "all-cabal-hashes",
"branch": "hackage",
"message": "Update from Hackage at 2019-07-29T18:23:42Z",
"rev": "8cb9e194879225aab2a35838b4ab4b53f85d8eb8",
"sha256": "1krylhakvipxy2yiiswy2jq0f1giczx3y9gy335kq5r84brpar3v"
"message": "Update from Hackage at 2019-10-04T19:39:33Z",
"rev": "225a775bc1a9304cdce5b01c514336ef3f5d5877",
"sha256": "0xfkcv2lk22afpgpddp4fc566w175341nc9d8mkb2vp0isd6xalz"
}
#!/usr/bin/env python3
import re
from pathlib import Path
from typing import NewType, Dict, Set, List
Version = NewType("Version", str)
PkgName = NewType("PkgName", str)
BOOT_PKGS = {
"base",
"template-haskell",
"time",
"Cabal",
"ghc",
} # type: Set[PkgName]
PATCHES = Path('patches')
def split_pkg_version(pkg_name_ver: str):
m = re.match(r'(.+)-([0-9]+(.[0-9]+)*)', pkg_name_ver)
if m is None:
return None
else:
return (m.group(1), m.group(2))
def get_pkg_versions(patches: Path) -> Dict[PkgName, Set[Version]]:
res = {} # type: Dict[PkgName, Set[Version]]
for filename in patches.iterdir():
pkg_name, version = split_pkg_version(filename.stem)
if pkg_name not in res:
res[pkg_name] = set() # type: Set[Version]
res[pkg_name].add(version)
return res
def main():
for pkg in BOOT_PKGS:
print(f"allow-newer: *:{pkg}")
print('constraints:')
pkg_versions = get_pkg_versions(PATCHES)
constraints = [] # type: List[str]
for pkg_name, versions in pkg_versions.items():
versions_str = ' || '.join(f'=={ver}' for ver in versions)
constraints.append(f' {pkg_name} {versions_str}')
print(',\n'.join(constraints))
if __name__ == '__main__':
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment