Source code
Revision control
Copy as Markdown
Other Tools
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
"""Render about:license from the LICENSES declarations collected tree-wide.
The build backend writes every LICENSES entry reachable in this configuration
to <objdir>/licenses.json; this turns that into HTML. Because an unconfigured
directory is never traversed, a license only appears in builds that ship the
code it covers, which is what the old per-entry #ifdefs did by hand.
"""
import html
import json
import os
import re
from mako.template import Template
# MPL and the LGPLs head the list: they cover the bulk of the product rather
# than one vendored library, and about:license has always led with them.
PINNED = ("mpl", "lgpl", "lgpl-3.0")
def sort_key(license):
try:
return (0, PINNED.index(license["id"]), "")
except ValueError:
return (1, 0, license["title"].lower())
# The blocks an application splices into the page, in the order they are passed
# as extra inputs. These were #includesubst of a path held in a per-directory
# DEFINE, so they cannot come from the global config: browser/base substitutes
# APP_LICENSE_BLOCK and overrides chrome://global/content/license.html with the
# result, which is the about:license Firefox actually ships.
APP_BLOCKS = (
"app_license_block",
"app_license_list_block",
"app_license_body_block",
"app_license_product_name",
)
# Whitespace and markup at the very end of a notice, so the colon that matters
# is found whether the notice closes its last tag or not.
TRAILING_MARKUP = re.compile(r"(?:\s|<[^<>]*>)*$")
def leads_paths(notice):
"""Whether a notice already introduces the coverage list itself.
A notice trailing off in a colon ("This license applies to the files:") is
the heading for the list that follows, so the template must not add a
second one of its own in front of it.
"""
return TRAILING_MARKUP.sub("", notice).endswith(":")
CODE = re.compile(r"<code>\s*([^<]*?)\s*</code>")
def notice_names_paths(notice, paths):
"""Whether a notice already names every path the license covers.
Most notices read "This license applies to files in the directory
<code>third_party/rust/nom</code>.", which a coverage list would only
repeat.
"""
named = {name.rstrip("/") for name in CODE.findall(notice)}
return all(path.rstrip("/") in named for path in paths)
def read_blocks(paths):
blocks = dict.fromkeys(APP_BLOCKS, "")
for name, path in zip(APP_BLOCKS, paths):
with open(path, encoding="utf-8") as fh:
blocks[name] = fh.read()
if bool(blocks["app_license_list_block"]) != bool(blocks["app_license_body_block"]):
raise Exception(
"APP_LICENSE_LIST_BLOCK and APP_LICENSE_BODY_BLOCK go together."
)
return blocks
def render(template_path, licenses, substs, blocks=None):
"""Render about:license.
Kept free of buildconfig, which is an objdir module, so `mach vendor
licenses` can call it with the substs it already holds.
"""
for license in licenses:
if not license.get("html"):
license["text"] = html.escape(license["text"], quote=False)
license["notice_leads_paths"] = leads_paths(license.get("notice") or "")
license["notice_names_paths"] = notice_names_paths(
license.get("notice") or "", license.get("paths") or []
)
acknowledgements = [
l["acknowledgement"]
for l in sorted(licenses, key=lambda l: l["id"])
if l.get("acknowledgement")
]
return Template(filename=template_path, output_encoding=None).render(
licenses=sorted(licenses, key=sort_key),
acknowledgements=acknowledgements,
config=substs,
**(blocks or dict.fromkeys(APP_BLOCKS, "")),
)
def main(output, template_path, licenses_path, *app_block_paths):
import buildconfig
with open(licenses_path, encoding="utf-8") as fh:
licenses = json.load(fh)["licenses"]
# An artifact build does not traverse the directories only a compiling
# build does, so it takes their records from the build its binaries came
# from, as installed by `mach artifact install`.
deps = {template_path, licenses_path, *app_block_paths}
if buildconfig.substs.get("MOZ_ARTIFACT_BUILDS"):
compiled_path = os.path.join(
buildconfig.topobjdir, "dist", "licenses.artifact.json"
)
deps.add(compiled_path)
if os.path.exists(compiled_path):
with open(compiled_path, encoding="utf-8") as fh:
compiled = {l["id"]: l for l in json.load(fh)["licenses"]}
for license in licenses:
extra = compiled.pop(license["id"], {}).get("paths", [])
license["paths"] = sorted(set(license["paths"]) | set(extra))
licenses += compiled.values()
output.write(
render(
template_path,
licenses,
buildconfig.substs,
read_blocks(app_block_paths),
)
)
return deps