Mercurial > minori
comparison dep/fmt/support/manage.py @ 343:1faa72660932
*: transfer back to cmake from autotools
autotools just made lots of things more complicated than
they should have and many things broke (i.e. translations)
author | Paper <paper@paper.us.eu.org> |
---|---|
date | Thu, 20 Jun 2024 05:56:06 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
342:adb79bdde329 | 343:1faa72660932 |
---|---|
1 #!/usr/bin/env python3 | |
2 | |
3 """Manage site and releases. | |
4 | |
5 Usage: | |
6 manage.py release [<branch>] | |
7 manage.py site | |
8 | |
9 For the release command $FMT_TOKEN should contain a GitHub personal access token | |
10 obtained from https://github.com/settings/tokens. | |
11 """ | |
12 | |
13 from __future__ import print_function | |
14 import datetime, docopt, errno, fileinput, json, os | |
15 import re, requests, shutil, sys | |
16 from contextlib import contextmanager | |
17 from distutils.version import LooseVersion | |
18 from subprocess import check_call | |
19 | |
20 | |
21 class Git: | |
22 def __init__(self, dir): | |
23 self.dir = dir | |
24 | |
25 def call(self, method, args, **kwargs): | |
26 return check_call(['git', method] + list(args), **kwargs) | |
27 | |
28 def add(self, *args): | |
29 return self.call('add', args, cwd=self.dir) | |
30 | |
31 def checkout(self, *args): | |
32 return self.call('checkout', args, cwd=self.dir) | |
33 | |
34 def clean(self, *args): | |
35 return self.call('clean', args, cwd=self.dir) | |
36 | |
37 def clone(self, *args): | |
38 return self.call('clone', list(args) + [self.dir]) | |
39 | |
40 def commit(self, *args): | |
41 return self.call('commit', args, cwd=self.dir) | |
42 | |
43 def pull(self, *args): | |
44 return self.call('pull', args, cwd=self.dir) | |
45 | |
46 def push(self, *args): | |
47 return self.call('push', args, cwd=self.dir) | |
48 | |
49 def reset(self, *args): | |
50 return self.call('reset', args, cwd=self.dir) | |
51 | |
52 def update(self, *args): | |
53 clone = not os.path.exists(self.dir) | |
54 if clone: | |
55 self.clone(*args) | |
56 return clone | |
57 | |
58 | |
59 def clean_checkout(repo, branch): | |
60 repo.clean('-f', '-d') | |
61 repo.reset('--hard') | |
62 repo.checkout(branch) | |
63 | |
64 | |
65 class Runner: | |
66 def __init__(self, cwd): | |
67 self.cwd = cwd | |
68 | |
69 def __call__(self, *args, **kwargs): | |
70 kwargs['cwd'] = kwargs.get('cwd', self.cwd) | |
71 check_call(args, **kwargs) | |
72 | |
73 | |
74 def create_build_env(): | |
75 """Create a build environment.""" | |
76 class Env: | |
77 pass | |
78 env = Env() | |
79 | |
80 # Import the documentation build module. | |
81 env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
82 sys.path.insert(0, os.path.join(env.fmt_dir, 'doc')) | |
83 import build | |
84 | |
85 env.build_dir = 'build' | |
86 env.versions = build.versions | |
87 | |
88 # Virtualenv and repos are cached to speed up builds. | |
89 build.create_build_env(os.path.join(env.build_dir, 'virtualenv')) | |
90 | |
91 env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt')) | |
92 return env | |
93 | |
94 | |
95 @contextmanager | |
96 def rewrite(filename): | |
97 class Buffer: | |
98 pass | |
99 buffer = Buffer() | |
100 if not os.path.exists(filename): | |
101 buffer.data = '' | |
102 yield buffer | |
103 return | |
104 with open(filename) as f: | |
105 buffer.data = f.read() | |
106 yield buffer | |
107 with open(filename, 'w') as f: | |
108 f.write(buffer.data) | |
109 | |
110 | |
111 fmt_repo_url = 'git@github.com:fmtlib/fmt' | |
112 | |
113 | |
114 def update_site(env): | |
115 env.fmt_repo.update(fmt_repo_url) | |
116 | |
117 doc_repo = Git(os.path.join(env.build_dir, 'fmtlib.github.io')) | |
118 doc_repo.update('git@github.com:fmtlib/fmtlib.github.io') | |
119 | |
120 for version in env.versions: | |
121 clean_checkout(env.fmt_repo, version) | |
122 target_doc_dir = os.path.join(env.fmt_repo.dir, 'doc') | |
123 # Remove the old theme. | |
124 for entry in os.listdir(target_doc_dir): | |
125 path = os.path.join(target_doc_dir, entry) | |
126 if os.path.isdir(path): | |
127 shutil.rmtree(path) | |
128 # Copy the new theme. | |
129 for entry in ['_static', '_templates', 'basic-bootstrap', 'bootstrap', | |
130 'conf.py', 'fmt.less']: | |
131 src = os.path.join(env.fmt_dir, 'doc', entry) | |
132 dst = os.path.join(target_doc_dir, entry) | |
133 copy = shutil.copytree if os.path.isdir(src) else shutil.copyfile | |
134 copy(src, dst) | |
135 # Rename index to contents. | |
136 contents = os.path.join(target_doc_dir, 'contents.rst') | |
137 if not os.path.exists(contents): | |
138 os.rename(os.path.join(target_doc_dir, 'index.rst'), contents) | |
139 # Fix issues in reference.rst/api.rst. | |
140 for filename in ['reference.rst', 'api.rst', 'index.rst']: | |
141 pattern = re.compile('doxygenfunction.. (bin|oct|hexu|hex)$', re.M) | |
142 with rewrite(os.path.join(target_doc_dir, filename)) as b: | |
143 b.data = b.data.replace('std::ostream &', 'std::ostream&') | |
144 b.data = re.sub(pattern, r'doxygenfunction:: \1(int)', b.data) | |
145 b.data = b.data.replace('std::FILE*', 'std::FILE *') | |
146 b.data = b.data.replace('unsigned int', 'unsigned') | |
147 #b.data = b.data.replace('operator""_', 'operator"" _') | |
148 b.data = b.data.replace( | |
149 'format_to_n(OutputIt, size_t, string_view, Args&&', | |
150 'format_to_n(OutputIt, size_t, const S&, const Args&') | |
151 b.data = b.data.replace( | |
152 'format_to_n(OutputIt, std::size_t, string_view, Args&&', | |
153 'format_to_n(OutputIt, std::size_t, const S&, const Args&') | |
154 if version == ('3.0.2'): | |
155 b.data = b.data.replace( | |
156 'fprintf(std::ostream&', 'fprintf(std::ostream &') | |
157 if version == ('5.3.0'): | |
158 b.data = b.data.replace( | |
159 'format_to(OutputIt, const S&, const Args&...)', | |
160 'format_to(OutputIt, const S &, const Args &...)') | |
161 if version.startswith('5.') or version.startswith('6.'): | |
162 b.data = b.data.replace(', size_t', ', std::size_t') | |
163 if version.startswith('7.'): | |
164 b.data = b.data.replace(', std::size_t', ', size_t') | |
165 b.data = b.data.replace('join(It, It', 'join(It, Sentinel') | |
166 if version.startswith('7.1.'): | |
167 b.data = b.data.replace(', std::size_t', ', size_t') | |
168 b.data = b.data.replace('join(It, It', 'join(It, Sentinel') | |
169 b.data = b.data.replace( | |
170 'fmt::format_to(OutputIt, const S&, Args&&...)', | |
171 'fmt::format_to(OutputIt, const S&, Args&&...) -> ' + | |
172 'typename std::enable_if<enable, OutputIt>::type') | |
173 b.data = b.data.replace('aa long', 'a long') | |
174 b.data = b.data.replace('serveral', 'several') | |
175 if version.startswith('6.2.'): | |
176 b.data = b.data.replace( | |
177 'vformat(const S&, basic_format_args<' + | |
178 'buffer_context<Char>>)', | |
179 'vformat(const S&, basic_format_args<' + | |
180 'buffer_context<type_identity_t<Char>>>)') | |
181 # Fix a broken link in index.rst. | |
182 index = os.path.join(target_doc_dir, 'index.rst') | |
183 with rewrite(index) as b: | |
184 b.data = b.data.replace( | |
185 'doc/latest/index.html#format-string-syntax', 'syntax.html') | |
186 # Fix issues in syntax.rst. | |
187 index = os.path.join(target_doc_dir, 'syntax.rst') | |
188 with rewrite(index) as b: | |
189 b.data = b.data.replace( | |
190 '..productionlist:: sf\n', '.. productionlist:: sf\n ') | |
191 b.data = b.data.replace('Examples:\n', 'Examples::\n') | |
192 # Build the docs. | |
193 html_dir = os.path.join(env.build_dir, 'html') | |
194 if os.path.exists(html_dir): | |
195 shutil.rmtree(html_dir) | |
196 include_dir = env.fmt_repo.dir | |
197 if LooseVersion(version) >= LooseVersion('5.0.0'): | |
198 include_dir = os.path.join(include_dir, 'include', 'fmt') | |
199 elif LooseVersion(version) >= LooseVersion('3.0.0'): | |
200 include_dir = os.path.join(include_dir, 'fmt') | |
201 import build | |
202 build.build_docs(version, doc_dir=target_doc_dir, | |
203 include_dir=include_dir, work_dir=env.build_dir) | |
204 shutil.rmtree(os.path.join(html_dir, '.doctrees')) | |
205 # Create symlinks for older versions. | |
206 for link, target in {'index': 'contents', 'api': 'reference'}.items(): | |
207 link = os.path.join(html_dir, link) + '.html' | |
208 target += '.html' | |
209 if os.path.exists(os.path.join(html_dir, target)) and \ | |
210 not os.path.exists(link): | |
211 os.symlink(target, link) | |
212 # Copy docs to the website. | |
213 version_doc_dir = os.path.join(doc_repo.dir, version) | |
214 try: | |
215 shutil.rmtree(version_doc_dir) | |
216 except OSError as e: | |
217 if e.errno != errno.ENOENT: | |
218 raise | |
219 shutil.move(html_dir, version_doc_dir) | |
220 | |
221 | |
222 def release(args): | |
223 env = create_build_env() | |
224 fmt_repo = env.fmt_repo | |
225 | |
226 branch = args.get('<branch>') | |
227 if branch is None: | |
228 branch = 'master' | |
229 if not fmt_repo.update('-b', branch, fmt_repo_url): | |
230 clean_checkout(fmt_repo, branch) | |
231 | |
232 # Update the date in the changelog and extract the version and the first | |
233 # section content. | |
234 changelog = 'ChangeLog.md' | |
235 changelog_path = os.path.join(fmt_repo.dir, changelog) | |
236 is_first_section = True | |
237 first_section = [] | |
238 for i, line in enumerate(fileinput.input(changelog_path, inplace=True)): | |
239 if i == 0: | |
240 version = re.match(r'# (.*) - TBD', line).group(1) | |
241 line = '# {} - {}\n'.format( | |
242 version, datetime.date.today().isoformat()) | |
243 elif not is_first_section: | |
244 pass | |
245 elif line.startswith('#'): | |
246 is_first_section = False | |
247 else: | |
248 first_section.append(line) | |
249 sys.stdout.write(line) | |
250 if first_section[0] == '\n': | |
251 first_section.pop(0) | |
252 | |
253 changes = '' | |
254 code_block = False | |
255 stripped = False | |
256 for line in first_section: | |
257 if re.match(r'^\s*```', line): | |
258 code_block = not code_block | |
259 changes += line | |
260 stripped = False | |
261 continue | |
262 if code_block: | |
263 changes += line | |
264 continue | |
265 if line == '\n': | |
266 changes += line | |
267 if stripped: | |
268 changes += line | |
269 stripped = False | |
270 continue | |
271 if stripped: | |
272 line = ' ' + line.lstrip() | |
273 changes += line.rstrip() | |
274 stripped = True | |
275 | |
276 cmakelists = 'CMakeLists.txt' | |
277 for line in fileinput.input(os.path.join(fmt_repo.dir, cmakelists), | |
278 inplace=True): | |
279 prefix = 'set(FMT_VERSION ' | |
280 if line.startswith(prefix): | |
281 line = prefix + version + ')\n' | |
282 sys.stdout.write(line) | |
283 | |
284 # Add the version to the build script. | |
285 script = os.path.join('doc', 'build.py') | |
286 script_path = os.path.join(fmt_repo.dir, script) | |
287 for line in fileinput.input(script_path, inplace=True): | |
288 m = re.match(r'( *versions \+= )\[(.+)\]', line) | |
289 if m: | |
290 line = '{}[{}, \'{}\']\n'.format(m.group(1), m.group(2), version) | |
291 sys.stdout.write(line) | |
292 | |
293 fmt_repo.checkout('-B', 'release') | |
294 fmt_repo.add(changelog, cmakelists, script) | |
295 fmt_repo.commit('-m', 'Update version') | |
296 | |
297 # Build the docs and package. | |
298 run = Runner(fmt_repo.dir) | |
299 run('cmake', '.') | |
300 run('make', 'doc', 'package_source') | |
301 update_site(env) | |
302 | |
303 # Create a release on GitHub. | |
304 fmt_repo.push('origin', 'release') | |
305 auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')} | |
306 r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', | |
307 headers=auth_headers, | |
308 data=json.dumps({'tag_name': version, | |
309 'target_commitish': 'release', | |
310 'body': changes, 'draft': True})) | |
311 if r.status_code != 201: | |
312 raise Exception('Failed to create a release ' + str(r)) | |
313 id = r.json()['id'] | |
314 uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases' | |
315 package = 'fmt-{}.zip'.format(version) | |
316 r = requests.post( | |
317 '{}/{}/assets?name={}'.format(uploads_url, id, package), | |
318 headers={'Content-Type': 'application/zip'} | auth_headers, | |
319 data=open('build/fmt/' + package, 'rb')) | |
320 if r.status_code != 201: | |
321 raise Exception('Failed to upload an asset ' + str(r)) | |
322 | |
323 | |
324 if __name__ == '__main__': | |
325 args = docopt.docopt(__doc__) | |
326 if args.get('release'): | |
327 release(args) | |
328 elif args.get('site'): | |
329 update_site(create_build_env()) |