Skip to content

Commit f5ce486

Browse files
Merge pull request #7 from smithy-lang/sphinx-autobuild
Automatically build sphinx projects
2 parents d2b932c + e1e0b12 commit f5ce486

File tree

3 files changed

+240
-2
lines changed

3 files changed

+240
-2
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.docgen.core;
7+
8+
import static java.lang.String.format;
9+
10+
import java.io.BufferedReader;
11+
import java.io.IOException;
12+
import java.io.InputStreamReader;
13+
import java.nio.charset.Charset;
14+
import java.nio.file.Path;
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
import java.util.logging.Logger;
18+
import software.amazon.smithy.codegen.core.CodegenException;
19+
import software.amazon.smithy.utils.SmithyUnstableApi;
20+
21+
/**
22+
* Provides various utility methods.
23+
*/
24+
@SmithyUnstableApi
25+
public final class DocgenUtils {
26+
27+
private static final Logger LOGGER = Logger.getLogger(DocgenUtils.class.getName());
28+
29+
private DocgenUtils() {}
30+
31+
/**
32+
* Executes a given shell command in a given directory.
33+
*
34+
* @param command The string command to execute, e.g. "sphinx-build".
35+
* @param directory The directory to run the command in.
36+
* @return Returns the console output of the command.
37+
*/
38+
public static String runCommand(String command, Path directory) {
39+
String[] finalizedCommand;
40+
if (System.getProperty("os.name").toLowerCase().startsWith("windows")) {
41+
finalizedCommand = new String[]{"cmd.exe", "/c", command};
42+
} else {
43+
finalizedCommand = new String[]{"sh", "-c", command};
44+
}
45+
46+
ProcessBuilder processBuilder = new ProcessBuilder(finalizedCommand)
47+
.redirectErrorStream(true)
48+
.directory(directory.toFile());
49+
50+
try {
51+
Process process = processBuilder.start();
52+
List<String> output = new ArrayList<>();
53+
54+
// Capture output for reporting.
55+
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(
56+
process.getInputStream(), Charset.defaultCharset()))) {
57+
String line;
58+
while ((line = bufferedReader.readLine()) != null) {
59+
LOGGER.finest(line);
60+
output.add(line);
61+
}
62+
}
63+
64+
process.waitFor();
65+
process.destroy();
66+
67+
String joinedOutput = String.join(System.lineSeparator(), output);
68+
if (process.exitValue() != 0) {
69+
throw new CodegenException(format(
70+
"Command `%s` failed with output:%n%n%s", command, joinedOutput));
71+
}
72+
return joinedOutput;
73+
} catch (InterruptedException | IOException e) {
74+
throw new CodegenException(e);
75+
}
76+
}
77+
78+
/**
79+
* Replaces all newline characters in a string with the system line separator.
80+
* @param input The string to normalize
81+
* @return A string with system-appropriate newlines.
82+
*/
83+
public static String normalizeNewlines(String input) {
84+
return input.replaceAll("\r?\n", System.lineSeparator());
85+
}
86+
}

smithy-docgen-core/src/main/java/software/amazon/smithy/docgen/core/integrations/SphinxIntegration.java

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,68 @@
55

66
package software.amazon.smithy.docgen.core.integrations;
77

8+
import static java.lang.String.format;
9+
import static software.amazon.smithy.docgen.core.DocgenUtils.normalizeNewlines;
10+
import static software.amazon.smithy.docgen.core.DocgenUtils.runCommand;
11+
812
import java.util.List;
913
import java.util.Set;
1014
import java.util.logging.Logger;
15+
import software.amazon.smithy.codegen.core.CodegenException;
1116
import software.amazon.smithy.docgen.core.DocFormat;
1217
import software.amazon.smithy.docgen.core.DocGenerationContext;
1318
import software.amazon.smithy.docgen.core.DocIntegration;
1419
import software.amazon.smithy.docgen.core.DocSettings;
1520
import software.amazon.smithy.docgen.core.sections.sphinx.ConfSection;
1621
import software.amazon.smithy.docgen.core.sections.sphinx.MakefileSection;
22+
import software.amazon.smithy.docgen.core.sections.sphinx.RequirementsSection;
1723
import software.amazon.smithy.docgen.core.sections.sphinx.WindowsMakeSection;
1824
import software.amazon.smithy.docgen.core.writers.SphinxMarkdownWriter;
1925
import software.amazon.smithy.model.shapes.ServiceShape;
2026
import software.amazon.smithy.utils.SmithyInternalApi;
2127

