Ben Gamari pushed to branch wip/boot-lib-testing at Glasgow Haskell Compiler / GHC

Commits:

15 changed files:

Changes:

  • .gitlab-ci.yml
    ... ... @@ -205,6 +205,25 @@ not-interruptible:
    205 205
         - if: $NIGHTLY
    
    206 206
           when: always
    
    207 207
     
    
    208
    +.nix:
    
    209
    +  image: nixos/nix:2.25.2
    
    210
    +  variables:
    
    211
    +    LANG: "C.UTF-8"
    
    212
    +  before_script:
    
    213
    +    - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
    
    214
    +    # Note [Nix-in-Docker]
    
    215
    +    # ~~~~~~~~~~~~~~~~~~~~
    
    216
    +    # The nixos/nix default config is max-jobs=1 and cores=$(logical
    
    217
    +    # cores num) which doesn't play nice with our $CPUS convention. We
    
    218
    +    # fix it before invoking any nix build to avoid oversubscribing
    
    219
    +    # while allowing a reasonable degree of parallelism.
    
    220
    +    # FIXME: Disabling build-users-group=nixbld is a workaround for a Nix-in-Docker issue. See
    
    221
    +    # https://gitlab.haskell.org/ghc/head.hackage/-/issues/38#note_560487 for
    
    222
    +    # discussion.
    
    223
    +    - echo "cores = $CPUS" >> /etc/nix/nix.conf
    
    224
    +    - echo "max-jobs = $CPUS" >> /etc/nix/nix.conf
    
    225
    +    - nix run nixpkgs#gnused -- -i -e 's/ nixbld//' /etc/nix/nix.conf
    
    226
    +
    
    208 227
     
    
    209 228
     ############################################################
    
    210 229
     # Validate jobs
    
    ... ... @@ -255,6 +274,24 @@ typecheck-testsuite:
    255 274
         - mypy testsuite/driver/runtests.py
    
    256 275
       dependencies: []
    
    257 276
     
    
    277
    +lint-boot-tags:
    
    278
    +  extends: [.lint, .nix]
    
    279
    +  script:
    
    280
    +    - nix run ./utils/check-submodules# -- check-tags
    
    281
    +  rules:
    
    282
    +    - if: $RELEASE_JOB
    
    283
    +      allow_failure: false
    
    284
    +    - allow_failure: true
    
    285
    +
    
    286
    +lint-boot-versions:
    
    287
    +  extends: [.lint, .nix]
    
    288
    +  script:
    
    289
    +    - nix run ./utils/check-submodules# -- check-versions
    
    290
    +  rules:
    
    291
    +    - if: $RELEASE_JOB
    
    292
    +      allow_failure: false
    
    293
    +    - allow_failure: true
    
    294
    +
    
    258 295
     # We allow the submodule checker to fail when run on merge requests (to
    
    259 296
     # accommodate, e.g., haddock changes not yet upstream) but not on `master` or
    
    260 297
     # Marge jobs.
    
    ... ... @@ -292,26 +329,11 @@ lint-author:
    292 329
         - *drafts-can-fail-lint
    
    293 330
     
    
    294 331
     lint-ci-config:
    
    295
    -  image: nixos/nix:2.25.2
    
    296
    -  extends: .lint
    
    332
    +  extends: [.lint, .nix]
    
    297 333
       # We don't need history/submodules in this job
    
    298 334
       variables:
    
    299 335
         GIT_DEPTH: 1
    
    300 336
         GIT_SUBMODULE_STRATEGY: none
    
    301
    -  before_script:
    
    302
    -    - echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf
    
    303
    -    # Note [Nix-in-Docker]
    
    304
    -    # ~~~~~~~~~~~~~~~~~~~~
    
    305
    -    # The nixos/nix default config is max-jobs=1 and cores=$(logical
    
    306
    -    # cores num) which doesn't play nice with our $CPUS convention. We
    
    307
    -    # fix it before invoking any nix build to avoid oversubscribing
    
    308
    -    # while allowing a reasonable degree of parallelism.
    
    309
    -    # FIXME: Disabling build-users-group=nixbld is a workaround for a Nix-in-Docker issue. See
    
    310
    -    # https://gitlab.haskell.org/ghc/head.hackage/-/issues/38#note_560487 for
    
    311
    -    # discussion.
    
    312
    -    - echo "cores = $CPUS" >> /etc/nix/nix.conf
    
    313
    -    - echo "max-jobs = $CPUS" >> /etc/nix/nix.conf
    
    314
    -    - nix run nixpkgs#gnused -- -i -e 's/ nixbld//' /etc/nix/nix.conf
    
    315 337
       script:
    
    316 338
         - nix run .gitlab/generate-ci#generate-jobs
    
    317 339
         # 1 if .gitlab/generate_jobs changed the output of the generated config
    

  • utils/check-submodules/LICENSE
    1
    +Copyright (c) 2024, Ben Gamari
    
    2
    +
    
    3
    +All rights reserved.
    
    4
    +
    
    5
    +Redistribution and use in source and binary forms, with or without
    
    6
    +modification, are permitted provided that the following conditions are met:
    
    7
    +
    
    8
    +    * Redistributions of source code must retain the above copyright
    
    9
    +      notice, this list of conditions and the following disclaimer.
    
    10
    +
    
    11
    +    * Redistributions in binary form must reproduce the above
    
    12
    +      copyright notice, this list of conditions and the following
    
    13
    +      disclaimer in the documentation and/or other materials provided
    
    14
    +      with the distribution.
    
    15
    +
    
    16
    +    * Neither the name of Ben Gamari nor the names of other
    
    17
    +      contributors may be used to endorse or promote products derived
    
    18
    +      from this software without specific prior written permission.
    
    19
    +
    
    20
    +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    
    21
    +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    
    22
    +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    
    23
    +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    
    24
    +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    
    25
    +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    
    26
    +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    
    27
    +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    
    28
    +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    
    29
    +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    
    30
    +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

  • utils/check-submodules/README.mkd
    1
    +# check-submodules
    
    2
    +
    
    3
    +This is a utilities used in GHC CI to verify the consistency and
    
    4
    +up-to-date-ness of GHC's boot library dependencies. Specifically
    
    5
    +we verify that:
    
    6
    +
    
    7
    + * the referenced commits are released on Hackage
    
    8
    + * that the Hackage version has not been deprecated
    
    9
    + * that there is not a more recent version in the same major series
    
    10
    +
    
    11
    +## Usage
    
    12
    +
    
    13
    +In the GHC tree:
    
    14
    +```bash
    
    15
    +nix run ./utils/check-submodules#
    
    16
    +```

  • utils/check-submodules/app/Main.hs
    1
    +module Main (main) where
    
    2
    +
    
    3
    +import CheckVersions
    
    4
    +import CheckTags
    
    5
    +import System.Environment (getArgs)
    
    6
    +
    
    7
    +main :: IO ()
    
    8
    +main = do
    
    9
    +  args <- getArgs
    
    10
    +  case args of
    
    11
    +    ["check-versions"] -> checkVersions
    
    12
    +    ["check-tags"] -> checkTags
    
    13
    +    ["summarize"] -> summarize
    
    14
    +    ["email"] -> maintainerEmails >>= putStrLn
    
    15
    +    _ -> fail "invalid mode (valid modes: check-versions, check-tags, summarize, email)"

  • utils/check-submodules/check-submodules.cabal
    1
    +cabal-version:      3.0
    
    2
    +name:               check-submodules
    
    3
    +version:            0.1.0.0
    
    4
    +-- synopsis:
    
    5
    +-- description:
    
    6
    +homepage:           https://gitlab.haskell.org/ghc/ghc
    
    7
    +license:            BSD-3-Clause
    
    8
    +license-file:       LICENSE
    
    9
    +author:             Ben Gamari
    
    10
    +maintainer:         ben@smart-cactus.org
    
    11
    +copyright:          (c) 2024 Ben Gamari
    
    12
    +category:           Development
    
    13
    +build-type:         Simple
    
    14
    +-- extra-source-files:
    
    15
    +
    
    16
    +common warnings
    
    17
    +    ghc-options: -Wall
    
    18
    +
    
    19
    +executable check-submodules
    
    20
    +    import:           warnings
    
    21
    +    main-is:          Main.hs
    
    22
    +    build-depends:    base,
    
    23
    +                      check-submodules
    
    24
    +    hs-source-dirs:   app
    
    25
    +    default-language: Haskell2010
    
    26
    +
    
    27
    +library
    
    28
    +    import:           warnings
    
    29
    +    exposed-modules:  Git,
    
    30
    +                      Hackage,
    
    31
    +                      CheckVersions,
    
    32
    +                      CheckTags,
    
    33
    +                      Packages,
    
    34
    +                      Package,
    
    35
    +                      Pretty
    
    36
    +    build-depends:    base,
    
    37
    +                      wreq,
    
    38
    +                      aeson,
    
    39
    +                      bytestring,
    
    40
    +                      text,
    
    41
    +                      transformers,
    
    42
    +                      prettyprinter,
    
    43
    +                      prettyprinter-ansi-terminal,
    
    44
    +                      filepath,
    
    45
    +                      microlens,
    
    46
    +                      containers,
    
    47
    +                      typed-process,
    
    48
    +                      Cabal
    
    49
    +    hs-source-dirs:   src
    
    50
    +    default-language: Haskell2010

  • utils/check-submodules/flake.lock
    1
    +{
    
    2
    +  "nodes": {
    
    3
    +    "flake-utils": {
    
    4
    +      "inputs": {
    
    5
    +        "systems": "systems"
    
    6
    +      },
    
    7
    +      "locked": {
    
    8
    +        "lastModified": 1731533236,
    
    9
    +        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
    
    10
    +        "owner": "numtide",
    
    11
    +        "repo": "flake-utils",
    
    12
    +        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
    
    13
    +        "type": "github"
    
    14
    +      },
    
    15
    +      "original": {
    
    16
    +        "owner": "numtide",
    
    17
    +        "repo": "flake-utils",
    
    18
    +        "type": "github"
    
    19
    +      }
    
    20
    +    },
    
    21
    +    "nixpkgs": {
    
    22
    +      "locked": {
    
    23
    +        "lastModified": 1734083684,
    
    24
    +        "narHash": "sha256-5fNndbndxSx5d+C/D0p/VF32xDiJCJzyOqorOYW4JEo=",
    
    25
    +        "path": "/nix/store/0xbni69flk8380w0apw4h640n37wn1i9-source",
    
    26
    +        "rev": "314e12ba369ccdb9b352a4db26ff419f7c49fa84",
    
    27
    +        "type": "path"
    
    28
    +      },
    
    29
    +      "original": {
    
    30
    +        "id": "nixpkgs",
    
    31
    +        "type": "indirect"
    
    32
    +      }
    
    33
    +    },
    
    34
    +    "root": {
    
    35
    +      "inputs": {
    
    36
    +        "flake-utils": "flake-utils",
    
    37
    +        "nixpkgs": "nixpkgs"
    
    38
    +      }
    
    39
    +    },
    
    40
    +    "systems": {
    
    41
    +      "locked": {
    
    42
    +        "lastModified": 1681028828,
    
    43
    +        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
    
    44
    +        "owner": "nix-systems",
    
    45
    +        "repo": "default",
    
    46
    +        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
    
    47
    +        "type": "github"
    
    48
    +      },
    
    49
    +      "original": {
    
    50
    +        "owner": "nix-systems",
    
    51
    +        "repo": "default",
    
    52
    +        "type": "github"
    
    53
    +      }
    
    54
    +    }
    
    55
    +  },
    
    56
    +  "root": "root",
    
    57
    +  "version": 7
    
    58
    +}

  • utils/check-submodules/flake.nix
    1
    +{
    
    2
    +  description = "GHC boot library linting";
    
    3
    +
    
    4
    +  inputs.flake-utils.url = "github:numtide/flake-utils";
    
    5
    +
    
    6
    +  outputs = { self, nixpkgs, flake-utils }:
    
    7
    +    flake-utils.lib.eachDefaultSystem (system:
    
    8
    +      let pkgs = nixpkgs.legacyPackages.${system}; in
    
    9
    +      {
    
    10
    +        packages = rec {
    
    11
    +          check-submodules = pkgs.haskellPackages.callCabal2nix "generate-ci" ./. {};
    
    12
    +          default = check-submodules;
    
    13
    +        };
    
    14
    +
    
    15
    +        devShells.default = self.packages.${system}.default.env;
    
    16
    +
    
    17
    +        apps = rec {
    
    18
    +          check-submodules = flake-utils.lib.mkApp {
    
    19
    +            drv = self.packages.${system}.check-submodules;
    
    20
    +          };
    
    21
    +          default = check-submodules;
    
    22
    +        };
    
    23
    +      }
    
    24
    +    );
    
    25
    +}
    
    26
    +

  • utils/check-submodules/hie.yaml
    1
    +cradle:
    
    2
    +  cabal:

  • utils/check-submodules/src/CheckTags.hs
    1
    +{-# LANGUAGE ImportQualifiedPost #-}
    
    2
    +{-# LANGUAGE OverloadedStrings #-}
    
    3
    +{-# LANGUAGE TupleSections #-}
    
    4
    +
    
    5
    +module CheckTags
    
    6
    +    ( checkTags
    
    7
    +    ) where
    
    8
    +
    
    9
    +import Data.List (isPrefixOf, isSuffixOf)
    
    10
    +import Git qualified
    
    11
    +import Package (Package(..))
    
    12
    +import Packages (packages)
    
    13
    +import Pretty
    
    14
    +import Control.Monad (unless)
    
    15
    +
    
    16
    +findReleaseTag :: Git.GitRepo -> Package -> IO (Maybe Git.Tag)
    
    17
    +findReleaseTag repo pkg = do
    
    18
    +    allTags <- Git.reachableTags repo "HEAD"
    
    19
    +    case filter (\tag -> pkgIsReleaseTag pkg tag || isGhcTag tag) allTags of
    
    20
    +      [] -> return Nothing
    
    21
    +      tag:_ -> return (Just tag)
    
    22
    +
    
    23
    +isGhcTag :: Git.Tag -> Bool
    
    24
    +isGhcTag tag = "-ghc" `isSuffixOf` tag
    
    25
    +
    
    26
    +checkTag :: Git.GitRepo -> Package -> IO (Maybe Doc)
    
    27
    +checkTag repo pkg = do
    
    28
    +    mb_tag <- findReleaseTag repo pkg
    
    29
    +    case mb_tag of
    
    30
    +      Nothing -> return $ Just "No release tags found"
    
    31
    +      Just tag -> checkChanges repo tag
    
    32
    +
    
    33
    +-- | Check whether the tag only deviates from HEAD in trivial ways.
    
    34
    +checkChanges :: Git.GitRepo -> Git.Ref -> IO (Maybe Doc)
    
    35
    +checkChanges repo tag = do
    
    36
    +    files <- Git.changedFiles repo tag "HEAD"
    
    37
    +    case filter (not . okayChange) files of
    
    38
    +      [] -> return Nothing
    
    39
    +      badFiles  -> do
    
    40
    +          described <- Git.describeRef repo "HEAD"
    
    41
    +          let msg = vsep
    
    42
    +                [ "Tag" <+> ppCommit (pretty tag) <+> "differs from" <+> ppCommit (pretty described) <+> "in:"
    
    43
    +                , bulletList fileList
    
    44
    +                ]
    
    45
    +              maxFiles = 5
    
    46
    +              fileList
    
    47
    +                | n > 0 =
    
    48
    +                    take maxFiles (map pretty badFiles) ++
    
    49
    +                    ["... and" <+> pretty n <+> "other" <+> plural "file" "files" n]
    
    50
    +                | otherwise = map pretty badFiles
    
    51
    +                where n = length badFiles - maxFiles
    
    52
    +          return $ Just msg
    
    53
    +
    
    54
    +okayChange :: FilePath -> Bool
    
    55
    +okayChange path
    
    56
    +  | "." `isPrefixOf` path = True
    
    57
    +  | ".gitignore" `isSuffixOf` path = True
    
    58
    +  | otherwise = False
    
    59
    +
    
    60
    +checkTags :: IO ()
    
    61
    +checkTags = do
    
    62
    +    let ghcRepo = Git.GitRepo "."
    
    63
    +    errs <- mapM (\pkg -> (pkg,) <$> checkTag (Git.submoduleIn ghcRepo (pkgPath pkg)) pkg) packages
    
    64
    +    putDoc $ bulletList
    
    65
    +      [ severityIcon Error <+> ppPackage pkg <> ":" <+> err
    
    66
    +      | (pkg, Just err) <- errs
    
    67
    +      ]
    
    68
    +    unless (null errs) $ fail "Tag issues above"

  • utils/check-submodules/src/CheckVersions.hs
    1
    +{-# LANGUAGE ImportQualifiedPost #-}
    
    2
    +{-# LANGUAGE OverloadedStrings #-}
    
    3
    +{-# LANGUAGE TupleSections #-}
    
    4
    +
    
    5
    +module CheckVersions
    
    6
    +    ( checkVersions
    
    7
    +    , summarize
    
    8
    +    , maintainerEmails
    
    9
    +    ) where
    
    10
    +
    
    11
    +import Control.Monad (forM_)
    
    12
    +import Control.Monad.IO.Class
    
    13
    +import Control.Monad.Trans.Writer
    
    14
    +import Data.Function (on)
    
    15
    +import Data.List (intercalate, sort, nubBy)
    
    16
    +import Data.Map.Strict qualified as M
    
    17
    +import Data.Text qualified as T
    
    18
    +import Data.Version
    
    19
    +import Distribution.Types.PackageName qualified as C
    
    20
    +import System.Exit
    
    21
    +
    
    22
    +import Hackage (getVersions, PackageState (..))
    
    23
    +import Pretty
    
    24
    +import Package
    
    25
    +import Packages
    
    26
    +
    
    27
    +isPvpCompatible :: Version -> Version -> Bool
    
    28
    +isPvpCompatible a b =
    
    29
    +    take 2 (versionBranch a) == take 2 (versionBranch b)
    
    30
    +
    
    31
    +updateVersion :: M.Map Version PackageState -> Version -> Maybe Version
    
    32
    +updateVersion available v
    
    33
    +  | [] <- compatible = Nothing
    
    34
    +  | otherwise        = Just $ maximum compatible
    
    35
    +  where
    
    36
    +    compatible =
    
    37
    +      [ v'
    
    38
    +      | (v', Normal) <- M.assocs available -- non-deprecated versions available via Hackage...
    
    39
    +      , v' > v                             -- that are newer than the submodule...
    
    40
    +      , v' `isPvpCompatible` v             -- and are compatible with the submodule
    
    41
    +      ]
    
    42
    +
    
    43
    +checkPackage :: Package -> WriterT [(Severity, Doc)] IO ()
    
    44
    +checkPackage pkg = do
    
    45
    +    v <- liftIO $ getPackageVersion pkg
    
    46
    +    available <- liftIO $ getVersions (pkgName pkg)
    
    47
    +
    
    48
    +    case M.lookup v available of
    
    49
    +        Nothing         -> tellMsg Error $ "Version" <+> ppVersion v <+> "is not on Hackage"
    
    50
    +        Just Deprecated -> tellMsg Error $ "Version" <+> ppVersion v <+> "has been deprecated"
    
    51
    +        Just Normal     -> return ()
    
    52
    +
    
    53
    +    case updateVersion available v of
    
    54
    +        Nothing -> return ()
    
    55
    +        Just v' -> tellMsg Warning $ "Shipping with" <+> ppVersion v <+> "but newer version" <+> ppVersion v' <+> "is available"
    
    56
    +
    
    57
    +tellMsg :: Severity -> Doc -> WriterT [(Severity, Doc)] IO ()
    
    58
    +tellMsg sev msg = tell [(sev, msg)]
    
    59
    +
    
    60
    +summarizeSubmodules :: [Package] -> IO ()
    
    61
    +summarizeSubmodules pkgs = forM_ pkgs $ \pkg -> do
    
    62
    +    v <- getPackageVersion pkg
    
    63
    +    putStrLn $ "    " <> C.unPackageName (pkgName pkg) <> " " <> showVersion v <> " @ " <> pkgPath pkg
    
    64
    +
    
    65
    +maintainerEmails :: IO String
    
    66
    +maintainerEmails = do
    
    67
    +    maintainers <- concat <$> mapM getPackageMaintainers packages
    
    68
    +    return $ intercalate ", " $ map (T.unpack . contactRecipient) $ nubBy ((==) `on` contactEmail) $ sort maintainers
    
    69
    +
    
    70
    +summarize :: IO ()
    
    71
    +summarize =
    
    72
    +    summarizeSubmodules packages
    
    73
    +
    
    74
    +checkVersions :: IO ()
    
    75
    +checkVersions = do
    
    76
    +    errs <- mapM (\pkg -> map (pkg, ) <$> execWriterT (checkPackage pkg)) packages
    
    77
    +    putDoc $ bulletList
    
    78
    +      [ severityIcon sev <+> ppPackage pkg <> ":" <+> err
    
    79
    +      | (pkg, (sev, err)) <- concat errs
    
    80
    +      ]
    
    81
    +    exitWith $ if null errs then ExitSuccess else ExitFailure 1
    
    82
    +

  • utils/check-submodules/src/Git.hs
    1
    +{-# LANGUAGE ImportQualifiedPost #-}
    
    2
    +
    
    3
    +module Git
    
    4
    +    ( GitRepo(..)
    
    5
    +    , submoduleIn
    
    6
    +
    
    7
    +    , Ref
    
    8
    +    , describeRef
    
    9
    +    , submoduleCommit
    
    10
    +    , Tag
    
    11
    +    , reachableTags
    
    12
    +    , changedFiles
    
    13
    +    ) where
    
    14
    +
    
    15
    +import System.Process.Typed
    
    16
    +import Data.ByteString.Lazy.Char8 qualified as BSL
    
    17
    +import System.FilePath ((</>))
    
    18
    +
    
    19
    +newtype GitRepo = GitRepo { gitRepoPath :: FilePath }
    
    20
    +
    
    21
    +submoduleIn :: GitRepo -> FilePath -> GitRepo
    
    22
    +submoduleIn (GitRepo path) submod =
    
    23
    +    GitRepo $ path </> submod
    
    24
    +
    
    25
    +type Ref = String
    
    26
    +type Tag = String
    
    27
    +
    
    28
    +runGit :: GitRepo -> [String] -> IO BSL.ByteString
    
    29
    +runGit (GitRepo path) args = do
    
    30
    +    readProcessStdout_ $ setWorkingDir path (proc "git" args)
    
    31
    +
    
    32
    +describeRef :: GitRepo -> Ref -> IO String
    
    33
    +describeRef repo ref =
    
    34
    +    head . lines . BSL.unpack <$> runGit repo ["describe", "--always", ref]
    
    35
    +
    
    36
    +-- | Get the commit of the given submodule.
    
    37
    +submoduleCommit :: GitRepo -> FilePath -> IO Ref
    
    38
    +submoduleCommit repo submodule = do
    
    39
    +    out <- runGit repo ["submodule", "status", submodule]
    
    40
    +    case BSL.words $ BSL.drop 1 out of
    
    41
    +      commit:_ -> return $ BSL.unpack commit
    
    42
    +      _ -> fail "Unrecognized output from `git submodule status`"
    
    43
    +
    
    44
    +-- | Get the most recent tags reacheable from the given commit.
    
    45
    +reachableTags :: GitRepo -> Ref -> IO [Tag]
    
    46
    +reachableTags repo ref =
    
    47
    +    reverse . map BSL.unpack . BSL.lines <$> runGit repo ["tag", "--sort=taggerdate", "--merged", ref]
    
    48
    +
    
    49
    +changedFiles :: GitRepo -> Ref -> Ref -> IO [FilePath]
    
    50
    +changedFiles repo a b = do
    
    51
    +    map BSL.unpack . BSL.lines <$> runGit repo ["diff", "--name-only", a, b]
    
    52
    +

  • utils/check-submodules/src/Hackage.hs
    1
    +{-# LANGUAGE LambdaCase #-}
    
    2
    +{-# LANGUAGE OverloadedStrings #-}
    
    3
    +
    
    4
    +module Hackage
    
    5
    +    ( PackageState(..)
    
    6
    +    , getVersions
    
    7
    +    ) where
    
    8
    +
    
    9
    +import qualified Data.Map.Strict as M
    
    10
    +import Lens.Micro
    
    11
    +import Network.Wreq
    
    12
    +import Distribution.Types.PackageName
    
    13
    +import qualified Data.Aeson as JSON
    
    14
    +import Data.Version
    
    15
    +
    
    16
    +data PackageState = Normal | Deprecated
    
    17
    +    deriving (Show)
    
    18
    +
    
    19
    +instance JSON.FromJSON PackageState where
    
    20
    +    parseJSON = JSON.withText "package state" $ \case
    
    21
    +        "normal" -> pure Normal
    
    22
    +        "deprecated" -> pure Deprecated
    
    23
    +        _ -> fail "unknown PackageState"
    
    24
    +
    
    25
    +getVersions :: PackageName -> IO (M.Map Version PackageState)
    
    26
    +getVersions pn = do
    
    27
    +    r <- asJSON =<< getWith opts url
    
    28
    +    maybe (fail "getVersions: failed") pure (r ^? responseBody)
    
    29
    +  where
    
    30
    +    opts = defaults & header "Accept" .~ ["application/json"]
    
    31
    +    url = "https://hackage.haskell.org/package/" <> unPackageName pn
    
    32
    +

  • utils/check-submodules/src/Package.hs
    1
    +{-# LANGUAGE ImportQualifiedPost #-}
    
    2
    +{-# LANGUAGE OverloadedStrings #-}
    
    3
    +
    
    4
    +module Package
    
    5
    +  ( Contact(..)
    
    6
    +  , parseContact
    
    7
    +  , contactRecipient
    
    8
    +
    
    9
    +  , Package(..)
    
    10
    +  , getPackageVersion
    
    11
    +  , getPackageMaintainers
    
    12
    +  ) where
    
    13
    +
    
    14
    +import Data.ByteString qualified as BS
    
    15
    +import Data.Text qualified as T
    
    16
    +import Data.Version
    
    17
    +import Distribution.PackageDescription.Parsec qualified as C
    
    18
    +import Distribution.Types.GenericPackageDescription qualified as C
    
    19
    +import Distribution.Types.PackageDescription qualified as C
    
    20
    +import Distribution.Types.PackageId qualified as C
    
    21
    +import Distribution.Types.PackageName (PackageName)
    
    22
    +import Distribution.Types.PackageName qualified as C
    
    23
    +import Distribution.Types.Version qualified as C
    
    24
    +import Distribution.Utils.ShortText qualified as C
    
    25
    +import System.FilePath
    
    26
    +
    
    27
    +data Contact = Contact { contactName, contactEmail :: T.Text }
    
    28
    +    deriving (Eq, Ord, Show)
    
    29
    +
    
    30
    +parseContact :: T.Text -> Contact
    
    31
    +parseContact t
    
    32
    +  | '<' `T.elem` t =
    
    33
    +    let (name,email) = T.break (== '<') t
    
    34
    +     in Contact (T.strip name) (T.strip $ T.takeWhile (/= '>') $ T.drop 1 email)
    
    35
    +  | otherwise = Contact "" t
    
    36
    +
    
    37
    +contactRecipient :: Contact -> T.Text
    
    38
    +contactRecipient (Contact name email)
    
    39
    +  | T.null name = email
    
    40
    +  | otherwise = name <> " <" <> email <> ">"
    
    41
    +
    
    42
    +data Package = Package { pkgName :: PackageName
    
    43
    +                       , pkgPath :: FilePath
    
    44
    +                       , pkgIsReleaseTag :: String -> Bool
    
    45
    +                       }
    
    46
    +
    
    47
    +getPackageDescription :: Package -> IO C.PackageDescription
    
    48
    +getPackageDescription pkg = do
    
    49
    +    Just gpd <- C.parseGenericPackageDescriptionMaybe <$> BS.readFile (pkgPath pkg </> C.unPackageName (pkgName pkg) <.> "cabal")
    
    50
    +    return $ C.packageDescription gpd
    
    51
    +
    
    52
    +getPackageMaintainers :: Package -> IO [Contact]
    
    53
    +getPackageMaintainers pkg =
    
    54
    +    map (parseContact . T.strip . T.filter (/= '\n')) . T.splitOn ","
    
    55
    +    . T.pack . C.fromShortText . C.maintainer
    
    56
    +    <$> getPackageDescription pkg
    
    57
    +
    
    58
    +getPackageVersion :: Package -> IO Version
    
    59
    +getPackageVersion pkg =
    
    60
    +    Data.Version.makeVersion . C.versionNumbers . C.pkgVersion . C.package
    
    61
    +    <$> getPackageDescription pkg
    
    62
    +

  • utils/check-submodules/src/Packages.hs
    1
    +{-# LANGUAGE OverloadedStrings #-}
    
    2
    +
    
    3
    +module Packages (packages) where
    
    4
    +
    
    5
    +import Package
    
    6
    +import Data.Char (isDigit)
    
    7
    +import qualified Distribution.Types.PackageName as C
    
    8
    +import Data.List
    
    9
    +
    
    10
    +packages :: [Package]
    
    11
    +packages =
    
    12
    +    [ stdPackage "file-io" "libraries/file-io"
    
    13
    +    , stdPackage "hsc2hs" "utils/hsc2hs"
    
    14
    +    , Package "Cabal" "libraries/Cabal/Cabal"  (isPrefixTag "Cabal-")
    
    15
    +    , Package "Cabal-syntax" "libraries/Cabal/Cabal-syntax" (isPrefixTag "Cabal-syntax-")
    
    16
    +    , stdPackage "bytestring" "libraries/bytestring"
    
    17
    +    , stdPackage "binary" "libraries/binary"
    
    18
    +    , stdPackage "array" "libraries/array"
    
    19
    +    , stdPackage "containers" "libraries/containers/containers"
    
    20
    +    , stdPackage "deepseq" "libraries/deepseq"
    
    21
    +    , stdPackage "directory" "libraries/directory"
    
    22
    +    , stdPackage "filepath" "libraries/filepath"
    
    23
    +    , stdPackage "haskeline" "libraries/haskeline"
    
    24
    +    , stdPackage "hpc" "libraries/hpc"
    
    25
    +    , stdPackage "mtl" "libraries/mtl"
    
    26
    +    , stdPackage "parsec" "libraries/parsec"
    
    27
    +    , stdPackage "pretty" "libraries/pretty"
    
    28
    +    , stdPackage "process" "libraries/process"
    
    29
    +    , stdPackage "terminfo" "libraries/terminfo"
    
    30
    +    , stdPackage "text" "libraries/text"
    
    31
    +    , stdPackage "time" "libraries/time"
    
    32
    +    , stdPackage "unix" "libraries/unix"
    
    33
    +    , stdPackage "exceptions" "libraries/exceptions"
    
    34
    +    , stdPackage "semaphore-compat" "libraries/semaphore-compat"
    
    35
    +    , stdPackage "stm" "libraries/stm"
    
    36
    +    , stdPackage "Win32" "libraries/Win32"
    
    37
    +    , stdPackage "xhtml" "libraries/xhtml"
    
    38
    +    ]
    
    39
    +
    
    40
    +stdPackage :: C.PackageName -> FilePath -> Package
    
    41
    +stdPackage name path = Package name path stdIsReleaseTag
    
    42
    +
    
    43
    +looksLikeVersion :: String -> Bool
    
    44
    +looksLikeVersion =
    
    45
    +    all (\c -> isDigit c || c == '.')
    
    46
    +
    
    47
    +isPrefixTag :: String -> String -> Bool
    
    48
    +isPrefixTag prefix tag
    
    49
    +  | Just rest <- prefix `stripPrefix` tag = looksLikeVersion rest
    
    50
    +  | otherwise = False
    
    51
    +
    
    52
    +stdIsReleaseTag :: String -> Bool
    
    53
    +stdIsReleaseTag tag =
    
    54
    +    isPrefixTag "v" tag || isPrefixTag "" tag

  • utils/check-submodules/src/Pretty.hs
    1
    +{-# LANGUAGE ImportQualifiedPost #-}
    
    2
    +{-# LANGUAGE OverloadedStrings #-}
    
    3
    +
    
    4
    +module Pretty
    
    5
    +    ( module Prettyprinter
    
    6
    +    , Doc
    
    7
    +    , mkMsg
    
    8
    +    , Severity(..)
    
    9
    +    , severityIcon
    
    10
    +    , bulletList
    
    11
    +    , ppCommit
    
    12
    +    , ppPackage
    
    13
    +    , ppVersion
    
    14
    +    , ppHeading
    
    15
    +    , putDoc
    
    16
    +    ) where
    
    17
    +
    
    18
    +import Data.Version
    
    19
    +import Package
    
    20
    +import Prettyprinter hiding (Doc)
    
    21
    +import Prettyprinter qualified as PP
    
    22
    +import Prettyprinter.Render.Terminal
    
    23
    +import Distribution.Types.PackageName qualified as C
    
    24
    +
    
    25
    +type Doc = PP.Doc AnsiStyle
    
    26
    +
    
    27
    +ppPackage :: Package -> Doc
    
    28
    +ppPackage =
    
    29
    +    annotate (color Green) . pretty . C.unPackageName . pkgName
    
    30
    +
    
    31
    +ppVersion :: Version -> Doc
    
    32
    +ppVersion v =
    
    33
    +    annotate (color Blue) $ pretty $ showVersion v
    
    34
    +
    
    35
    +ppCommit :: Doc -> Doc
    
    36
    +ppCommit =
    
    37
    +    annotate (color Blue)
    
    38
    +
    
    39
    +ppHeading :: Doc -> Doc
    
    40
    +ppHeading =
    
    41
    +    annotate bold . ("#" <+>)
    
    42
    +
    
    43
    +bullet :: Doc
    
    44
    +bullet = "โ€ฃ"
    
    45
    +
    
    46
    +bulletList :: [Doc] -> Doc
    
    47
    +bulletList xs = vcat [ " " <> bullet <+> align x | x <- xs ]
    
    48
    +
    
    49
    +data Severity = Info | Warning | Error
    
    50
    +
    
    51
    +severityIcon :: Severity -> Doc
    
    52
    +severityIcon Info    = annotate (color Blue) "โ„น" -- "๐Ÿ”ต"
    
    53
    +severityIcon Warning = "๐ŸŸก"
    
    54
    +severityIcon Error   = annotate (color Red) "โœ—" -- "๐Ÿ”ด"
    
    55
    +
    
    56
    +mkMsg :: Severity -> Doc -> Doc
    
    57
    +mkMsg s msg = severityIcon s <+> msg