Zubin pushed to branch wip/27183 at Glasgow Haskell Compiler / GHC
Commits:
-
553822f6
by Zubin Duggal at 2026-04-28T17:10:53+05:30
12 changed files:
- .gitlab-ci.yml
- .gitlab/merge_request_templates/Default.md
- changelog.d/config
- docs/users_guide/ghc_config.py.in
- hadrian/src/Rules/Changelog.hs
- libraries/integer-gmp/integer-gmp.cabal
- testsuite/tests/linters/Makefile
- utils/changelog-d/ChangelogD.hs
- utils/changelog-d/README.md
- + utils/changelog-d/tests/config
- + utils/changelog-d/tests/expected/test-parser-rewriter.md
- + utils/changelog-d/tests/test-parser-rewriter
Changes:
| ... | ... | @@ -247,6 +247,9 @@ ghc-linters: |
| 247 | 247 | |
| 248 | 248 | # Check that MRs include a changelog entry in changelog.d/.
|
| 249 | 249 | # Skipped if the MR has the ~"no-changelog" label.
|
| 250 | +#
|
|
| 251 | +# If MR's diff touches libraries/base/, the changelog must also have a non-empty
|
|
| 252 | +# `clc:` field.
|
|
| 250 | 253 | lint-changelog:
|
| 251 | 254 | stage: tool-lint
|
| 252 | 255 | image: "registry.gitlab.haskell.org/ghc/ci-images/x86_64-linux-deb13:$DOCKER_REV"
|
| ... | ... | @@ -254,6 +257,7 @@ lint-changelog: |
| 254 | 257 | variables:
|
| 255 | 258 | BUILD_FLAVOUR: default
|
| 256 | 259 | CHANGELOG_EXPECT_MR: "$CI_MERGE_REQUEST_IID"
|
| 260 | + CHANGELOG_EXPECT_CLC: ""
|
|
| 257 | 261 | script:
|
| 258 | 262 | # Check that the MR adds at least one changelog entry
|
| 259 | 263 | - git fetch "$CI_MERGE_REQUEST_PROJECT_URL" "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
|
| ... | ... | @@ -276,6 +280,10 @@ lint-changelog: |
| 276 | 280 | when: never
|
| 277 | 281 | - if: '$CI_MERGE_REQUEST_LABELS =~ /.*no-changelog.*/'
|
| 278 | 282 | when: never
|
| 283 | + - changes:
|
|
| 284 | + - libraries/base/**/*
|
|
| 285 | + variables:
|
|
| 286 | + CHANGELOG_EXPECT_CLC: "1"
|
|
| 279 | 287 | - if: $CI_MERGE_REQUEST_ID
|
| 280 | 288 | - *drafts-can-fail-lint
|
| 281 | 289 |
| ... | ... | @@ -23,7 +23,8 @@ https://gitlab.haskell.org/ghc/ghc/-/wikis/Contributing-a-Patch |
| 23 | 23 | - [ ] This MR solves the problem described in the following issue: <!-- issue number here (please open a new issue if there isn't one) -->
|
| 24 | 24 | - [ ] A changelog entry was added in `changelog.d/` for user-facing changes (see [changelog guide][changelog]).
|
| 25 | 25 | If this MR does not need a changelog entry, the ~"no-changelog" label was applied.
|
| 26 | -- [ ] This MR does not make any significant changes to `base`, or it has an accompanying [CLC proposal](https://github.com/haskell/core-libraries-committee#base-package).
|
|
| 26 | +- [ ] This MR does not make any significant changes to `base`, or it has an accompanying [CLC proposal](https://github.com/haskell/core-libraries-committee#base-package)
|
|
| 27 | + and the changelog fragment uses `section: base` with the `clc: #<proposal>` field set.
|
|
| 27 | 28 | - [ ] If this MR has the potential to break user programs, the ~"user-facing" label was applied to
|
| 28 | 29 | test against head.hackage.
|
| 29 | 30 | - [ ] All commits are either individually buildable or squashed.
|
| ... | ... | @@ -27,6 +27,7 @@ sections: { |
| 27 | 27 | cmm Cmm
|
| 28 | 28 | build-tools Build tools
|
| 29 | 29 | base ``base`` library
|
| 30 | + ghc-internal ``ghc-internal`` library
|
|
| 30 | 31 | ghc-prim ``ghc-prim`` library
|
| 31 | 32 | ghc-lib ``ghc`` library
|
| 32 | 33 | ghc-heap ``ghc-heap`` library
|
| ... | ... | @@ -36,6 +37,18 @@ sections: { |
| 36 | 37 | ghc-toolchain ``ghc-toolchain``
|
| 37 | 38 | }
|
| 38 | 39 | |
| 40 | +-- markdown-targets: sections that also need to end up in
|
|
| 41 | +-- per-library changelog files. The optional third token
|
|
| 42 | +-- lists extra fields that might be required for this section
|
|
| 43 | +-- like `clc` for base.
|
|
| 44 | +markdown-targets: {
|
|
| 45 | + base libraries/base/changelog.md clc
|
|
| 46 | + ghc-internal libraries/ghc-internal/CHANGELOG.md
|
|
| 47 | + ghc-prim libraries/ghc-prim/changelog.md
|
|
| 48 | + ghc-experimental libraries/ghc-experimental/CHANGELOG.md
|
|
| 49 | + template-haskell libraries/template-haskell/changelog.md
|
|
| 50 | +}
|
|
| 51 | + |
|
| 39 | 52 | included-libraries-preamble: {
|
| 40 | 53 | The package database provided with this distribution also contains a number of
|
| 41 | 54 | packages other than GHC itself. See the changelogs provided with these packages
|
| ... | ... | @@ -7,12 +7,14 @@ if parse_version(sphinx.__version__) >= parse_version("4.0.0"): |
| 7 | 7 | 'ghc-ticket': ('https://gitlab.haskell.org/ghc/ghc/issues/%s', '#%s'),
|
| 8 | 8 | 'ghc-wiki': ('https://gitlab.haskell.org/ghc/ghc/wikis/%s', '%s'),
|
| 9 | 9 | 'ghc-mr': ('https://gitlab.haskell.org/ghc/ghc/-/merge_requests/%s', '!%s'),
|
| 10 | + 'clc': ('https://github.com/haskell/core-libraries-committee/issues/%s', 'CLC proposal #%s'),
|
|
| 10 | 11 | }
|
| 11 | 12 | else:
|
| 12 | 13 | extlinks = {
|
| 13 | 14 | 'ghc-ticket': ('https://gitlab.haskell.org/ghc/ghc/issues/%s', '#'),
|
| 14 | 15 | 'ghc-wiki': ('https://gitlab.haskell.org/ghc/ghc/wikis/%s', ''),
|
| 15 | 16 | 'ghc-mr': ('https://gitlab.haskell.org/ghc/ghc/-/merge_requests/%s', '!'),
|
| 17 | + 'clc': ('https://github.com/haskell/core-libraries-committee/issues/%s', 'CLC proposal #'),
|
|
| 16 | 18 | }
|
| 17 | 19 | |
| 18 | 20 | libs_base_uri = '../libraries'
|
| ... | ... | @@ -11,8 +11,9 @@ import qualified System.Directory as IO |
| 11 | 11 | -- | Rules for generating and managing changelog entries.
|
| 12 | 12 | --
|
| 13 | 13 | -- Targets:
|
| 14 | --- hadrian/build changelog -- generate release notes
|
|
| 14 | +-- hadrian/build changelog -- generate RST release notes
|
|
| 15 | 15 | -- hadrian/build changelog --changelog-version=10.2.1 -- with explicit version
|
| 16 | +-- hadrian/build libraries-changelog-markdown -- emit per-library Markdown bullets to stdout
|
|
| 16 | 17 | -- hadrian/build changelog-clear -- remove old entries
|
| 17 | 18 | changelogRules :: Rules ()
|
| 18 | 19 | changelogRules = do
|
| ... | ... | @@ -25,19 +26,6 @@ changelogRules = do |
| 25 | 26 | ctx <- programContext stage0Boot changelogD
|
| 26 | 27 | progPath <- programPath ctx
|
| 27 | 28 | need [progPath]
|
| 28 | - |
|
| 29 | - -- These cabal files are needed by changelog-d to determine the
|
|
| 30 | - -- versions of packages shipped with GHC.
|
|
| 31 | - let templatedCabalFiles = map pkgCabalFile
|
|
| 32 | - [ ghcBoot
|
|
| 33 | - , ghcBootTh
|
|
| 34 | - , ghcExperimental
|
|
| 35 | - , ghcInternal
|
|
| 36 | - , ghci
|
|
| 37 | - , compiler
|
|
| 38 | - , ghcHeap
|
|
| 39 | - , templateHaskell
|
|
| 40 | - ]
|
|
| 41 | 29 | need templatedCabalFiles
|
| 42 | 30 | |
| 43 | 31 | top <- topDirectory
|
| ... | ... | @@ -47,6 +35,18 @@ changelogRules = do |
| 47 | 35 | :: Action ()
|
| 48 | 36 | putSuccess $ "| Generated release notes: " ++ outFile
|
| 49 | 37 | |
| 38 | + phony "libraries-changelog-markdown" $ do
|
|
| 39 | + ctx <- programContext stage0Boot changelogD
|
|
| 40 | + progPath <- programPath ctx
|
|
| 41 | + need [progPath]
|
|
| 42 | + need templatedCabalFiles
|
|
| 43 | + |
|
| 44 | + top <- topDirectory
|
|
| 45 | + cmd_ [progPath]
|
|
| 46 | + [ top -/- "changelog.d/"
|
|
| 47 | + , "--libraries-changelog-markdown"
|
|
| 48 | + ]
|
|
| 49 | + |
|
| 50 | 50 | phony "changelog-clear" $ do
|
| 51 | 51 | top <- topDirectory
|
| 52 | 52 | let dir = top -/- "changelog.d"
|
| ... | ... | @@ -54,3 +54,17 @@ changelogRules = do |
| 54 | 54 | let toRemove = filter (\f -> f /= "config" && not (isPrefixOf "." f)) entries
|
| 55 | 55 | liftIO $ mapM_ (IO.removeFile . (dir -/-)) toRemove
|
| 56 | 56 | putSuccess $ "| Removed " ++ show (length toRemove) ++ " changelog entries"
|
| 57 | + where
|
|
| 58 | + -- These cabal files are needed by changelog-d to determine the
|
|
| 59 | + -- versions of packages shipped with GHC.
|
|
| 60 | + templatedCabalFiles = map pkgCabalFile
|
|
| 61 | + [ ghcBoot
|
|
| 62 | + , ghcBootTh
|
|
| 63 | + , ghcExperimental
|
|
| 64 | + , ghcInternal
|
|
| 65 | + , ghci
|
|
| 66 | + , compiler
|
|
| 67 | + , ghcHeap
|
|
| 68 | + , templateHaskell
|
|
| 69 | + , base
|
|
| 70 | + ] |
| ... | ... | @@ -13,6 +13,9 @@ build-type: Simple |
| 13 | 13 | homepage: https://www.haskell.org/ghc/
|
| 14 | 14 | bug-reports: https://gitlab.haskell.org/ghc/ghc/issues/new
|
| 15 | 15 | |
| 16 | +extra-source-files:
|
|
| 17 | + changelog.md
|
|
| 18 | + |
|
| 16 | 19 | description:
|
| 17 | 20 | This package used to provide an implementation of the standard 'Integer'
|
| 18 | 21 | type based on the
|
| ... | ... | @@ -30,8 +30,12 @@ notes: |
| 30 | 30 | (cd $(TOP)/.. && $(LINT_NOTES) broken-refs)
|
| 31 | 31 | |
| 32 | 32 | changelog-d:
|
| 33 | -ifdef CHANGELOG_EXPECT_MR
|
|
| 33 | +ifneq "$(CHANGELOG_EXPECT_MR)" ""
|
|
| 34 | +ifneq "$(CHANGELOG_EXPECT_CLC)" ""
|
|
| 35 | + (cd $(TOP)/.. && $(CHANGELOG_D) changelog.d/ --validate --expect-mr $(CHANGELOG_EXPECT_MR) --expect-clc)
|
|
| 36 | +else
|
|
| 34 | 37 | (cd $(TOP)/.. && $(CHANGELOG_D) changelog.d/ --validate --expect-mr $(CHANGELOG_EXPECT_MR))
|
| 38 | +endif
|
|
| 35 | 39 | else
|
| 36 | 40 | (cd $(TOP)/.. && $(CHANGELOG_D) changelog.d/ --validate)
|
| 37 | 41 | endif
|
| ... | ... | @@ -10,15 +10,15 @@ |
| 10 | 10 | module Main (main) where
|
| 11 | 11 | |
| 12 | 12 | import Control.Exception (Exception (..))
|
| 13 | -import Control.Monad (unless, void, when)
|
|
| 14 | -import Data.Char (isSpace)
|
|
| 13 | +import Control.Monad (filterM, unless, void, when)
|
|
| 14 | +import Data.Char (isAlpha, isSpace)
|
|
| 15 | 15 | import Data.Foldable (for_, toList, traverse_)
|
| 16 | 16 | import Data.Function (on)
|
| 17 | -import Data.List (intercalate, sort, sortBy)
|
|
| 17 | +import Data.List (find, intercalate, isPrefixOf, isSuffixOf, sort, sortBy, stripPrefix)
|
|
| 18 | 18 | import Data.Maybe (isJust, isNothing, mapMaybe)
|
| 19 | 19 | import Data.Set (Set)
|
| 20 | 20 | import Data.Traversable (for)
|
| 21 | -import System.Directory (listDirectory)
|
|
| 21 | +import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
|
|
| 22 | 22 | import System.Environment (getArgs)
|
| 23 | 23 | import System.Exit (exitFailure)
|
| 24 | 24 | import System.FilePath ((</>), dropTrailingPathSeparator, takeDirectory)
|
| ... | ... | @@ -58,16 +58,35 @@ usage = unlines |
| 58 | 58 | , " Collect changelog entries and produce release notes."
|
| 59 | 59 | , ""
|
| 60 | 60 | , "Options:"
|
| 61 | - , " --version <version> Version number for RST file header (e.g. 10.2.1)"
|
|
| 62 | - , " --validate Validate entries only, no output"
|
|
| 63 | - , " --expect-mr <N> Check that at least one entry references MR !N"
|
|
| 64 | - , " --help Show this help"
|
|
| 61 | + , " --version <version> Version number for RST file header (e.g. 10.2.1)"
|
|
| 62 | + , " --validate Validate entries only, no output"
|
|
| 63 | + , " --expect-mr <N> Check that at least one entry references MR !N"
|
|
| 64 | + , " --expect-clc Require the entry matched by --expect-mr"
|
|
| 65 | + , " to have a non-empty 'clc:' field. Used by"
|
|
| 66 | + , " CI for MRs touching base."
|
|
| 67 | + , " --libraries-changelog-markdown Emit per-library Markdown bullets to"
|
|
| 68 | + , " stdout (suppresses RST emission). Output"
|
|
| 69 | + , " is intended to be pasted into each"
|
|
| 70 | + , " libraries/<lib>/changelog.md by hand;"
|
|
| 71 | + , " --section <key> Restrict --libraries-changelog-markdown"
|
|
| 72 | + , " to a single section. Without this, all"
|
|
| 73 | + , " configured markdown-targets are emitted,"
|
|
| 74 | + , " separated by HTML-comment markers."
|
|
| 75 | + , " --help Show this help"
|
|
| 65 | 76 | ]
|
| 66 | 77 | |
| 67 | 78 | parseArgs :: [String] -> Either String Opts
|
| 68 | 79 | parseArgs = go defaultOpts
|
| 69 | 80 | where
|
| 70 | - defaultOpts = Opts "changelog.d" Nothing False Nothing
|
|
| 81 | + defaultOpts = Opts
|
|
| 82 | + { optDirectory = "changelog.d"
|
|
| 83 | + , optVersion = Nothing
|
|
| 84 | + , optValidate = False
|
|
| 85 | + , optExpectMR = Nothing
|
|
| 86 | + , optExpectCLC = False
|
|
| 87 | + , optMarkdown = False
|
|
| 88 | + , optMdSection = Nothing
|
|
| 89 | + }
|
|
| 71 | 90 | |
| 72 | 91 | go opts [] = Right opts
|
| 73 | 92 | go _ ("--help" : _) = Left ""
|
| ... | ... | @@ -78,6 +97,11 @@ parseArgs = go defaultOpts |
| 78 | 97 | [(mr, "")] -> go opts { optExpectMR = Just mr } rest
|
| 79 | 98 | _ -> Left $ "--expect-mr requires a number, got: " ++ n
|
| 80 | 99 | go _ ("--expect-mr" : []) = Left "--expect-mr requires an argument"
|
| 100 | + go opts ("--expect-clc" : rest) = go opts { optExpectCLC = True } rest
|
|
| 101 | + go opts ("--libraries-changelog-markdown" : rest) =
|
|
| 102 | + go opts { optMarkdown = True } rest
|
|
| 103 | + go opts ("--section" : s : rest) = go opts { optMdSection = Just s } rest
|
|
| 104 | + go _ ("--section" : []) = Left "--section requires an argument"
|
|
| 81 | 105 | go _ (('-':'-':opt) : _) = Left $ "Unknown option: --" ++ opt
|
| 82 | 106 | go _ (('-':opt) : _) = Left $ "Unknown option: -" ++ opt
|
| 83 | 107 | go opts (dir : rest) = go opts { optDirectory = dir } rest
|
| ... | ... | @@ -124,9 +148,14 @@ makeChangelog Opts {..} = do |
| 124 | 148 | either (exitWithExc . PlainError) return $
|
| 125 | 149 | parseWith parseConfig filename contents
|
| 126 | 150 | |
| 151 | + -- Read only regular files, skipping config, dotfiles, and any
|
|
| 152 | + -- subdirectories (e.g. golden-output dirs alongside test fragments).
|
|
| 127 | 153 | dirContents <- filter (not . isTmpFile) <$> listDirectory optDirectory
|
| 154 | + fragmentNames <-
|
|
| 155 | + filterM (\name -> doesFileExist (optDirectory </> name))
|
|
| 156 | + (filter (/= "config") $ sort dirContents)
|
|
| 128 | 157 | allEntries <- fmap Map.fromList $
|
| 129 | - for (filter (/= "config") $ sort dirContents) $ \name -> do
|
|
| 158 | + for fragmentNames $ \name -> do
|
|
| 130 | 159 | let fp = optDirectory </> name
|
| 131 | 160 | contents <- BS.readFile fp
|
| 132 | 161 | entry <- parseEntryFile fp contents
|
| ... | ... | @@ -140,17 +169,38 @@ makeChangelog Opts {..} = do |
| 140 | 169 | exitWithExc $ PlainError "Validation failed."
|
| 141 | 170 | |
| 142 | 171 | -- Check expected MR number if specified
|
| 143 | - for_ optExpectMR $ \expectedMR -> do
|
|
| 144 | - let expectedMRNum = MRNumber expectedMR
|
|
| 145 | - entriesWithMR = Map.filter (\e -> expectedMRNum `Set.member` entryMrs e) allEntries
|
|
| 146 | - when (Map.null entriesWithMR && not (Map.null allEntries)) $ do
|
|
| 147 | - hPutStrLn stderr $ "Warning: No changelog entry references this MR (!" ++ show expectedMR ++ ")."
|
|
| 148 | - hPutStrLn stderr $ "Add 'mrs: !" ++ show expectedMR ++ "' to your changelog entry."
|
|
| 149 | - hPutStrLn stderr ""
|
|
| 150 | - exitFailure
|
|
| 172 | + matchedByMR <- case optExpectMR of
|
|
| 173 | + Nothing -> pure Map.empty
|
|
| 174 | + Just expectedMR -> do
|
|
| 175 | + let expectedMRNum = MRNumber expectedMR
|
|
| 176 | + withMR = Map.filter (\e -> expectedMRNum `Set.member` entryMrs e) allEntries
|
|
| 177 | + when (Map.null withMR && not (Map.null allEntries)) $ do
|
|
| 178 | + hPutStrLn stderr $ "Warning: No changelog entry references this MR (!" ++ show expectedMR ++ ")."
|
|
| 179 | + hPutStrLn stderr $ "Add 'mrs: !" ++ show expectedMR ++ "' to your changelog entry."
|
|
| 180 | + hPutStrLn stderr ""
|
|
| 181 | + exitFailure
|
|
| 182 | + pure withMR
|
|
| 183 | + |
|
| 184 | + -- --expect-clc: assert that the MR-matched entry has clc: set.
|
|
| 185 | + when optExpectCLC $ case optExpectMR of
|
|
| 186 | + Nothing -> exitWithExc $ PlainError
|
|
| 187 | + "--expect-clc requires --expect-mr (which entry to check?)"
|
|
| 188 | + Just expectedMR ->
|
|
| 189 | + when (not (Map.null matchedByMR)
|
|
| 190 | + && all (Set.null . entryClcs) matchedByMR) $ do
|
|
| 191 | + hPutStrLn stderr $
|
|
| 192 | + "Error: changelog entry for !" ++ show expectedMR
|
|
| 193 | + ++ " does not have a 'clc:' field."
|
|
| 194 | + hPutStrLn stderr
|
|
| 195 | + "Changes to base or user-facing changes require a CLC proposal."
|
|
| 196 | + hPutStrLn stderr "Add 'clc: #<proposal>' to your changelog entry."
|
|
| 197 | + exitFailure
|
|
| 151 | 198 | |
| 152 | 199 | unless optValidate $
|
| 153 | - outputRST optDirectory optVersion cfg (Map.elems allEntries)
|
|
| 200 | + if optMarkdown
|
|
| 201 | + then outputMarkdown optDirectory cfg optMdSection
|
|
| 202 | + (Map.elems allEntries)
|
|
| 203 | + else outputRST optDirectory optVersion cfg (Map.elems allEntries)
|
|
| 154 | 204 | |
| 155 | 205 | -------------------------------------------------------------------------------
|
| 156 | 206 | -- RST output
|
| ... | ... | @@ -218,6 +268,9 @@ formatEntry Entry {..} = |
| 218 | 268 | ] ++
|
| 219 | 269 | [ "(:ghc-mr:`" ++ show n ++ "`)"
|
| 220 | 270 | | MRNumber n <- Set.toList entryMrs
|
| 271 | + ] ++
|
|
| 272 | + [ "(:clc:`" ++ show n ++ "`)"
|
|
| 273 | + | CLCNumber n <- Set.toList entryClcs
|
|
| 221 | 274 | ]
|
| 222 | 275 | |
| 223 | 276 | description = maybe "" (\d -> "\n" ++ trim d ++ "\n\n") entryDescription
|
| ... | ... | @@ -262,25 +315,281 @@ generateIncludedLibraries baseDir preamble libs = do |
| 262 | 315 | where
|
| 263 | 316 | fst3 (a, _, _) = a
|
| 264 | 317 | |
| 265 | - extractField :: String -> String -> Maybe String
|
|
| 266 | - extractField fieldName contents =
|
|
| 267 | - case mapMaybe (matchField fieldName) (lines contents) of
|
|
| 268 | - (v:_) -> Just v
|
|
| 269 | - [] -> Nothing
|
|
| 270 | - |
|
| 271 | - matchField :: String -> String -> Maybe String
|
|
| 272 | - matchField fieldName line =
|
|
| 273 | - let stripped = dropWhile isSpace line
|
|
| 274 | - (key, rest) = break (\c -> c == ':' || isSpace c) stripped
|
|
| 275 | - in if map toLower' key == map toLower' fieldName
|
|
| 276 | - then case dropWhile isSpace rest of
|
|
| 277 | - (':':val) -> Just (trim (dropWhile isSpace val))
|
|
| 278 | - _ -> Nothing
|
|
| 279 | - else Nothing
|
|
| 280 | - |
|
| 281 | - toLower' c
|
|
| 282 | - | c >= 'A' && c <= 'Z' = toEnum (fromEnum c + 32)
|
|
| 283 | - | otherwise = c
|
|
| 318 | +extractField :: String -> String -> Maybe String
|
|
| 319 | +extractField fieldName contents =
|
|
| 320 | + case mapMaybe (matchField fieldName) (lines contents) of
|
|
| 321 | + (v:_) -> Just v
|
|
| 322 | + [] -> Nothing
|
|
| 323 | + |
|
| 324 | +matchField :: String -> String -> Maybe String
|
|
| 325 | +matchField fieldName line =
|
|
| 326 | + let stripped = dropWhile isSpace line
|
|
| 327 | + (key, rest) = break (\c -> c == ':' || isSpace c) stripped
|
|
| 328 | + in if map toLower' key == map toLower' fieldName
|
|
| 329 | + then case dropWhile isSpace rest of
|
|
| 330 | + (':':val) -> Just (trim (dropWhile isSpace val))
|
|
| 331 | + _ -> Nothing
|
|
| 332 | + else Nothing
|
|
| 333 | + |
|
| 334 | +toLower' :: Char -> Char
|
|
| 335 | +toLower' c
|
|
| 336 | + | c >= 'A' && c <= 'Z' = toEnum (fromEnum c + 32)
|
|
| 337 | + | otherwise = c
|
|
| 338 | + |
|
| 339 | +-------------------------------------------------------------------------------
|
|
| 340 | +-- Markdown output
|
|
| 341 | +-------------------------------------------------------------------------------
|
|
| 342 | + |
|
| 343 | +-- | Emit per-library Markdown bullets to stdout.
|
|
| 344 | +--
|
|
| 345 | +-- With 'mSect' set, emit just that section's bullets (used interactively).
|
|
| 346 | +-- Without it, emit every section listed in @markdown-targets:@, separated
|
|
| 347 | +-- by HTML comments naming each section
|
|
| 348 | +outputMarkdown
|
|
| 349 | + :: FilePath -- ^ changelog.d directory (used to locate cabal files)
|
|
| 350 | + -> Cfg
|
|
| 351 | + -> Maybe String -- ^ --section <key>
|
|
| 352 | + -> [Entry]
|
|
| 353 | + -> IO ()
|
|
| 354 | +outputMarkdown dir Cfg{..} mSect entries = do
|
|
| 355 | + targets <- case mSect of
|
|
| 356 | + Just key -> case find ((== key) . mtSection) cfgMarkdownTargets of
|
|
| 357 | + Nothing -> exitWithExc $ PlainError $
|
|
| 358 | + "Unknown markdown section: " ++ key
|
|
| 359 | + ++ "\nKnown sections: "
|
|
| 360 | + ++ intercalate ", " (map mtSection cfgMarkdownTargets)
|
|
| 361 | + Just mt -> pure [mt]
|
|
| 362 | + Nothing -> pure cfgMarkdownTargets
|
|
| 363 | + |
|
| 364 | + let multi = isNothing mSect
|
|
| 365 | + baseDir = takeDirectory (dropTrailingPathSeparator dir)
|
|
| 366 | + |
|
| 367 | + case mSect of
|
|
| 368 | + Just key | not (any (\mt -> mtSection mt == key) cfgMarkdownTargets) ->
|
|
| 369 | + -- impossible; handled above
|
|
| 370 | + pure ()
|
|
| 371 | + Just key | null (entriesFor key entries) ->
|
|
| 372 | + exitWithExc $ PlainError $ "No entries for section " ++ key
|
|
| 373 | + _ -> pure ()
|
|
| 374 | + |
|
| 375 | + for_ targets $ \mt -> do
|
|
| 376 | + let es = entriesFor (mtSection mt) entries
|
|
| 377 | + unless (null es) $ do
|
|
| 378 | + when multi $ do
|
|
| 379 | + putStrLn $ "<!-- ===== " ++ mtSection mt
|
|
| 380 | + ++ " (" ++ mtPath mt ++ ") ===== -->"
|
|
| 381 | + putStrLn ""
|
|
| 382 | + libVer <- readLibraryVersion baseDir (mtPath mt)
|
|
| 383 | + putStrLn $ "## " ++ libVer ++ " *TBA*"
|
|
| 384 | + putStrLn ""
|
|
| 385 | + for_ (sortBy (flip compare `on` hasDescription) es) $ \entry ->
|
|
| 386 | + putStr (formatEntryMd entry)
|
|
| 387 | + when multi $ putStrLn ""
|
|
| 388 | + |
|
| 389 | +entriesFor :: String -> [Entry] -> [Entry]
|
|
| 390 | +entriesFor key = filter $ \e -> case entrySection e of
|
|
| 391 | + Just (Section s) -> s == key
|
|
| 392 | + Nothing -> False
|
|
| 393 | + |
|
| 394 | +-- | Given the path of a library's @changelog.md@ (repo-relative), find the
|
|
| 395 | +-- sibling @*.cabal@ (or @*.cabal.in@) and read the @version:@ field.
|
|
| 396 | +readLibraryVersion :: FilePath -> FilePath -> IO String
|
|
| 397 | +readLibraryVersion baseDir mdPath = do
|
|
| 398 | + let libDir = takeDirectory mdPath
|
|
| 399 | + libDirFs = baseDir </> libDir
|
|
| 400 | + exists <- doesDirectoryExist libDirFs
|
|
| 401 | + if not exists
|
|
| 402 | + then do
|
|
| 403 | + hPutStrLn stderr $ "Warning: directory does not exist: " ++ libDirFs
|
|
| 404 | + pure "?.?.?"
|
|
| 405 | + else do
|
|
| 406 | + candidates <- listDirectory libDirFs
|
|
| 407 | + let cabals = filter (\f -> ".cabal" `isSuffixOf` f) candidates
|
|
| 408 | + -- Prefer non-templated *.cabal over *.cabal.in (the former is
|
|
| 409 | + -- the rendered file Hadrian needs before invoking us).
|
|
| 410 | + ranked = sortBy (compare `on` (\f -> if ".cabal.in" `isSuffixOf` f then (1::Int) else 0)) cabals
|
|
| 411 | + case ranked of
|
|
| 412 | + [] -> do
|
|
| 413 | + hPutStrLn stderr $
|
|
| 414 | + "Warning: no .cabal file under " ++ libDir
|
|
| 415 | + pure "?.?.?"
|
|
| 416 | + (cf:_) -> do
|
|
| 417 | + contents <- readFile (libDirFs </> cf)
|
|
| 418 | + case extractField "version" contents of
|
|
| 419 | + Just v -> pure v
|
|
| 420 | + Nothing -> do
|
|
| 421 | + hPutStrLn stderr $
|
|
| 422 | + "Warning: could not parse version from " ++ libDir </> cf
|
|
| 423 | + pure "?.?.?"
|
|
| 424 | + |
|
| 425 | +-- | Format an Entry as a Markdown bullet. Mirrors 'formatEntry' for RST
|
|
| 426 | +-- but emits Markdown links for issues/MRs/CLC and rewrites RST inline
|
|
| 427 | +-- markup to markdown.
|
|
| 428 | +formatEntryMd :: Entry -> String
|
|
| 429 | +formatEntryMd Entry{..} = indentBulletMd (header ++ description)
|
|
| 430 | + where
|
|
| 431 | + header = unwords $
|
|
| 432 | + [ rstToMarkdown entrySynopsis ] ++
|
|
| 433 | + [ mdLink ("#" ++ show n)
|
|
| 434 | + ("https://gitlab.haskell.org/ghc/ghc/issues/" ++ show n)
|
|
| 435 | + | IssueNumber n <- Set.toList entryIssues
|
|
| 436 | + ] ++
|
|
| 437 | + [ mdLink ("!" ++ show n)
|
|
| 438 | + ("https://gitlab.haskell.org/ghc/ghc/-/merge_requests/" ++ show n)
|
|
| 439 | + | MRNumber n <- Set.toList entryMrs
|
|
| 440 | + ] ++
|
|
| 441 | + [ mdLink ("CLC proposal #" ++ show n)
|
|
| 442 | + ("https://github.com/haskell/core-libraries-committee/issues/" ++ show n)
|
|
| 443 | + | CLCNumber n <- Set.toList entryClcs
|
|
| 444 | + ]
|
|
| 445 | + |
|
| 446 | + description = maybe "" (\d -> "\n\n" ++ rstToMarkdown (trim d) ++ "\n") entryDescription
|
|
| 447 | + |
|
| 448 | + mdLink :: String -> String -> String
|
|
| 449 | + mdLink txt url = "(" ++ "[" ++ txt ++ "](" ++ url ++ ")" ++ ")"
|
|
| 450 | + |
|
| 451 | +-- | Indent text as a Markdown bullet: the first line gets @"* "@ prefix,
|
|
| 452 | +-- subsequent lines are indented two spaces. Mirrors 'indentBullet'.
|
|
| 453 | +indentBulletMd :: String -> String
|
|
| 454 | +indentBulletMd = unlines . go . lines
|
|
| 455 | + where
|
|
| 456 | + go [] = []
|
|
| 457 | + go (x:xs) = ("* " ++ x) : map indentLine xs
|
|
| 458 | + indentLine "" = ""
|
|
| 459 | + indentLine s = " " ++ s
|
|
| 460 | + |
|
| 461 | +-------------------------------------------------------------------------------
|
|
| 462 | +-- RST -> Markdown rewriting
|
|
| 463 | +-------------------------------------------------------------------------------
|
|
| 464 | +--
|
|
| 465 | +-- Applies the following rules:
|
|
| 466 | +--
|
|
| 467 | +-- | RST | Markdown |
|
|
| 468 | +-- | -------------------------------------------------| ------------------------------------------------------------------------------------------------------ |
|
|
| 469 | +-- | ``code`` (double-backtick) | `code` (single-backtick) |
|
|
| 470 | +-- | `text <url>`_ | [text](url) |
|
|
| 471 | +-- | :ghc-ticket:`N` | [#N](https://gitlab.haskell.org/ghc/ghc/issues/N) |
|
|
| 472 | +-- | :ghc-mr:`N` | [!N](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/N) |
|
|
| 473 | +-- | :ghc-wiki:`p` | [p](https://gitlab.haskell.org/ghc/ghc/wikis/p) |
|
|
| 474 | +-- | :clc:`N` | [CLC proposal #N](https://github.com/haskell/core-libraries-committee/issues/N) |
|
|
| 475 | +-- | :ghc-flag:`-foo` | `-foo` |
|
|
| 476 | +-- | :extension:`E` | `E` |
|
|
| 477 | +-- | :ghci-cmd:`X`, :rts-flag:`X` | `X` |
|
|
| 478 | +-- | :base-ref:`Mod.id` `` | `Mod.id` |
|
|
| 479 | +-- | :th-ref:, :cabal-ref: ,:ghc-prim-ref: | `ref` |
|
|
| 480 | +-- | .. code-block:: lang + indented body | Triple-backtick fenced block with `lang` |
|
|
| 481 | +-- | .. note:: / .. warning:: | `> **Note:**` / `> **Warning:**` blockquote |
|
|
| 482 | + |
|
| 483 | +rstToMarkdown :: String -> String
|
|
| 484 | +rstToMarkdown s =
|
|
| 485 | + let trailingNL = not (null s) && last s == '\n'
|
|
| 486 | + body = intercalate "\n" . blockPass . lines . inlinePass $ s
|
|
| 487 | + in body ++ (if trailingNL then "\n" else "")
|
|
| 488 | + |
|
| 489 | +inlinePass :: String -> String
|
|
| 490 | +inlinePass [] = []
|
|
| 491 | +-- Double-backtick code: ``code`` → `code`
|
|
| 492 | +inlinePass ('`':'`':rest) =
|
|
| 493 | + case breakOnSubstring "``" rest of
|
|
| 494 | + (body, _:_:after) -> "`" ++ body ++ "`" ++ inlinePass after
|
|
| 495 | + _ -> '`':'`': inlinePass rest
|
|
| 496 | +-- RST hyperlink: `text <url>`_ → [text](url)
|
|
| 497 | +inlinePass ('`':rest)
|
|
| 498 | + | Just (txt, url, after) <- pickRstLink rest =
|
|
| 499 | + "[" ++ trim txt ++ "](" ++ url ++ ")" ++ inlinePass after
|
|
| 500 | +-- :role:`body` interpreted-text role
|
|
| 501 | +inlinePass (':':rest)
|
|
| 502 | + | Just (role, body, after) <- pickRole rest =
|
|
| 503 | + renderRole role body ++ inlinePass after
|
|
| 504 | +inlinePass (c:cs) = c : inlinePass cs
|
|
| 505 | + |
|
| 506 | +breakOnSubstring :: String -> String -> (String, String)
|
|
| 507 | +breakOnSubstring needle = go
|
|
| 508 | + where
|
|
| 509 | + go [] = ([], [])
|
|
| 510 | + go s@(c:cs)
|
|
| 511 | + | needle `isPrefixOf` s = ([], s)
|
|
| 512 | + | otherwise =
|
|
| 513 | + let (a, b) = go cs in (c:a, b)
|
|
| 514 | + |
|
| 515 | +-- | Try to consume a @\`text \<url\>\`_@ RST hyperlink starting after the
|
|
| 516 | +-- leading backtick. Returns @(text, url, rest)@ on success.
|
|
| 517 | +pickRstLink :: String -> Maybe (String, String, String)
|
|
| 518 | +pickRstLink xs = do
|
|
| 519 | + let (txt, r1) = break (== '<') xs
|
|
| 520 | + case r1 of
|
|
| 521 | + '<':r2 -> do
|
|
| 522 | + let (url, r3) = break (== '>') r2
|
|
| 523 | + case r3 of
|
|
| 524 | + '>':'`':'_':'_':after -> Just (txt, url, after)
|
|
| 525 | + '>':'`':'_':after -> Just (txt, url, after)
|
|
| 526 | + _ -> Nothing
|
|
| 527 | + _ -> Nothing
|
|
| 528 | + |
|
| 529 | +-- | Try to consume a @role:\`body\`@ interpreted-text role starting just
|
|
| 530 | +-- after the leading colon.
|
|
| 531 | +pickRole :: String -> Maybe (String, String, String)
|
|
| 532 | +pickRole xs =
|
|
| 533 | + let (name, r1) = span (\c -> isAlpha c || c == '-') xs
|
|
| 534 | + in case (null name, r1) of
|
|
| 535 | + (False, ':':'`':r2) -> case break (== '`') r2 of
|
|
| 536 | + (body, '`':after) | not (null body) -> Just (name, body, after)
|
|
| 537 | + _ -> Nothing
|
|
| 538 | + _ -> Nothing
|
|
| 539 | + |
|
| 540 | +-- | Render a known interpreted-text role to Markdown.
|
|
| 541 | +renderRole :: String -> String -> String
|
|
| 542 | +renderRole role body = case role of
|
|
| 543 | + "ghc-ticket" -> mdLink ("#" ++ body) ("https://gitlab.haskell.org/ghc/ghc/issues/" ++ body)
|
|
| 544 | + "ghc-mr" -> mdLink ("!" ++ body) ("https://gitlab.haskell.org/ghc/ghc/-/merge_requests/" ++ body)
|
|
| 545 | + "ghc-wiki" -> mdLink body ("https://gitlab.haskell.org/ghc/ghc/wikis/" ++ body)
|
|
| 546 | + "clc" -> mdLink ("CLC proposal #" ++ body)
|
|
| 547 | + ("https://github.com/haskell/core-libraries-committee/issues/" ++ body)
|
|
| 548 | + "ghc-flag" -> "`" ++ body ++ "`"
|
|
| 549 | + "extension" -> "`" ++ body ++ "`"
|
|
| 550 | + "ghci-cmd" -> "`" ++ body ++ "`"
|
|
| 551 | + "rts-flag" -> "`" ++ body ++ "`"
|
|
| 552 | + "doc" -> body
|
|
| 553 | + "base-ref" -> "`" ++ body ++ "`"
|
|
| 554 | + "th-ref" -> "`" ++ body ++ "`"
|
|
| 555 | + "cabal-ref" -> "`" ++ body ++ "`"
|
|
| 556 | + "ghc-prim-ref" -> "`" ++ body ++ "`"
|
|
| 557 | + _ -> ":" ++ role ++ ":`" ++ body ++ "`"
|
|
| 558 | + where
|
|
| 559 | + mdLink txt url = "[" ++ txt ++ "](" ++ url ++ ")"
|
|
| 560 | + |
|
| 561 | +-- | Block-level transforms applied after the inline pass.
|
|
| 562 | +blockPass :: [String] -> [String]
|
|
| 563 | +blockPass [] = []
|
|
| 564 | +blockPass (l:rest)
|
|
| 565 | + | Just lang <- stripPrefix ".. code-block:: " (trim l) =
|
|
| 566 | + let (body, rest') = takeIndentedBlock rest
|
|
| 567 | + in ("```" ++ lang) : map (dropIndent 4) body ++ ["```"] ++ blockPass rest'
|
|
| 568 | + | trim l == ".. note::" =
|
|
| 569 | + let (body, rest') = takeIndentedBlock rest
|
|
| 570 | + in "> **Note:**" : map (("> " ++) . dropIndent 4) body ++ blockPass rest'
|
|
| 571 | + | trim l == ".. warning::" =
|
|
| 572 | + let (body, rest') = takeIndentedBlock rest
|
|
| 573 | + in "> **Warning:**" : map (("> " ++) . dropIndent 4) body ++ blockPass rest'
|
|
| 574 | + | otherwise = l : blockPass rest
|
|
| 575 | + |
|
| 576 | +-- | Take a block of indented (or blank) lines following a directive; stop
|
|
| 577 | +-- at the first non-blank, non-indented line.
|
|
| 578 | +takeIndentedBlock :: [String] -> ([String], [String])
|
|
| 579 | +takeIndentedBlock = go . dropWhile null
|
|
| 580 | + where
|
|
| 581 | + go [] = ([], [])
|
|
| 582 | + go (x:xs)
|
|
| 583 | + | null x = let (a, b) = go xs in (x:a, b)
|
|
| 584 | + | take 1 x == " " = let (a, b) = go xs in (x:a, b)
|
|
| 585 | + | otherwise = ([], x:xs)
|
|
| 586 | + |
|
| 587 | +-- | Drop up to @n@ leading spaces from a line.
|
|
| 588 | +dropIndent :: Int -> String -> String
|
|
| 589 | +dropIndent _ "" = ""
|
|
| 590 | +dropIndent 0 s = s
|
|
| 591 | +dropIndent n (' ':cs) = dropIndent (n-1) cs
|
|
| 592 | +dropIndent _ s = s
|
|
| 284 | 593 | |
| 285 | 594 | -------------------------------------------------------------------------------
|
| 286 | 595 | -- Section grouping
|
| ... | ... | @@ -303,10 +612,13 @@ groupBySections sectionDefs entries = |
| 303 | 612 | -------------------------------------------------------------------------------
|
| 304 | 613 | |
| 305 | 614 | data Opts = Opts
|
| 306 | - { optDirectory :: FilePath
|
|
| 307 | - , optVersion :: Maybe String
|
|
| 308 | - , optValidate :: Bool
|
|
| 309 | - , optExpectMR :: Maybe Int -- ^ Expected MR number
|
|
| 615 | + { optDirectory :: FilePath
|
|
| 616 | + , optVersion :: Maybe String
|
|
| 617 | + , optValidate :: Bool
|
|
| 618 | + , optExpectMR :: Maybe Int -- ^ Expected MR number
|
|
| 619 | + , optExpectCLC :: Bool -- ^ Require entry matched by --expect-mr to have clc:
|
|
| 620 | + , optMarkdown :: Bool -- ^ Emit per-library Markdown to stdout
|
|
| 621 | + , optMdSection :: Maybe String -- ^ Restrict markdown emission to one section
|
|
| 310 | 622 | }
|
| 311 | 623 | deriving (Show)
|
| 312 | 624 | |
| ... | ... | @@ -332,6 +644,24 @@ instance C.Parsec MRNumber where |
| 332 | 644 | instance C.Pretty MRNumber where
|
| 333 | 645 | pretty (MRNumber n) = PP.char '!' PP.<> PP.int n
|
| 334 | 646 | |
| 647 | +newtype CLCNumber = CLCNumber Int
|
|
| 648 | + deriving (Eq, Ord, Show)
|
|
| 649 | + |
|
| 650 | +instance C.Parsec CLCNumber where
|
|
| 651 | + parsec = do
|
|
| 652 | + _ <- P.char '#'
|
|
| 653 | + CLCNumber <$> P.integral
|
|
| 654 | + |
|
| 655 | +instance C.Pretty CLCNumber where
|
|
| 656 | + pretty (CLCNumber n) = PP.char '#' PP.<> PP.int n
|
|
| 657 | + |
|
| 658 | +data MarkdownTarget = MarkdownTarget
|
|
| 659 | + { mtSection :: String -- ^ section key matching an entry's `section:`
|
|
| 660 | + , mtPath :: FilePath -- ^ target changelog path, repo-relative
|
|
| 661 | + , mtRequiredFields :: [String] -- ^ extra required-fields when this section is used
|
|
| 662 | + }
|
|
| 663 | + deriving (Show)
|
|
| 664 | + |
|
| 335 | 665 | newtype Section = Section String
|
| 336 | 666 | deriving (Eq, Ord, Show)
|
| 337 | 667 | |
| ... | ... | @@ -351,6 +681,7 @@ data Cfg = Cfg |
| 351 | 681 | , cfgPreamble :: String
|
| 352 | 682 | , cfgIncludedLibraries :: [(FilePath, String)] -- ^ (cabalPath, description)
|
| 353 | 683 | , cfgIncludedLibrariesPreamble :: String
|
| 684 | + , cfgMarkdownTargets :: [MarkdownTarget]
|
|
| 354 | 685 | }
|
| 355 | 686 | deriving (Show)
|
| 356 | 687 | |
| ... | ... | @@ -364,6 +695,7 @@ parseConfig fields0 = do |
| 364 | 695 | , cfgPreamble = cfgRawPreamble raw
|
| 365 | 696 | , cfgIncludedLibraries = parseIncludedLibraries (cfgRawIncludedLibraries raw)
|
| 366 | 697 | , cfgIncludedLibrariesPreamble = cfgRawIncludedLibrariesPreamble raw
|
| 698 | + , cfgMarkdownTargets = parseMarkdownTargets (cfgRawMarkdownTargets raw)
|
|
| 367 | 699 | }
|
| 368 | 700 | where
|
| 369 | 701 | (fields, sections) = C.partitionFields fields0
|
| ... | ... | @@ -378,6 +710,7 @@ data CfgRaw = CfgRaw |
| 378 | 710 | , cfgRawPreamble :: String
|
| 379 | 711 | , cfgRawIncludedLibraries :: String
|
| 380 | 712 | , cfgRawIncludedLibrariesPreamble :: String
|
| 713 | + , cfgRawMarkdownTargets :: String
|
|
| 381 | 714 | }
|
| 382 | 715 | |
| 383 | 716 | cfgRawRequiredFieldsL :: Functor f => (Set String -> f (Set String)) -> CfgRaw -> f CfgRaw
|
| ... | ... | @@ -395,6 +728,9 @@ cfgRawIncludedLibrariesL f s = (\x -> s { cfgRawIncludedLibraries = x }) <$> f ( |
| 395 | 728 | cfgRawIncludedLibrariesPreambleL :: Functor f => (String -> f String) -> CfgRaw -> f CfgRaw
|
| 396 | 729 | cfgRawIncludedLibrariesPreambleL f s = (\x -> s { cfgRawIncludedLibrariesPreamble = x }) <$> f (cfgRawIncludedLibrariesPreamble s)
|
| 397 | 730 | |
| 731 | +cfgRawMarkdownTargetsL :: Functor f => (String -> f String) -> CfgRaw -> f CfgRaw
|
|
| 732 | +cfgRawMarkdownTargetsL f s = (\x -> s { cfgRawMarkdownTargets = x }) <$> f (cfgRawMarkdownTargets s)
|
|
| 733 | + |
|
| 398 | 734 | cfgRawGrammar :: C.ParsecFieldGrammar CfgRaw CfgRaw
|
| 399 | 735 | cfgRawGrammar = CfgRaw
|
| 400 | 736 | <$> C.monoidalFieldAla "required-fields" (C.alaSet' C.FSep C.Token) cfgRawRequiredFieldsL
|
| ... | ... | @@ -402,6 +738,7 @@ cfgRawGrammar = CfgRaw |
| 402 | 738 | <*> C.freeTextFieldDef "preamble" cfgRawPreambleL
|
| 403 | 739 | <*> C.freeTextFieldDef "included-libraries" cfgRawIncludedLibrariesL
|
| 404 | 740 | <*> C.freeTextFieldDef "included-libraries-preamble" cfgRawIncludedLibrariesPreambleL
|
| 741 | + <*> C.freeTextFieldDef "markdown-targets" cfgRawMarkdownTargetsL
|
|
| 405 | 742 | |
| 406 | 743 | parseSections :: String -> [(String, String)]
|
| 407 | 744 | parseSections = mapMaybe parseLine . lines
|
| ... | ... | @@ -419,6 +756,20 @@ parseIncludedLibraries = mapMaybe parseLine . lines |
| 419 | 756 | (path, rest) | not (null path) -> Just (path, trim rest)
|
| 420 | 757 | _ -> Nothing
|
| 421 | 758 | |
| 759 | +-- | Parse the @markdown-targets:@ block.
|
|
| 760 | +--
|
|
| 761 | +-- Each non-empty, non-comment line is
|
|
| 762 | +-- <section-key> <path> [<extra-required-field>...]
|
|
| 763 | +-- The extra tokens declare additional fields required of any entry whose section: matches.
|
|
| 764 | +parseMarkdownTargets :: String -> [MarkdownTarget]
|
|
| 765 | +parseMarkdownTargets = mapMaybe parseLine . lines
|
|
| 766 | + where
|
|
| 767 | + parseLine l = case words (trim l) of
|
|
| 768 | + [] -> Nothing
|
|
| 769 | + [_] -> Nothing -- need at least section + path
|
|
| 770 | + (sect:path:extra) ->
|
|
| 771 | + Just $ MarkdownTarget sect path extra
|
|
| 772 | + |
|
| 422 | 773 | -------------------------------------------------------------------------------
|
| 423 | 774 | -- Entry
|
| 424 | 775 | -------------------------------------------------------------------------------
|
| ... | ... | @@ -428,6 +779,7 @@ data Entry = Entry |
| 428 | 779 | , entryDescription :: Maybe String
|
| 429 | 780 | , entryMrs :: Set MRNumber
|
| 430 | 781 | , entryIssues :: Set IssueNumber
|
| 782 | + , entryClcs :: Set CLCNumber
|
|
| 431 | 783 | , entrySection :: Maybe Section
|
| 432 | 784 | }
|
| 433 | 785 | deriving (Show)
|
| ... | ... | @@ -447,6 +799,9 @@ entryMrsL f s = (\x -> s { entryMrs = x }) <$> f (entryMrs s) |
| 447 | 799 | entryIssuesL :: Functor f => (Set IssueNumber -> f (Set IssueNumber)) -> Entry -> f Entry
|
| 448 | 800 | entryIssuesL f s = (\x -> s { entryIssues = x }) <$> f (entryIssues s)
|
| 449 | 801 | |
| 802 | +entryClcsL :: Functor f => (Set CLCNumber -> f (Set CLCNumber)) -> Entry -> f Entry
|
|
| 803 | +entryClcsL f s = (\x -> s { entryClcs = x }) <$> f (entryClcs s)
|
|
| 804 | + |
|
| 450 | 805 | entrySectionL :: Functor f => (Maybe Section -> f (Maybe Section)) -> Entry -> f Entry
|
| 451 | 806 | entrySectionL f s = (\x -> s { entrySection = x }) <$> f (entrySection s)
|
| 452 | 807 | |
| ... | ... | @@ -477,6 +832,7 @@ entryGrammar = Entry |
| 477 | 832 | <*> C.freeTextField "description" entryDescriptionL
|
| 478 | 833 | <*> C.monoidalFieldAla "mrs" (C.alaSet C.NoCommaFSep) entryMrsL
|
| 479 | 834 | <*> C.monoidalFieldAla "issues" (C.alaSet C.NoCommaFSep) entryIssuesL
|
| 835 | + <*> C.monoidalFieldAla "clc" (C.alaSet C.NoCommaFSep) entryClcsL
|
|
| 480 | 836 | <*> C.optionalField "section" entrySectionL
|
| 481 | 837 | |
| 482 | 838 | -------------------------------------------------------------------------------
|
| ... | ... | @@ -510,8 +866,21 @@ validateEntry cfg entry = foldMap (\validator -> validator cfg entry) |
| 510 | 866 | |
| 511 | 867 | validateRequiredFields :: Validator
|
| 512 | 868 | validateRequiredFields Cfg{..} Entry{..} = fmap RequiredFieldError $
|
| 513 | - mapMaybe checkField $ Set.toList cfgRequiredFields
|
|
| 869 | + mapMaybe checkField $ Set.toList effectiveRequired
|
|
| 514 | 870 | where
|
| 871 | + -- Effective required-fields = global cfgRequiredFields + extras for the
|
|
| 872 | + -- entry's section as declared in cfgMarkdownTargets
|
|
| 873 | + -- (e.g. `base` adds `clc`).
|
|
| 874 | + effectiveRequired =
|
|
| 875 | + cfgRequiredFields `Set.union`
|
|
| 876 | + Set.fromList
|
|
| 877 | + [ f
|
|
| 878 | + | Just (Section sect) <- [entrySection]
|
|
| 879 | + , mt <- cfgMarkdownTargets
|
|
| 880 | + , mtSection mt == sect
|
|
| 881 | + , f <- mtRequiredFields mt
|
|
| 882 | + ]
|
|
| 883 | + |
|
| 515 | 884 | checkField :: String -> Maybe RequiredFieldError
|
| 516 | 885 | checkField reqField = case fieldIsEmpty reqField of
|
| 517 | 886 | Left err -> Just err
|
| ... | ... | @@ -522,6 +891,7 @@ validateRequiredFields Cfg{..} Entry{..} = fmap RequiredFieldError $ |
| 522 | 891 | fieldIsEmpty "description" = pure $ isNothing entryDescription
|
| 523 | 892 | fieldIsEmpty "mrs" = pure $ null entryMrs
|
| 524 | 893 | fieldIsEmpty "issues" = pure $ null entryIssues
|
| 894 | + fieldIsEmpty "clc" = pure $ null entryClcs
|
|
| 525 | 895 | fieldIsEmpty "section" = pure $ isNothing entrySection
|
| 526 | 896 | fieldIsEmpty f = Left $ UnknownRequiredField f
|
| 527 | 897 |
| ... | ... | @@ -23,46 +23,55 @@ description: { |
| 23 | 23 | |
| 24 | 24 | **Required fields:** `section`, `synopsis`, `mrs`, `issues`
|
| 25 | 25 | |
| 26 | -**Optional fields:** `description`
|
|
| 26 | +**Optional fields:** `description`, `clc`
|
|
| 27 | + |
|
| 28 | +**Conditionally required**: entries with `section: base` MUST also include a `clc:`
|
|
| 29 | +field referencing the CLC proposal authorising the change.
|
|
| 27 | 30 | |
| 28 | 31 | If your MR doesn't need a changelog entry, apply the `no-changelog` label.
|
| 29 | 32 | |
| 30 | 33 | ### Fields
|
| 31 | 34 | |
| 32 | -| Field | Format | Description |
|
|
| 33 | -| ------------- | ------------------------------- | -----------------------------------------------|
|
|
| 34 | -| `synopsis` | Free-form RST | Brief description of the change |
|
|
| 35 | -| `mrs` | `!N` (space-separated) | MR number(s) |
|
|
| 36 | -| `issues` | `#N` (space-separated) | Issue number(s) |
|
|
| 37 | -| `section` | Section key (see below) | GHC component |
|
|
| 38 | -| `description` | Free-form RST in `{ ... }` | Extended details. Printed after the main entry |
|
|
| 35 | +| Field | Format | Description |
|
|
| 36 | +| ------------- | -------------------------- | ----------------------------------------------------- |
|
|
| 37 | +| `synopsis` | Free-form RST | Brief description of the change |
|
|
| 38 | +| `mrs` | `!N` (space-separated) | MR number(s) |
|
|
| 39 | +| `issues` | `#N` (space-separated) | Issue number(s) |
|
|
| 40 | +| `clc` | `#N` (space-separated) | CLC proposal number(s). Required for `section: base`. |
|
|
| 41 | +| `section` | Section key (see below) | GHC component |
|
|
| 42 | +| `description` | Free-form RST | Extended details. Printed after the main entry |
|
|
| 39 | 43 | |
| 40 | 44 | ### Section keys
|
| 41 | 45 | |
| 42 | -| Key | Heading |
|
|
| 43 | -| ------------------ | -------------------------------- |
|
|
| 44 | -| `language` | Language |
|
|
| 45 | -| `compiler` | Compiler |
|
|
| 46 | -| `profiling` | Profiling |
|
|
| 47 | -| `codegen` | Code generation |
|
|
| 48 | -| `llvm-backend` | LLVM backend |
|
|
| 49 | -| `js-backend` | JavaScript backend |
|
|
| 50 | -| `wasm-backend` | WebAssembly backend |
|
|
| 51 | -| `ghci` | GHCi |
|
|
| 52 | -| `rts` | Runtime system |
|
|
| 53 | -| `linker` | Linker |
|
|
| 54 | -| `bytecode` | Bytecode compiler |
|
|
| 55 | -| `packaging` | Packaging & build system |
|
|
| 56 | -| `cmm` | Cmm |
|
|
| 57 | -| `build-tools` | Build tools |
|
|
| 58 | -| `base` | ``base`` library |
|
|
| 59 | -| `ghc-prim` | ``ghc-prim`` library |
|
|
| 60 | -| `ghc-lib` | ``ghc`` library |
|
|
| 61 | -| `ghc-heap` | ``ghc-heap`` library |
|
|
| 62 | -| `ghc-experimental` | ``ghc-experimental`` library |
|
|
| 63 | -| `template-haskell` | ``template-haskell`` library |
|
|
| 64 | -| `ghc-pkg` | ``ghc-pkg`` |
|
|
| 65 | -| `ghc-toolchain` | ``ghc-toolchain`` |
|
|
| 46 | +The "Markdown" column indicates whether entries in that section also flow to
|
|
| 47 | +a per-library `changelog.md`. Sections without a
|
|
| 48 | +Markdown target appear only in the GHC release notes RST.
|
|
| 49 | + |
|
| 50 | +| Key | Heading | Markdown target |
|
|
| 51 | +| ------------------ | ---------------------------- | ---------------------------------------------- |
|
|
| 52 | +| `language` | Language | — |
|
|
| 53 | +| `compiler` | Compiler | — |
|
|
| 54 | +| `profiling` | Profiling | — |
|
|
| 55 | +| `codegen` | Code generation | — |
|
|
| 56 | +| `llvm-backend` | LLVM backend | — |
|
|
| 57 | +| `js-backend` | JavaScript backend | — |
|
|
| 58 | +| `wasm-backend` | WebAssembly backend | — |
|
|
| 59 | +| `ghci` | GHCi | — |
|
|
| 60 | +| `rts` | Runtime system | — |
|
|
| 61 | +| `linker` | Linker | — |
|
|
| 62 | +| `bytecode` | Bytecode compiler | — |
|
|
| 63 | +| `packaging` | Packaging & build system | — |
|
|
| 64 | +| `cmm` | Cmm | — |
|
|
| 65 | +| `build-tools` | Build tools | — |
|
|
| 66 | +| `base` | ``base`` library | `libraries/base/changelog.md` |
|
|
| 67 | +| `ghc-internal` | ``ghc-internal`` library | `libraries/ghc-internal/CHANGELOG.md` |
|
|
| 68 | +| `ghc-prim` | ``ghc-prim`` library | `libraries/ghc-prim/changelog.md` |
|
|
| 69 | +| `ghc-lib` | ``ghc`` library | — |
|
|
| 70 | +| `ghc-heap` | ``ghc-heap`` library | — |
|
|
| 71 | +| `ghc-experimental` | ``ghc-experimental`` library | `libraries/ghc-experimental/CHANGELOG.md` |
|
|
| 72 | +| `template-haskell` | ``template-haskell`` library | `libraries/template-haskell/changelog.md` |
|
|
| 73 | +| `ghc-pkg` | ``ghc-pkg`` | — |
|
|
| 74 | +| `ghc-toolchain` | ``ghc-toolchain`` | — |
|
|
| 66 | 75 | |
| 67 | 76 | ### Entry format
|
| 68 | 77 | |
| ... | ... | @@ -83,20 +92,34 @@ library's `Distribution.Fields` parser |
| 83 | 92 | ## Configuration
|
| 84 | 93 | |
| 85 | 94 | The file `changelog.d/config` declares the structure of the generated release
|
| 86 | -notes: required fields, section names, preamble text, and the included-libraries
|
|
| 87 | -table. Edit it when adding new sections or changing release note formatting.
|
|
| 95 | +notes: required fields, section names, preamble text, the included-libraries
|
|
| 96 | +table, and the `markdown-targets:` mapping that wires sections to per-library
|
|
| 97 | +`changelog.md` files. Edit it when adding new sections or changing release-note
|
|
| 98 | +formatting.
|
|
| 99 | + |
|
| 100 | +The `markdown-targets:` block is the source of truth for "which section's
|
|
| 101 | +entries get a Markdown emission, and which extra fields (e.g. `clc`) are
|
|
| 102 | +required for that section." Each line is `<section-key> <path> [<extra-required-field>...]`.
|
|
| 88 | 103 | |
| 89 | 104 | ## For maintainers
|
| 90 | 105 | |
| 91 | 106 | ### Hadrian targets
|
| 92 | 107 | |
| 93 | -Generate release notes:
|
|
| 108 | +Generate RST release notes (existing behaviour):
|
|
| 94 | 109 | ```
|
| 95 | 110 | hadrian/build changelog # uses project version
|
| 96 | 111 | hadrian/build changelog --changelog-version=10.2.1 # explicit version
|
| 97 | 112 | ```
|
| 98 | 113 | Output: `docs/users_guide/<version>-notes.rst`
|
| 99 | 114 | |
| 115 | +Generate per-library Markdown bullets:
|
|
| 116 | + |
|
| 117 | +```
|
|
| 118 | +hadrian/build libraries-changelog-markdown
|
|
| 119 | +```
|
|
| 120 | + |
|
| 121 | +Output is one stream containing every configured `markdown-targets:` section.
|
|
| 122 | + |
|
| 100 | 123 | Clear entries after branch cut:
|
| 101 | 124 | |
| 102 | 125 | ```
|
| ... | ... | @@ -108,3 +131,25 @@ Validate entries: |
| 108 | 131 | ```
|
| 109 | 132 | hadrian/build test --only=changelog-d
|
| 110 | 133 | ```
|
| 134 | + |
|
| 135 | +### RST -> Markdown rewrite rules
|
|
| 136 | + |
|
| 137 | +`--libraries-changelog-markdown` rewrites the inline RST in each entry to Markdown:
|
|
| 138 | + |
|
| 139 | +| RST | Markdown |
|
|
| 140 | +| -------------------------------------------------| ------------------------------------------------------------------------------------------------------ |
|
|
| 141 | +| ``code`` (double-backtick) | `code` (single-backtick) |
|
|
| 142 | +| `text <url>`_ | [text](url) |
|
|
| 143 | +| :ghc-ticket:`N` | [#N](https://gitlab.haskell.org/ghc/ghc/issues/N) |
|
|
| 144 | +| :ghc-mr:`N` | [!N](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/N) |
|
|
| 145 | +| :ghc-wiki:`p` | [p](https://gitlab.haskell.org/ghc/ghc/wikis/p) |
|
|
| 146 | +| :clc:`N` | [CLC proposal #N](https://github.com/haskell/core-libraries-committee/issues/N) |
|
|
| 147 | +| :ghc-flag:`-foo` | `-foo` |
|
|
| 148 | +| :extension:`E` | `E` |
|
|
| 149 | +| :ghci-cmd:`X`, :rts-flag:`X` | `X` |
|
|
| 150 | +| :base-ref:`Mod.id` `` | `Mod.id` |
|
|
| 151 | +| :th-ref:, :cabal-ref: ,:ghc-prim-ref: | `ref` |
|
|
| 152 | +| .. code-block:: lang + indented body | Triple-backtick fenced block with `lang` |
|
|
| 153 | +| .. note:: / .. warning:: | `> **Note:**` / `> **Warning:**` blockquote |
|
|
| 154 | + |
|
| 155 | + |
| 1 | +-- Minimal config for running changelog-d against the test fixture in
|
|
| 2 | +-- this directory. Mirrors the structure of the project-root
|
|
| 3 | +-- changelog.d/config but only declares the sections + markdown-targets
|
|
| 4 | +-- the fixture exercises. The path declared in markdown-targets is a
|
|
| 5 | +-- placeholder; readLibraryVersion warns and falls back to "?.?.?" when
|
|
| 6 | +-- the directory does not exist, which is captured in the golden output.
|
|
| 7 | +required-fields: synopsis mrs issues section
|
|
| 8 | + |
|
| 9 | +sections: {
|
|
| 10 | + base ``base`` library
|
|
| 11 | +}
|
|
| 12 | + |
|
| 13 | +markdown-targets: {
|
|
| 14 | + base _fake/changelog.md clc
|
|
| 15 | +} |
| 1 | +## ?.?.? *TBA*
|
|
| 2 | + |
|
| 3 | +* Self-test fixture exercising the parser/rewriter. Uses double-backtick `code`,
|
|
| 4 | + RST hyperlinks [the changelog wiki](https://gitlab.haskell.org/ghc/ghc/-/wikis/contributing/changelog),
|
|
| 5 | + GHC-flavoured roles [#12345](https://gitlab.haskell.org/ghc/ghc/issues/12345), [!6789](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/6789), [commentary/compiler](https://gitlab.haskell.org/ghc/ghc/wikis/commentary/compiler),
|
|
| 6 | + [CLC proposal #123](https://github.com/haskell/core-libraries-committee/issues/123), `-fxxx`, `TypeApplications`, `:type`,
|
|
| 7 | + `-N`, haddock cross-refs `Data.Maybe.fromMaybe`,
|
|
| 8 | + `Language.Haskell.TH.Lib`, `Distribution.Simple`,
|
|
| 9 | + `GHC.Prim`, the internal-doc role, and an :unknown-role:`pass-through`. ([#26002](https://gitlab.haskell.org/ghc/ghc/issues/26002)) ([!15830](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/15830)) ([CLC proposal #0](https://github.com/haskell/core-libraries-committee/issues/0))
|
|
| 10 | + |
|
| 11 | + This description block exercises block-level rewrites and inline rewrites
|
|
| 12 | + inside a multi-line braced field.
|
|
| 13 | + |
|
| 14 | + Inline forms inside the description: `inline code`, `DataKinds`,
|
|
| 15 | + [!15830](https://gitlab.haskell.org/ghc/ghc/-/merge_requests/15830), `Control.Applicative`, and a [bare RST link](https://example.invalid/).
|
|
| 16 | + |
|
| 17 | + > **Note:**
|
|
| 18 | + > This is an RST note admonition. It should render as a Markdown
|
|
| 19 | + > blockquote prefixed with `> **Note:**`.
|
|
| 20 | + >
|
|
| 21 | + > **Warning:**
|
|
| 22 | + > This is an RST warning admonition. It should render as a Markdown
|
|
| 23 | + > blockquote prefixed with `> **Warning:**`.
|
|
| 24 | + >
|
|
| 25 | + ```haskell
|
|
| 26 | + foo :: Int -> Int
|
|
| 27 | + foo x = x + 1
|
|
| 28 | + bar :: String
|
|
| 29 | + bar = "hello"
|
|
| 30 | + |
|
| 31 | + ```
|
|
| 32 | + After the code block, plain prose continues. Verify that the renderer
|
|
| 33 | + exits the fenced block correctly and resumes paragraph flow here. |
| 1 | +-- This file exercises every construct supported by changelog-d's parser
|
|
| 2 | +-- and RST -> Markdown rewriter. It is kept in tree as a regression
|
|
| 3 | +-- fixture: when the parser or rewriter is touched, run
|
|
| 4 | +-- cabal run changelog-d -- --validate changelog.d/
|
|
| 5 | +-- cabal run changelog-d -- --libraries-changelog-markdown changelog.d/
|
|
| 6 | +-- and visually compare the output. The tool treats this like any
|
|
| 7 | +-- other fragment, so it WILL appear in `--version`'ed RST and in
|
|
| 8 | +-- `--libraries-changelog-markdown` output. Delete it before cutting a
|
|
| 9 | +-- release, or move it under utils/changelog-d/tests/ if/when that
|
|
| 10 | +-- directory is wired up.
|
|
| 11 | +section: base
|
|
| 12 | +synopsis: Self-test fixture exercising the parser/rewriter. Uses double-backtick ``code``,
|
|
| 13 | + RST hyperlinks `the changelog wiki <https://gitlab.haskell.org/ghc/ghc/-/wikis/contributing/changelog>`_,
|
|
| 14 | + GHC-flavoured roles :ghc-ticket:`12345`, :ghc-mr:`6789`, :ghc-wiki:`commentary/compiler`,
|
|
| 15 | + :clc:`123`, :ghc-flag:`-fxxx`, :extension:`TypeApplications`, :ghci-cmd:`:type`,
|
|
| 16 | + :rts-flag:`-N`, haddock cross-refs :base-ref:`Data.Maybe.fromMaybe`,
|
|
| 17 | + :th-ref:`Language.Haskell.TH.Lib`, :cabal-ref:`Distribution.Simple`,
|
|
| 18 | + :ghc-prim-ref:`GHC.Prim`, the :doc:`internal-doc` role, and an :unknown-role:`pass-through`.
|
|
| 19 | +issues: #26002
|
|
| 20 | +mrs: !15830
|
|
| 21 | +clc: #0
|
|
| 22 | + |
|
| 23 | +description: {
|
|
| 24 | + This description block exercises block-level rewrites and inline rewrites
|
|
| 25 | + inside a multi-line braced field.
|
|
| 26 | + |
|
| 27 | + Inline forms inside the description: ``inline code``, :extension:`DataKinds`,
|
|
| 28 | + :ghc-mr:`15830`, :base-ref:`Control.Applicative`, and a `bare RST link
|
|
| 29 | + <https://example.invalid/>`_.
|
|
| 30 | + |
|
| 31 | + .. note::
|
|
| 32 | + This is an RST note admonition. It should render as a Markdown
|
|
| 33 | + blockquote prefixed with ``> **Note:**``.
|
|
| 34 | + |
|
| 35 | + .. warning::
|
|
| 36 | + This is an RST warning admonition. It should render as a Markdown
|
|
| 37 | + blockquote prefixed with ``> **Warning:**``.
|
|
| 38 | + |
|
| 39 | + .. code-block:: haskell
|
|
| 40 | + |
|
| 41 | + foo :: Int -> Int
|
|
| 42 | + foo x = x + 1
|
|
| 43 | + bar :: String
|
|
| 44 | + bar = "hello"
|
|
| 45 | + |
|
| 46 | + After the code block, plain prose continues. Verify that the renderer
|
|
| 47 | + exits the fenced block correctly and resumes paragraph flow here.
|
|
| 48 | +} |