2228
/**
2329
* Adds Sphinx project scaffolding for compatible formats.
30+
*
31+
* <p>This integration runs in low priority to allow other integrations to generate
32+
* files that will be picked up by sphinx-build. To have an integration reliably run
33+
* after this, override {@link DocIntegration#runAfter} with the output of
34+
* {@link SphinxIntegration#name} in the list. Similarly, to guarantee an integration
35+
* is run before this, override {@link DocIntegration#runBefore} with the same argument.
36+
*
37+
* <p>To customize the project files generated by this integration, you can make use
38+
* of {@link DocIntegration#interceptors} to intercept and modify the files before
39+
* they're written. The following named code sections are used:
40+
*
41+
* <ul>
42+
* <li>{@link ConfSection}: Creates the {@code conf.py}
43+
* <li>{@link MakefileSection}: Creates the {@code Makefile} build script for unix.
44+
* <li>{@link WindowsMakeSection}: Creates the {@code make.bat} build script for
45+
* Windows.
46+
* <li>{@link RequirementsSection}: Creates the {@code requirements.txt} used to
47+
* build the docs. Any dependencies here will be installed into the environment
48+
* used to run {@code sphinx-build}.
49+
* </ul>
2450
*/
2551
@SmithyInternalApi
2652
public final class SphinxIntegration implements DocIntegration {
2753
private static final String MARKDOWN_FORMAT = "sphinx-markdown";
2854
private static final Set<String> FORMATS = Set.of(MARKDOWN_FORMAT);
2955
private static final Logger LOGGER = Logger.getLogger(SphinxIntegration.class.getName());
3056

57+
// The default requirements needed to build the docs.
58+
private static final List<String> REQUIREMENTS = List.of(
59+
"Sphinx==7.2.6",
60+
"myst-parser==2.0.0",
61+
"linkify-it-py==2.0.2"
62+
);
63+
64+
@Override
65+
public byte priority() {
66+
// Run at the end so that any integration-generated changes can happen.
67+
return -128;
68+
}
69+
3170
@Override
3271
public List<DocFormat> docFormats(DocSettings settings) {
3372
return List.of(
@@ -38,16 +77,26 @@ public List<DocFormat> docFormats(DocSettings settings) {
3877
@Override
3978
public void customize(DocGenerationContext context) {
4079
if (!FORMATS.contains(context.docFormat().name())) {
41-
LOGGER.finest(String.format(
80+
LOGGER.finest(format(
4281
"Format %s is not a Sphinx-compatible format, skipping Sphinx project setup.",
4382
context.docFormat().name()
4483
));
4584
return;
4685
}
4786
// TODO: add some way to disable project file generation
48-
LOGGER.finest("Generating Sphinx project files.");
87+
LOGGER.info("Generating Sphinx project files.");
88+
writeRequirements(context);
4989
writeConf(context);
5090
writeMakefile(context);
91+
runSphinx(context);
92+
}
93+
94+
private void writeRequirements(DocGenerationContext context) {
95+
context.writerDelegator().useFileWriter("requirements.txt", writer -> {
96+
writer.pushState(new RequirementsSection(context, REQUIREMENTS));
97+
REQUIREMENTS.forEach(writer::write);
98+
writer.popState();
99+
});
51100
}
52101

53102
private void writeConf(DocGenerationContext context) {
@@ -155,4 +204,85 @@ private void writeMakefile(DocGenerationContext context) {
155204
writer.popState();
156205
});
157206
}
207+
208+
private void runSphinx(DocGenerationContext context) {
209+
var baseDir = context.fileManifest().getBaseDir();
210+
211+
LOGGER.info("Flushing writers in preparation for sphinx-build.");
212+
context.writerDelegator().flushWriters();
213+
214+
// Python must be available to run sphinx
215+
try {
216+
LOGGER.info("Attempting to discover python3 in order to run sphinx.");
217+
runCommand("python3 --version", baseDir);
218+
} catch (CodegenException e) {
219+
LOGGER.warning("Unable to find python3 on path. Skipping automatic HTML doc build.");
220+
logManualBuildInstructions(context);
221+
return;
222+
}
223+
224+
// TODO: detect if the user's existing python environment can be used
225+
// You can get a big JSON document describing the python environment from
226+
// `pip inspect` that has all the information we need.
227+
try {
228+
// First, we create a virtualenv to install dependencies into. This is necessary
229+
// to not pollute the user's environment.
230+
runCommand("python3 -m venv venv", baseDir);
231+
232+
// Next, install the dependencies into the venv.
233+
runCommand("./venv/bin/pip install -r requirements.txt", baseDir);
234+
235+
// Finally, run sphinx itself.
236+
runCommand("./venv/bin/sphinx-build -M dirhtml content build", baseDir);
237+
238+
System.out.printf(normalizeNewlines("""
239+
Successfully built HTML docs. They can be found in "%1$s".
240+
241+
Other output formats can also be built. A python virtual environment \
242+
has been created at "%2$s" containing the build tools needed for \
243+
manually building the docs in other formats. See the virtual \
244+
environment docs for information on how to activate it: \
245+
https://docs.python.org/3/library/venv.html#how-venvs-work
246+
247+
Once the environment is activated, run `make dirhtml` from "%3$s" to \
248+
to build the docs, substituting dirhtml for whatever format you wish \
249+
to build.
250+
251+
To build the docs without activating the virtual environment, simply \
252+
run `./venv/bin/sphinx-build -M dirhtml content build` from "%3$s", \
253+
similarly substituting dirhtml for your desired format.
254+
255+
See sphinx docs for other output formats you can choose: \
256+
https://www.sphinx-doc.org/en/master/usage/builders/index.html
257+
258+
"""),
259+
baseDir.resolve("build/dirhtml"),
260+
baseDir.resolve("venv"),
261+
baseDir
262+
);
263+
} catch (CodegenException e) {
264+
LOGGER.warning("Unable to automatically build HTML docs: " + e);
265+
logManualBuildInstructions(context);
266+
}
267+
}
268+
269+
private void logManualBuildInstructions(DocGenerationContext context) {
270+
// TODO: try to get this printed out in the projection section
271+
System.out.printf(normalizeNewlines("""
272+
To build the HTML docs manually, you need to first install the python \
273+
dependencies. These can be found in the `requirements.txt` file in \
274+
"%1$s". The easiest way to install these is by running `pip install \
275+
-r requirements.txt`. Depending on your environment, you may need to \
276+
instead install them from your system package manager, or another \
277+
source.
278+
279+
Once the dependencies are installed, run `make dirhtml` from \
280+
"%1$s". Other output formats can also be built. See sphinx docs for \
281+
other output formats: \
282+
https://www.sphinx-doc.org/en/master/usage/builders/index.html
283+
284+
"""),
285+
context.fileManifest().getBaseDir()
286+
);
287+
}
158288
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.docgen.core.sections.sphinx;
7+
8+
import java.util.List;
9+
import software.amazon.smithy.docgen.core.DocGenerationContext;
10+
import software.amazon.smithy.utils.CodeSection;
11+
12+
/**
13+
* Generates a requirements file needed to install and run sphinx.
14+
*
15+
* <p>Any requirements added here will be installed in the environment used to
16+
* automatically build the docs with {@code sphinx-build}.
17+
*
18+
* @param context The context used to generate documentation.
19+
* @param requirements The requirements as a list of <a href="https://peps.python.org/pep-0508/">PEP 508</a> strings.
20+
*/
21+
public record RequirementsSection(DocGenerationContext context, List<String> requirements) implements CodeSection {
22+
}

0 commit comments

Comments
 (0)