Skip to content
This repository was archived by the owner on Jan 27, 2025. It is now read-only.

Commit 114f55e

Browse files
authored
Merge pull request #15 from ferrocene/pa-pull-from-ferrocene
Pull changes from the Ferrocene monorepo
2 parents 68e89ce + 1202908 commit 114f55e

File tree

13 files changed

+927
-235
lines changed

13 files changed

+927
-235
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616
steps:
1717
- uses: actions/checkout@v3
1818

19-
# Some Ferrocene builders require the use of Python 3.6. Use that on CI
19+
# Some Ferrocene builders require the use of Python 3.9. Use that on CI
2020
# to make sure there are no surprises when we import into Ferrocene.
2121
- uses: actions/setup-python@v3
2222
with:
23-
python-version: "3.6.x"
23+
python-version: "3.9.x"
2424

2525
- name: Check that the requirements are installable
2626
run: python3 -m pip install -r requirements.txt

exts/ferrocene_intersphinx_support.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# SPDX-License-Identifier: MIT OR Apache-2.0
2+
# SPDX-FileCopyrightText: Ferrous Systems and AdaCore
3+
4+
# This extension adds some helpers needed to integrate Ferrocene's build system
5+
# with InterSphinx. More specifically, the extension:
6+
#
7+
# - Defines the "ferrocene-intersphinx" Sphinx builder, which only produces the
8+
# objects.inv file required by InterSphinx. This is used to gather all the
9+
# inventories for all of our documentation before actually building anything,
10+
# as we have circular references between documents.
11+
#
12+
# - Defines the "ferrocene_intersphinx_mappings" configuration, which this
13+
# extension deserializes from JSON and then adds to the intersphinx_mapping
14+
# configuration. This is needed because the format of intersphinx_mapping is
15+
# too complex to be provided with the -D flag.
16+
17+
from sphinx.builders import Builder
18+
from sphinx.builders.html import StandaloneHTMLBuilder
19+
import json
20+
import sphinx
21+
22+
23+
class IntersphinxBuilder(Builder):
24+
name = "ferrocene-intersphinx"
25+
format = ""
26+
epilog = "InterSphinx inventory file generated."
27+
allow_parallel = True
28+
29+
def init(self):
30+
self.standalone_html_builder = StandaloneHTMLBuilder(self.app, self.env)
31+
32+
# Do not emit any warning in the ferrocene-intersphinx builder: there
33+
# will be warnings when using the builder, as the rest of the documents
34+
# won't be built yet, but we don't care about them.
35+
#
36+
# Keeping the warnings will confuse people who read the build logs,
37+
# thinking they should fix them while they're expected to happen.
38+
#
39+
# Unfortunately the only reliable way to suppress the warnings is
40+
# monkey-patching Sphinx's code, as you cannot set a global filter in
41+
# Python's logging module.
42+
sphinx.util.logging.WarningStreamHandler.emit = lambda _self, _record: None
43+
44+
def build(self, *args, **kwargs):
45+
# Normally you're not supposed to override the build() method, as
46+
# Sphinx calls all the relevant overrideable methods from it.
47+
#
48+
# Unfortunately though, Sphinx doesn't execute the finish() method if
49+
# there are no outdated docs (as we're simulating in this builder).
50+
#
51+
# Returning all documents from get_outdated_docs() would fix that
52+
# problem, but would also execute all the post_transforms for all
53+
# documents, which on large documents can take a while.
54+
#
55+
# Instead, we're returning an empty list of outdated documents, and
56+
# manually dumping the inventory here after the parent build() returns.
57+
super().build(*args, **kwargs)
58+
self.standalone_html_builder.dump_inventory()
59+
60+
def get_outdated_docs(self):
61+
return []
62+
63+
def prepare_writing(self, docnames):
64+
pass
65+
66+
def write_doc(self, docname, doctree):
67+
pass
68+
69+
def get_target_uri(self, docname, typ=None):
70+
# Defer to the standalone HTML builder to generate builders.
71+
return self.standalone_html_builder.get_target_uri(docname, typ)
72+
73+
74+
def inject_intersphinx_mappings(app, config):
75+
if config.ferrocene_intersphinx_mappings is not None:
76+
for inventory in json.loads(config.ferrocene_intersphinx_mappings):
77+
config.intersphinx_mapping[inventory["name"]] = (
78+
inventory["html_root"],
79+
inventory["inventory"],
80+
)
81+
82+
83+
def setup(app):
84+
app.add_builder(IntersphinxBuilder)
85+
86+
app.add_config_value("ferrocene_intersphinx_mappings", None, "env", [str])
87+
app.connect("config-inited", inject_intersphinx_mappings, priority=1)
88+
89+
return {
90+
"version": "0",
91+
"parallel_read_safe": True,
92+
"parallel_write_safe": True,
93+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-License-Identifier: MIT OR Apache-2.0
2+
# SPDX-FileCopyrightText: Ferrous Systems and AdaCore
3+
4+
from . import substitutions, document_id, domain, signature_page
5+
import string
6+
7+
8+
def setup(app):
9+
substitutions.setup(app)
10+
document_id.setup(app)
11+
domain.setup(app)
12+
signature_page.setup(app)
13+
14+
app.connect("config-inited", validate_config)
15+
app.add_config_value("ferrocene_id", None, "env", [str])
16+
app.add_config_value("ferrocene_substitutions_path", None, "env", [str])
17+
app.add_config_value("ferrocene_signed", False, "env", [str])
18+
19+
return {
20+
"version": "0",
21+
"parallel_read_safe": True,
22+
"parallel_write_safe": True,
23+
}
24+
25+
26+
def validate_config(app, config):
27+
for required in ["ferrocene_id", "ferrocene_substitutions_path"]:
28+
if config[required] is None:
29+
raise ValueError(f"Missing required {required} configuration")
30+
31+
if any(c not in string.ascii_uppercase for c in config["ferrocene_id"]):
32+
raise ValueError("ferrocene_id can only be uppercase letters")
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# SPDX-License-Identifier: MIT OR Apache-2.0
2+
# SPDX-FileCopyrightText: Ferrous Systems and AdaCore
3+
4+
# This module is responsible for generating the ID of the whole document. This
5+
# ID is supposed to uniquely identify the revision of the document, and it must
6+
# not change if the underlying content doesn't change.
7+
#
8+
# We can't use the git commit for the ID, as a commit could change something
9+
# unrelated to the content. Instead, we hash the parsed doctrees after all the
10+
# transforms executed, to capture the manually written content and the content
11+
# generated by other extensions, and use that as the ID.
12+
13+
from docutils import nodes
14+
from sphinx.transforms import SphinxTransform
15+
import hashlib
16+
import os
17+
import sphinx
18+
import struct
19+
20+
21+
class Hasher:
22+
def __init__(self):
23+
self.state = hashlib.sha1()
24+
25+
def string(self, string):
26+
encoded = string.encode("utf-8")
27+
28+
self.state.update(struct.pack("<Q", len(encoded)))
29+
self.state.update(encoded)
30+
31+
def node(self, node):
32+
self.string(node.tagname)
33+
34+
if isinstance(node, nodes.Text):
35+
self.state.update(b"\xFF") # Text node
36+
self.string(str(node))
37+
else:
38+
self.state.update(struct.pack("<Q", len(node.non_default_attributes())))
39+
for name, value in node.attlist():
40+
# The <document source=".."> attribute contains absolute paths
41+
# in it, breaking reproducibility.
42+
if isinstance(node, nodes.document) and name == "source":
43+
continue
44+
45+
self.string(name)
46+
self.string(repr(value))
47+
48+
for child in node.children:
49+
self.state.update(b"\xFE") # Enter child node
50+
self.node(child)
51+
self.state.update(b"\xFD") # Exit child node
52+
53+
def finalize(self):
54+
return self.state.hexdigest()
55+
56+
57+
class HashDocument(SphinxTransform):
58+
default_priority = 999
59+
60+
def apply(self):
61+
hasher = Hasher()
62+
hasher.node(self.document)
63+
64+
self.env.ferrocene_document_ids[self.env.docname] = hasher.finalize()
65+
66+
67+
def env_purge_doc(app, env, docname):
68+
if not hasattr(env, "ferrocene_document_ids"):
69+
env.ferrocene_document_ids = {}
70+
if docname in env.ferrocene_document_ids:
71+
del env.ferrocene_document_ids[docname]
72+
73+
74+
def env_merge_info(app, env, docnames, other):
75+
for doc in docnames:
76+
env.ferrocene_document_ids[doc] = other.ferrocene_document_ids[doc]
77+
78+
79+
def env_updated(app, env):
80+
hasher = Hasher()
81+
for document, hash in sorted(app.env.ferrocene_document_ids.items()):
82+
hasher.string(document)
83+
hasher.string(hash)
84+
85+
old_id = getattr(env, "ferrocene_document_id", None)
86+
new_id = f"{app.config.ferrocene_id}-{hasher.finalize()}"
87+
88+
app.env.ferrocene_document_id = new_id
89+
90+
# Mark all documents as updated if the ID changed since the last
91+
# invocation, to force all templates to use the new ID.
92+
if old_id != new_id:
93+
return env.all_docs.keys()
94+
95+
96+
def html_page_context(app, pagename, templatename, context, doctree):
97+
context["document_id"] = app.env.ferrocene_document_id
98+
99+
100+
def write_document_id(app):
101+
with sphinx.util.progress_message("writing document id"):
102+
with open(os.path.join(app.outdir, "document-id.txt"), "w") as f:
103+
f.write(f"{app.env.ferrocene_document_id}\n")
104+
105+
106+
def build_finished(app, exception):
107+
if exception is not None:
108+
return
109+
write_document_id(app)
110+
111+
112+
def setup(app):
113+
# We're not using an EnvCollector since it doesn't allow to set the
114+
# priority for the transform. Instead, our custom transform can set its
115+
# priority to 999. The downside is we have to connect events manually.
116+
app.add_transform(HashDocument)
117+
app.connect("env-purge-doc", env_purge_doc)
118+
app.connect("env-merge-info", env_merge_info)
119+
120+
app.connect("env-updated", env_updated)
121+
app.connect("html-page-context", html_page_context)
122+
app.connect("build-finished", build_finished)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# SPDX-License-Identifier: MIT OR Apache-2.0
2+
# SPDX-FileCopyrightText: Ferrous Systems and AdaCore
3+
4+
from docutils import nodes
5+
from sphinx.directives import ObjectDescription
6+
from sphinx.domains import Domain, ObjType
7+
from sphinx.roles import XRefRole
8+
import sphinx
9+
10+
11+
class Id:
12+
def __init__(self, document, id):
13+
self.document = document
14+
self.id = id
15+
16+
17+
class IdDirective(ObjectDescription):
18+
has_content = False
19+
required_arguments = 1
20+
option_spec = {}
21+
22+
def handle_signature(self, sig, signode):
23+
prefix = nodes.inline()
24+
prefix["classes"].append("hide-inside-tables")
25+
prefix += nodes.strong("", "Identifier:")
26+
prefix += nodes.Text(" ")
27+
28+
signode += prefix
29+
signode += nodes.literal("", sig)
30+
31+
def add_target_and_index(self, name_cls, sig, signode):
32+
id = Id(self.env.docname, sig)
33+
signode["ids"].append(id.id)
34+
35+
domain = self.env.get_domain("qualification")
36+
domain.add_id(id)
37+
38+
39+
class QualificationDomain(Domain):
40+
name = "qualification"
41+
labels = "Qualification Documents"
42+
directives = {
43+
"id": IdDirective,
44+
}
45+
roles = {
46+
"id": XRefRole(),
47+
}
48+
object_types = {
49+
"id": ObjType("Identifier", "id"),
50+
}
51+
52+
initial_data = {"ids": {}}
53+
# Bump whenever the format of the data changes!
54+
data_version = 1
55+
56+
def add_id(self, id):
57+
self.data["ids"][id.id] = id
58+
59+
def clear_doc(self, docname):
60+
self.data["ids"] = {
61+
key: value
62+
for key, value in self.data["ids"].items()
63+
if value.document != docname
64+
}
65+
66+
def merge_domaindata(self, docnames, otherdata):
67+
for key, value in otherdata["ids"].items():
68+
if value.document in docnames:
69+
self.data["ids"][value.id] = value
70+
71+
def resolve_xref(self, env, fromdocname, builder, type, target, node, contnode):
72+
if type != "id":
73+
raise RuntimeError(f"unsupported xref type {type}")
74+
75+
if target not in self.data["ids"]:
76+
return
77+
id = self.data["ids"][target]
78+
79+
return sphinx.util.nodes.make_refnode(
80+
builder, fromdocname, id.document, id.id, contnode
81+
)
82+
83+
def get_objects(self):
84+
for id in self.data["ids"].values():
85+
# (name, display_name, type, document, anchor, priority)
86+
yield id.id, id.id, "id", id.document, id.id, 1
87+
88+
89+
def setup(app):
90+
app.add_domain(QualificationDomain)

0 commit comments

Comments
 (0)