sphinx-java-autodoc 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sphinx_java_autodoc/__init__.py +239 -0
- sphinx_java_autodoc/__main__.py +67 -0
- sphinx_java_autodoc/domain.py +387 -0
- sphinx_java_autodoc/extractor.py +254 -0
- sphinx_java_autodoc/java/SphinxDoclet.java +1036 -0
- sphinx_java_autodoc/javadoc.py +45 -0
- sphinx_java_autodoc/model.py +173 -0
- sphinx_java_autodoc/render.py +469 -0
- sphinx_java_autodoc/static/java-autodoc.css +28 -0
- sphinx_java_autodoc-0.2.0.dist-info/METADATA +109 -0
- sphinx_java_autodoc-0.2.0.dist-info/RECORD +14 -0
- sphinx_java_autodoc-0.2.0.dist-info/WHEEL +4 -0
- sphinx_java_autodoc-0.2.0.dist-info/entry_points.txt +2 -0
- sphinx_java_autodoc-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""One compiler invocation per content fingerprint; no application code execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import xml.etree.ElementTree as ET
|
|
13
|
+
|
|
14
|
+
from sphinx.errors import ExtensionError
|
|
15
|
+
|
|
16
|
+
from .model import read_model
|
|
17
|
+
|
|
18
|
+
VERSION = "0.2.0" # x-release-please-version
|
|
19
|
+
DOCLET = Path(__file__).parent / "java" / "SphinxDoclet.java"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def run(arguments: list[str], *, cwd: Path | None = None) -> str:
|
|
23
|
+
try:
|
|
24
|
+
result = subprocess.run(
|
|
25
|
+
arguments,
|
|
26
|
+
cwd=cwd,
|
|
27
|
+
text=True,
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
errors="replace",
|
|
30
|
+
capture_output=True,
|
|
31
|
+
timeout=180,
|
|
32
|
+
)
|
|
33
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
34
|
+
raise ExtensionError(
|
|
35
|
+
f"Java autodoc: could not run {arguments[0]}: {exc}"
|
|
36
|
+
) from exc
|
|
37
|
+
if result.returncode:
|
|
38
|
+
raise ExtensionError(
|
|
39
|
+
f"Java autodoc: {Path(arguments[0]).name} failed (exit {result.returncode}). "
|
|
40
|
+
"Check JDK 25, source paths, --release and the compile classpath.\n"
|
|
41
|
+
+ result.stdout
|
|
42
|
+
+ result.stderr
|
|
43
|
+
)
|
|
44
|
+
return result.stdout + result.stderr
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def atomic_write(path: Path, data: bytes) -> None:
|
|
48
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
if path.exists() and path.read_bytes() == data:
|
|
50
|
+
return
|
|
51
|
+
with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as stream:
|
|
52
|
+
temporary = Path(stream.name)
|
|
53
|
+
try:
|
|
54
|
+
stream.write(data)
|
|
55
|
+
stream.flush()
|
|
56
|
+
stream.close()
|
|
57
|
+
os.replace(temporary, path)
|
|
58
|
+
finally:
|
|
59
|
+
temporary.unlink(missing_ok=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def extract(
|
|
63
|
+
sources: list[Path],
|
|
64
|
+
classpath: list[Path],
|
|
65
|
+
cache: Path,
|
|
66
|
+
*,
|
|
67
|
+
jdk: str = "",
|
|
68
|
+
release: str = "",
|
|
69
|
+
sourcepath: list[Path] | None = None,
|
|
70
|
+
) -> tuple[Path, str]:
|
|
71
|
+
"""Return immutable XML path and input fingerprint. All inputs are content hashed."""
|
|
72
|
+
roots = [p.resolve() for p in sources]
|
|
73
|
+
dependencies = [p.resolve() for p in classpath]
|
|
74
|
+
lookup = [p.resolve() for p in sourcepath or []]
|
|
75
|
+
for path in lookup:
|
|
76
|
+
if not path.is_dir():
|
|
77
|
+
raise ExtensionError(
|
|
78
|
+
f"Java autodoc: source lookup directory does not exist: {path}"
|
|
79
|
+
)
|
|
80
|
+
files: set[Path] = set()
|
|
81
|
+
for path in roots:
|
|
82
|
+
if not path.exists():
|
|
83
|
+
raise ExtensionError(f"Java autodoc: source path does not exist: {path}")
|
|
84
|
+
found = list(path.rglob("*.java")) if path.is_dir() else [path]
|
|
85
|
+
if not found or any(p.suffix != ".java" for p in found):
|
|
86
|
+
raise ExtensionError(f"Java autodoc: no Java source files in {path}")
|
|
87
|
+
files.update(found)
|
|
88
|
+
if not files:
|
|
89
|
+
raise ExtensionError(
|
|
90
|
+
"Java autodoc: no sources configured; set java_sources or java_model."
|
|
91
|
+
)
|
|
92
|
+
semantic_files = files | {p for root in lookup for p in root.rglob("*.java")}
|
|
93
|
+
if any(p.name == "module-info.java" for p in semantic_files):
|
|
94
|
+
raise ExtensionError(
|
|
95
|
+
"Java autodoc: JPMS module-info.java inputs are deferred; supply non-modular source roots or individual Java files and a compile classpath."
|
|
96
|
+
)
|
|
97
|
+
for path in dependencies:
|
|
98
|
+
if not path.exists():
|
|
99
|
+
raise ExtensionError(
|
|
100
|
+
f"Java autodoc: classpath entry does not exist: {path}"
|
|
101
|
+
)
|
|
102
|
+
tools = {}
|
|
103
|
+
for name in ("java", "javac", "javadoc"):
|
|
104
|
+
executable = str(Path(jdk) / "bin" / name) if jdk else shutil.which(name)
|
|
105
|
+
if not executable:
|
|
106
|
+
raise ExtensionError(
|
|
107
|
+
f"Java autodoc: {name} not found; install JDK 25 and set java_jdk."
|
|
108
|
+
)
|
|
109
|
+
tools[name] = executable
|
|
110
|
+
identity = run([tools["java"], "-XshowSettings:properties", "-version"])
|
|
111
|
+
# Use the actual runtime home, avoiding mixed tools from PATH and macOS launchers.
|
|
112
|
+
home = next(
|
|
113
|
+
(
|
|
114
|
+
line.split("=", 1)[1].strip()
|
|
115
|
+
for line in identity.splitlines()
|
|
116
|
+
if line.strip().startswith("java.home =")
|
|
117
|
+
),
|
|
118
|
+
"",
|
|
119
|
+
)
|
|
120
|
+
version = next(
|
|
121
|
+
(
|
|
122
|
+
line.split("=", 1)[1].strip()
|
|
123
|
+
for line in identity.splitlines()
|
|
124
|
+
if line.strip().startswith("java.version =")
|
|
125
|
+
),
|
|
126
|
+
"",
|
|
127
|
+
)
|
|
128
|
+
if version.split(".")[0] != "25":
|
|
129
|
+
raise ExtensionError(
|
|
130
|
+
f"Java autodoc: JDK 25 is required for this release; found {version or 'unknown'}."
|
|
131
|
+
)
|
|
132
|
+
if home:
|
|
133
|
+
tools = {name: str(Path(home) / "bin" / name) for name in tools}
|
|
134
|
+
digest = hashlib.sha256()
|
|
135
|
+
|
|
136
|
+
def update(data: bytes) -> None:
|
|
137
|
+
digest.update(len(data).to_bytes(8, "big"))
|
|
138
|
+
digest.update(data)
|
|
139
|
+
|
|
140
|
+
update(
|
|
141
|
+
json.dumps(
|
|
142
|
+
[
|
|
143
|
+
VERSION,
|
|
144
|
+
version,
|
|
145
|
+
home,
|
|
146
|
+
release,
|
|
147
|
+
[str(p) for p in roots],
|
|
148
|
+
[str(p) for p in dependencies],
|
|
149
|
+
[str(p) for p in lookup],
|
|
150
|
+
]
|
|
151
|
+
).encode()
|
|
152
|
+
)
|
|
153
|
+
update(DOCLET.read_bytes())
|
|
154
|
+
update(Path(__file__).read_bytes())
|
|
155
|
+
for path in sorted(semantic_files):
|
|
156
|
+
update(str(path).encode())
|
|
157
|
+
update(path.read_bytes())
|
|
158
|
+
for path in dependencies:
|
|
159
|
+
for entry in sorted(path.rglob("*")) if path.is_dir() else [path]:
|
|
160
|
+
if entry.is_file():
|
|
161
|
+
update(str(entry).encode())
|
|
162
|
+
update(entry.read_bytes())
|
|
163
|
+
# JDK replacement at the same path must not reuse an old compiler result.
|
|
164
|
+
for name in ("release", "lib/modules"):
|
|
165
|
+
path = Path(home) / name
|
|
166
|
+
if path.is_file():
|
|
167
|
+
with path.open("rb") as stream:
|
|
168
|
+
while block := stream.read(1024 * 1024):
|
|
169
|
+
digest.update(block)
|
|
170
|
+
fingerprint = digest.hexdigest()
|
|
171
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
destination = cache / f"{fingerprint}.xml"
|
|
173
|
+
if destination.exists():
|
|
174
|
+
read_model(destination)
|
|
175
|
+
return destination, fingerprint
|
|
176
|
+
with tempfile.TemporaryDirectory(prefix="extract-", dir=cache) as directory:
|
|
177
|
+
work = Path(directory)
|
|
178
|
+
classes = work / "classes"
|
|
179
|
+
run(
|
|
180
|
+
[
|
|
181
|
+
tools["javac"],
|
|
182
|
+
"-encoding",
|
|
183
|
+
"UTF-8",
|
|
184
|
+
"-proc:none",
|
|
185
|
+
"-d",
|
|
186
|
+
str(classes),
|
|
187
|
+
str(DOCLET),
|
|
188
|
+
]
|
|
189
|
+
)
|
|
190
|
+
output = work / "api.xml"
|
|
191
|
+
args = [
|
|
192
|
+
"-locale",
|
|
193
|
+
"en_US",
|
|
194
|
+
"-quiet",
|
|
195
|
+
"-encoding",
|
|
196
|
+
"UTF-8",
|
|
197
|
+
"-protected",
|
|
198
|
+
"-doclet",
|
|
199
|
+
"sphinxjava.SphinxDoclet",
|
|
200
|
+
"-docletpath",
|
|
201
|
+
str(classes),
|
|
202
|
+
"--sphinx-output",
|
|
203
|
+
str(output),
|
|
204
|
+
]
|
|
205
|
+
if release:
|
|
206
|
+
args += ["--release", str(release)]
|
|
207
|
+
# An explicit empty classpath avoids implicitly reading classes from cwd.
|
|
208
|
+
empty = work / "empty"
|
|
209
|
+
empty.mkdir()
|
|
210
|
+
args += ["-classpath", os.pathsep.join(map(str, dependencies)) or str(empty)]
|
|
211
|
+
args += [
|
|
212
|
+
"-sourcepath",
|
|
213
|
+
os.pathsep.join(
|
|
214
|
+
str(p) for p in dict.fromkeys([p for p in roots if p.is_dir()] + lookup)
|
|
215
|
+
)
|
|
216
|
+
or str(empty),
|
|
217
|
+
]
|
|
218
|
+
args += [str(p) for p in sorted(files)]
|
|
219
|
+
# JDK argument files avoid command length limits; no shell interpolation.
|
|
220
|
+
argfile = work / "javadoc.args"
|
|
221
|
+
argfile.write_text(
|
|
222
|
+
"\n".join(
|
|
223
|
+
'"'
|
|
224
|
+
+ s.replace("\\", "\\\\")
|
|
225
|
+
.replace('"', '\\"')
|
|
226
|
+
.replace("\n", "\\n")
|
|
227
|
+
.replace("\r", "\\r")
|
|
228
|
+
+ '"'
|
|
229
|
+
for s in args
|
|
230
|
+
),
|
|
231
|
+
encoding="utf-8",
|
|
232
|
+
)
|
|
233
|
+
diagnostics = run([tools["javadoc"], "@" + str(argfile)], cwd=work)
|
|
234
|
+
root = read_model(output)
|
|
235
|
+
root.set("extractor", VERSION)
|
|
236
|
+
if diagnostics.strip():
|
|
237
|
+
# Compiler warnings must remain visible on cache hits too.
|
|
238
|
+
labels = {str(work): "<extract>"}
|
|
239
|
+
for name, paths in (
|
|
240
|
+
("source", roots),
|
|
241
|
+
("sourcepath", lookup),
|
|
242
|
+
("classpath", dependencies),
|
|
243
|
+
):
|
|
244
|
+
for index, path in enumerate(paths):
|
|
245
|
+
labels.setdefault(str(path), f"<{name}:{index}>")
|
|
246
|
+
message = diagnostics.strip()
|
|
247
|
+
# Longest first, so a root like src does not relabel part of src-gen.
|
|
248
|
+
for path in sorted(labels, key=len, reverse=True):
|
|
249
|
+
message = message.replace(path, labels[path])
|
|
250
|
+
ET.SubElement(root, "diagnostic").text = message
|
|
251
|
+
atomic_write(
|
|
252
|
+
destination, ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
|
253
|
+
)
|
|
254
|
+
return destination, fingerprint
|