Zubin pushed to branch wip/27183 at Glasgow Haskell Compiler / GHC

Commits:

12 changed files:

Changes:

  • .gitlab-ci.yml
    ... ... @@ -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
     
    

  • .gitlab/merge_request_templates/Default.md
    ... ... @@ -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.
    

  • changelog.d/config
    ... ... @@ -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
    

  • docs/users_guide/ghc_config.py.in
    ... ... @@ -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'
    

  • hadrian/src/Rules/Changelog.hs
    ... ... @@ -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
    +        ]

  • libraries/integer-gmp/integer-gmp.cabal
    ... ... @@ -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
    

  • testsuite/tests/linters/Makefile
    ... ... @@ -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
    

  • utils/changelog-d/ChangelogD.hs
    ... ... @@ -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
     
    

  • utils/changelog-d/README.md
    ... ... @@ -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
    +

  • utils/changelog-d/tests/config
    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
    +}

  • utils/changelog-d/tests/expected/test-parser-rewriter.md
    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.

  • utils/changelog-d/tests/test-parser-rewriter
    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
    +}