Dave Rodgman | 8ae9257 | 2020-08-25 14:33:15 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | """This script generates the MbedTLS release notes in markdown format. |
| 3 | |
| 4 | It does this by calling assemble_changelog.py to generate the bulk of |
| 5 | content, and also inserting other content such as a brief description, |
| 6 | hashes for the tar and zip files containing the release, etc. |
| 7 | |
| 8 | Returns 0 on success, 1 on failure. |
| 9 | |
| 10 | Note: must be run from Mbed TLS root.""" |
| 11 | |
| 12 | # Copyright (c) 2020, Arm Limited, All Rights Reserved |
| 13 | # SPDX-License-Identifier: Apache-2.0 |
| 14 | # |
| 15 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 16 | # not use this file except in compliance with the License. |
| 17 | # You may obtain a copy of the License at |
| 18 | # |
| 19 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 20 | # |
| 21 | # Unless required by applicable law or agreed to in writing, software |
| 22 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 23 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 24 | # See the License for the specific language governing permissions and |
| 25 | # limitations under the License. |
| 26 | # |
| 27 | # This file is part of Mbed TLS (https://tls.mbed.org) |
| 28 | |
| 29 | import re |
| 30 | import sys |
| 31 | import os.path |
| 32 | import hashlib |
| 33 | import argparse |
| 34 | import tempfile |
| 35 | import subprocess |
| 36 | |
| 37 | TEMPLATE = """## Description |
| 38 | |
| 39 | These are the release notes for MbedTLS version {version}. |
| 40 | |
| 41 | {description} |
| 42 | |
| 43 | {changelog} |
| 44 | |
| 45 | ## Who should update |
| 46 | |
| 47 | {whoshouldupdate} |
| 48 | |
| 49 | ## Checksum |
| 50 | |
| 51 | The SHA256 hashes for the archives are: |
| 52 | |
| 53 | ``` |
| 54 | {tarhash} mbedtls-{version}.tar.gz |
| 55 | {ziphash} mbedtls-{version}.zip |
| 56 | ``` |
| 57 | """ |
| 58 | |
| 59 | WHO_SHOULD_UPDATE_DEFAULT = 'We recommend all affected users should \ |
| 60 | update to take advantage of the bug fixes contained in this release at \ |
| 61 | an appropriate point in their development lifecycle.' |
| 62 | |
| 63 | |
| 64 | CHECKLIST = '''Please review the release notes to ensure that all of the \ |
| 65 | following are documented (if needed): |
| 66 | - Missing functionality |
| 67 | - Changes in functionality |
| 68 | - Known issues |
| 69 | ''' |
| 70 | |
| 71 | |
| 72 | CUSTOM_WORDS = 'Hellman API APIs gz lifecycle Bugfix CMake inlined Crypto endian SHA xxx' |
| 73 | |
| 74 | |
| 75 | def sha256_digest(filename): |
| 76 | """Read given file and return a SHA256 digest""" |
| 77 | h = hashlib.sha256() |
| 78 | with open(filename, 'rb') as f: |
| 79 | h.update(f.read()) |
| 80 | return h.hexdigest() |
| 81 | |
| 82 | |
| 83 | def error(text): |
| 84 | """Display error message and exit""" |
| 85 | print(f'ERROR: {text}') |
| 86 | sys.exit(1) |
| 87 | |
| 88 | |
| 89 | def warn(text): |
| 90 | """Display warning message""" |
| 91 | print(f'WARNING: {text}') |
| 92 | |
| 93 | |
| 94 | def generate_content(args): |
| 95 | """Return template populated with given content""" |
| 96 | for field in ('version', 'tarhash', 'ziphash', 'changelog', |
| 97 | 'description', 'whoshouldupdate'): |
| 98 | if not field in args: |
| 99 | error(f'{field} not specified') |
| 100 | return TEMPLATE.format(**args) |
| 101 | |
| 102 | |
| 103 | def run_cmd(cmd, capture=True): |
| 104 | """Run given command in a shell and return the command output""" |
| 105 | # Note: [:-1] strips the trailing newline introduced by the shell. |
| 106 | if capture: |
| 107 | return subprocess.check_output(cmd, shell=True, input=None, |
| 108 | universal_newlines=True)[:-1] |
| 109 | else: |
| 110 | subprocess.call(cmd, shell=True) |
| 111 | |
| 112 | |
| 113 | def parse_args(args): |
| 114 | """Parse command line arguments and return cleaned up args""" |
| 115 | parser = argparse.ArgumentParser(description=__doc__) |
| 116 | parser.add_argument('-o', '--output', default='ReleaseNotes.md', |
| 117 | help='Output file (defaults to ReleaseNotes.md)') |
| 118 | parser.add_argument('-t', '--tar', action='store', |
| 119 | help='Optional tar containing release (to generate hash)') |
| 120 | parser.add_argument('-z', '--zip', action='store', |
| 121 | help='Optional zip containing release (to generate hash)') |
| 122 | parser.add_argument('-d', '--description', action='store', required=True, |
| 123 | help='Short description of release (or name of file containing this)') |
| 124 | parser.add_argument('-w', '--who', action='store', default=WHO_SHOULD_UPDATE_DEFAULT, |
| 125 | help='Optional short description of who should \ |
| 126 | update (or name of file containing this)') |
| 127 | args = parser.parse_args(args) |
| 128 | |
| 129 | # If these exist as files, interpret as files containing |
| 130 | # desired content rather than literal content. |
| 131 | for field in ('description', 'who'): |
| 132 | if os.path.exists(getattr(args, field)): |
| 133 | with open(getattr(args, field), 'r') as f: |
| 134 | setattr(args, field, f.read()) |
| 135 | |
| 136 | return args |
| 137 | |
| 138 | |
| 139 | def spellcheck(text): |
| 140 | with tempfile.NamedTemporaryFile() as temp_file: |
| 141 | with open(temp_file.name, 'w') as f: |
| 142 | f.write(text) |
| 143 | result = run_cmd(f'ispell -d american -w _- -a < {temp_file.name}') |
| 144 | input_lines = text.splitlines() |
| 145 | ispell_re = re.compile(r'& (\S+) \d+ \d+:.*') |
| 146 | bad_words = set() |
| 147 | bad_lines = set() |
| 148 | line_no = 1 |
| 149 | for l in result.splitlines(): |
| 150 | if l.strip() == '': |
| 151 | line_no += 1 |
| 152 | elif l.startswith('&'): |
| 153 | m = ispell_re.fullmatch(l) |
| 154 | word = m.group(1) |
| 155 | if word.isupper(): |
| 156 | # ignore all-uppercase words |
| 157 | pass |
| 158 | elif "_" in word: |
| 159 | # part of a non-English 'word' like PSA_CRYPTO_ECC |
| 160 | pass |
| 161 | elif word.startswith('-'): |
| 162 | # ignore flags |
| 163 | pass |
| 164 | elif word in CUSTOM_WORDS: |
| 165 | # accept known-good words |
| 166 | pass |
| 167 | else: |
| 168 | bad_words.add(word) |
| 169 | bad_lines.add(line_no) |
| 170 | if bad_words: |
| 171 | bad_lines = '\n'.join(' ' + input_lines[n] for n in sorted(bad_lines)) |
| 172 | bad_words = ', '.join(bad_words) |
| 173 | warn('Release notes contain the following mis-spelled ' \ |
| 174 | f'words: {bad_words}:\n{bad_lines}\n') |
| 175 | |
| 176 | |
| 177 | def gen_rel_notes(args): |
| 178 | """Return release note content from given command line args""" |
| 179 | # Get version by parsing version.h. Assumption is that bump_version |
| 180 | # has been run and this contains the correct version number. |
| 181 | version = run_cmd('cat include/mbedtls/version.h | \ |
| 182 | clang -Iinclude -dM -E - | grep "MBEDTLS_VERSION_STRING "') |
| 183 | version = version.split()[-1][1:-1] |
| 184 | |
| 185 | # Get main changelog content. |
| 186 | assemble_path = os.path.join(os.getcwd(), 'scripts', 'assemble_changelog.py') |
| 187 | with tempfile.NamedTemporaryFile() as temp_file: |
| 188 | run_cmd(f'{assemble_path} -o {temp_file.name} --latest-only') |
| 189 | with open(temp_file.name) as f: |
| 190 | changelog = f.read() |
| 191 | |
| 192 | arg_hash = { |
| 193 | 'version': version, |
| 194 | 'tarhash': '', |
| 195 | 'ziphash': '', |
| 196 | 'changelog': changelog.strip(), |
| 197 | 'description': args.description.strip(), |
| 198 | 'whoshouldupdate': args.who.strip() |
| 199 | } |
| 200 | |
| 201 | spellcheck(generate_content(arg_hash)) |
| 202 | |
| 203 | arg_hash['tarhash'] = sha256_digest(args.tar) if args.tar else "x" * 64 |
| 204 | arg_hash['ziphash'] = sha256_digest(args.zip) if args.zip else "x" * 64 |
| 205 | return generate_content(arg_hash) |
| 206 | |
| 207 | |
| 208 | def main(): |
| 209 | # Very basic check to see if we are in the root. |
| 210 | path = os.path.join(os.getcwd(), 'scripts', 'generate_release_notes.py') |
| 211 | if not os.path.exists(path): |
| 212 | error(f'{sys.argv[0]} must be run from the mbedtls root') |
| 213 | |
| 214 | args = parse_args(sys.argv[1:]) |
| 215 | |
| 216 | content = gen_rel_notes(args) |
| 217 | with open(args.output, 'w') as f: |
| 218 | f.write(content) |
| 219 | |
| 220 | print(CHECKLIST) |
| 221 | |
| 222 | |
| 223 | if __name__ == '__main__': |
| 224 | main() |