Ben Gamari pushed to branch wip/boot-lib-testing at Glasgow Haskell Compiler / GHC
Commits:
-
c0a4c9c9
by Ben Gamari at 2025-06-12T14:04:00-04:00
-
795105ee
by Ben Gamari at 2025-06-12T14:04:00-04:00
15 changed files:
- .gitlab-ci.yml
- + utils/check-submodules/LICENSE
- + utils/check-submodules/README.mkd
- + utils/check-submodules/app/Main.hs
- + utils/check-submodules/check-submodules.cabal
- + utils/check-submodules/flake.lock
- + utils/check-submodules/flake.nix
- + utils/check-submodules/hie.yaml
- + utils/check-submodules/src/CheckTags.hs
- + utils/check-submodules/src/CheckVersions.hs
- + utils/check-submodules/src/Git.hs
- + utils/check-submodules/src/Hackage.hs
- + utils/check-submodules/src/Package.hs
- + utils/check-submodules/src/Packages.hs
- + utils/check-submodules/src/Pretty.hs
Changes:
... | ... | @@ -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
|
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. |
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 | +``` |
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)" |
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 |
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 | +} |
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 | + |
1 | +cradle:
|
|
2 | + cabal: |
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" |
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 | + |
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 | + |
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 | + |
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 | + |
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 |
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 |