python/generate.py (305 lines of code) (raw):

# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # import re import urllib import yaml from pprint import pformat from pygments import highlight as pygments_highlight from pygments.lexers import get_lexer_by_name from pygments.formatters import HtmlFormatter from xml.sax.saxutils import escape as escape_html from plano import * ## General ## def setup_release_script(): if len(ARGS) not in (5, 6): exit("Usage: script RELEASE ISSUES_RELEASE SOURCE_RELEASE RELEASE-DIR [CHECKOUT-DIR]") notice("Setting up script '{}'", ARGS[0]) release = ARGS[1] issues_release = ARGS[2] source_release = ARGS[3] release_dir = ARGS[4] try: checkout_dir = ARGS[5] except IndexError: checkout_dir = None assert release != "" assert issues_release != "" assert source_release != "" assert release_dir != "" assert checkout_dir is None or checkout_dir != "" call_and_print_on_error("asciidoc --version") call_and_print_on_error("asciidoctor --version") call_and_print_on_error("cmake --version") call_and_print_on_error("dot -V") call_and_print_on_error("doxygen --version") #call_and_print_on_error("epydoc --version") #call_and_print_on_error("fop -version") call_and_print_on_error("gcc --version") call_and_print_on_error("make --version") call_and_print_on_error("rdoc --version") call_and_print_on_error("which sphinx-build") call_and_print_on_error("svn --version") call_and_print_on_error("which javadoc") call_and_print_on_error("xsltproc --version") notice("Release script inputs:") notice(" release: {}", pformat(release)) notice(" issues_release: {}", pformat(issues_release)) notice(" source_release: {}", pformat(source_release)) notice(" release_dir: {}", pformat(release_dir)) notice(" checkout_dir: {}", pformat(checkout_dir)) make_dir(release_dir) return (release, issues_release, source_release, release_dir, checkout_dir) def get_svn_release_url(module, release): project_url = "http://svn.apache.org/repos/asf/qpid" release_path_prefix = "tags/" if release.startswith("branches/"): release_path_prefix = "" if module == "main": path = "{}{}/qpid".format(release_path_prefix, release) if release == "trunk": path = "trunk/qpid" return "{}/{}".format(project_url, path) elif module == "cpp": path = "{}qpid-cpp-{}-rc/qpid".format(release_path_prefix, release) if release == "trunk": path = "trunk/qpid" return "{}/{}".format(project_url, path) path = "{}/{}{}".format(module, release_path_prefix, release) if release == "trunk": path = "{}/trunk".format(module, release) return "{}/{}".format(project_url, path) def get_git_release_branch_url(module, release, path=""): """ Get a URL to file path in GitHub If called without PATH it returns a URL that you can append paths to. """ modules = { "cpp": "https://github.com/apache/qpid-cpp/tree", "proton": "https://github.com/apache/qpid-proton/tree", } if release == "trunk": release = "main" elif release == "master": release = "main" return "{}/{}/{}".format(modules[module], release, path.lstrip("/")) def fetch_source(module, release): work_dir = make_temp_dir() user_temp_dir = make_user_temp_dir() dir_name = "qpid-{}-{}".format(module, release) source_dir = join(user_temp_dir, "transom", dir_name) url = "http://gitbox.apache.org/repos/asf/qpid-{}.git".format(module) if is_dir(source_dir): debug("Source already exists") return source_dir make_dir(source_dir) with working_dir(work_dir): call("git clone --bare --depth 20 --branch '{}' '{}' .", release, url) call("git archive '{}' | tar -xf - -C '{}'", release, source_dir, shell=True) return source_dir ## API reference ## _doxygen_conf_template = u""" DISABLE_INDEX = yes EXTRACT_ALL = no EXAMPLE_PATH = {example_paths} GENERATE_HTML = yes GENERATE_LATEX = no GENERATE_TREEVIEW = yes HAVE_DOT = no PROJECT_NUMBER = {release} QUIET = yes RECURSIVE = yes INPUT = {input_paths} PROJECT_NAME = "{title}" STRIP_FROM_PATH = {strip_paths} HTML_OUTPUT = {output_dir} """ def gen_doxygen(release, title, input_paths, strip_paths, output_dir, example_paths=[], config_file=None): input_paths = " ".join(input_paths) strip_paths = " ".join(strip_paths) example_paths = " ".join(example_paths) make_dir(output_dir) source_conf = "" local_conf = _doxygen_conf_template.format(**locals()) if config_file is not None: source_conf = read(config_file) path = make_temp_file() write(path, source_conf) append(path, local_conf) call("doxygen {}", path) touch(join(output_dir, "_transom_ignore_pages")) def gen_epydoc(release, title, input_paths, input_namespaces, output_dir): input_paths = ":".join(input_paths) input_namespaces = " ".join(input_namespaces) make_dir(output_dir) options = list() options.append("--graph all") options.append("--html") options.append("--name \"{}\"".format(title)) options.append("--no-private") options.append("--output {}".format(output_dir)) options.append("--quiet") options.append("--url \"http://qpid.apache.org/index.html\"") options = " ".join(options) env = ENV env["PYTHONPATH"] = input_paths call("epydoc {} {}", options, input_namespaces, env=env) touch(join(output_dir, "_transom_ignore_pages")) def gen_javadoc(release, title, input_paths, input_namespaces, output_dir): input_paths = ":".join(input_paths) input_namespaces = ":".join(input_namespaces) make_dir(output_dir) options = list() options.append("-windowtitle \"{}\"".format(title)) options.append("-doctitle \"{}\"".format(title)) options.append("-sourcepath {}".format(input_paths)) options.append("-subpackages {}".format(input_namespaces)) options.append("-d {}".format(output_dir)) options.append("-notimestamp") options.append("-noindex") options = " ".join(options) call("javadoc {}", options) touch(join(output_dir, "_transom_ignore_pages")) def gen_rdoc(release, title, base_input_path, input_paths, output_dir): output_dir = absolute_path(output_dir) # rdoc really wants to make the last dir make_dir(parent_dir(output_dir)) options = list() options.append("--fmt html") options.append("--op {}".format(output_dir)) options.append("--quiet") options.append("--title \"{}\"".format(title)) options = " ".join(options) input_paths = " ".join(input_paths) with working_dir(base_input_path): call("rdoc {} {}", options, input_paths) touch(join(output_dir, "_transom_ignore_pages")) def gen_yard(release, title, input_dir, input_namespaces, output_dir): output_dir = absolute_path(output_dir) options = list() options.append("--quiet") options.append("--no-progress") options.append("--title \"{}\"".format(title)) options.append("--output-dir {}".format(output_dir)) for namespace in input_namespaces: options.append("--api {}".format(namespace)) options = " ".join(options) with working_dir(input_dir): call("yard {}", options) # Workaround an annoying yard behavior - https://github.com/lsegal/yard/issues/837 #move(join(output_dir, "_index.html"), join(output_dir, "index.html")) touch(join(output_dir, "_transom_ignore_pages")) ## Examples ## def gen_examples(release, title, lang, input_dir, input_names, output_dir, readme_url=None, source_url=None): remove(output_dir) notice("Generating {}", title) for name in input_names: gen_example_page(release=release, input_dir=input_dir, input_name=name, output_dir=output_dir, lang=lang) gen_examples_index(release=release, input_names=input_names, output_dir=output_dir, title=title, readme_url=readme_url, source_url=source_url) _example_page_template = u""" <h1>{title}</h1> {content} <p><a href="{input_name}">Download this file</a></p> """ def gen_example_page(release, input_dir, input_name, output_dir, lang): input_path = join(input_dir, input_name) output_path = join(output_dir, input_name) html_output_path = join(output_dir, "{}.html.in".format(input_name)) content = read(input_path) content = strip_license_header(content, lang) content = content.strip() content = highlight(content, lang) title = input_name html = _example_page_template.format(**locals()) copy(input_path, output_path) write(html_output_path, html) _formatter = HtmlFormatter(linenos=False) def highlight(string, lang): lexer = get_lexer_by_name(lang) return pygments_highlight(string, lexer, _formatter) _license_header_regexes = { "c": re.compile(r"/\*.*?\*/", re.DOTALL), "cpp": re.compile(r"/\*.*?\*/", re.DOTALL), "csharp": re.compile(r"/\*.*?\*/", re.DOTALL), "java": re.compile(r"/\*.*?\*/", re.DOTALL), "php": re.compile(r"/\*.*?\*/", re.DOTALL), } def strip_license_header(string, lang): if lang in _license_header_regexes: regex = _license_header_regexes[lang] return re.sub(regex, "", string) elif lang in ("perl", "python", "ruby", "ini"): input_lines = string.split(LINE_SEP) output_lines = list() for i, line in enumerate(input_lines): if not line.startswith("#") and line != "": output_lines.extend(input_lines[i:]) break return LINE_SEP.join(output_lines) else: return string _examples_index_template = """ # {title} ## Example files {example_links} ## More information {info_links} """ def gen_examples_index(release, input_names, output_dir, title, readme_url=None, source_url=None): output_path = join(output_dir, "index.md") example_links = list() info_links = list() for name in input_names: example_links.append(" - [{}]({}.html)".format(name, name)) if readme_url: info_links.append(" - [README]({})".format(readme_url)) if source_url: info_links.append(" - [Source location]({})".format(source_url)) example_links = LINE_SEP.join(example_links) info_links = LINE_SEP.join(info_links) index = _examples_index_template.format(**locals()) write(output_path, index) ## Release notes ## def render_release_notes(project, release): issues = _fetch_issues(project, release) lines = list() improvements = _render_issues(issues, "New Feature", "Improvement") bugs = _render_issues(issues, "Bug") tasks = _render_issues(issues, "Task") if improvements is not None: lines.append("\n## New features and improvements\n") lines.append(improvements) if bugs is not None: lines.append("\n## Bugs fixed\n") lines.append(bugs) if tasks is not None: lines.append("\n## Tasks\n") lines.append(tasks) return "\n".join(lines) def _fetch_issues(project, release): query = list() fix_version = release if project in ("qpid-cpp", "qpid-python"): fix_version = "{}-{}".format(project, release) if project in ("broker-j"): fix_version = "qpid-java-broker-{}".format(release) if project in ("qpid-cpp", "qpid-java", "qpid-python", "broker-j"): project = "qpid" if project == "qpid-interop-test": project = "qpidit" if project == "proton": fix_version = "proton-c-{}".format(release) if project == "proton-j": project = "proton" fix_version = "proton-j-{}".format(release) if project == "protonj2": project = "proton" fix_version = "protonj2-{}".format(release) if project == "proton-dotnet": project = "proton" fix_version = "proton-dotnet-{}".format(release) query.append("project = '{}'".format(project)) query.append("fixVersion = '{}'".format(fix_version)) query.append("resolution = 'fixed'") query = " and ".join(query) query = "{} order by key asc".format(query) page_size = 100 params = { "jql": query, "fields": "summary,issuetype", "maxResults": page_size, } issues = list() for i in range(100): params["startAt"] = i * page_size url = "https://issues.apache.org/jira/rest/api/2/search?{}".format(urllib.parse.urlencode(params)) filename, headers = urllib.request.urlretrieve(url) with open(filename) as f: data = yaml.load(f, Loader=yaml.Loader) issues.extend(data["issues"]) if len(issues) >= int(data["total"]): break return issues def _render_issues(issues, *types): filtered_issues = [x for x in issues if x["fields"]["issuetype"]["name"] in types] if not filtered_issues: return None lines = list() for issue in filtered_issues: key = escape_html(issue["key"]) url = "https://issues.apache.org/jira/browse/{}".format(key) summary = escape_html(issue["fields"]["summary"]) lines.append(" - [{}]({}) - {}".format(key, url, summary)) return "\n".join(lines)