Skip to content

Commit bd1b603

Browse files
committed
chore(ci): replace non-local crate docs with redirect pages to docs.rs
1 parent 26f9794 commit bd1b603

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed

.github/workflows/doc.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,18 @@ jobs:
1616
with:
1717
profile: minimal
1818

19+
- name: Install dependencies (Linux)
20+
run: .github/scripts/install-deno.sh
21+
1922
- name: Build Documentation
2023
uses: actions-rs/cargo@v1
2124
with:
2225
command: doc
2326
args: -p r3_port_std -p r3_port_arm -p r3_port_arm_m -p r3_port_riscv -p r3_support_rp2040 -p r3_support_rza1 -p r3_portkit -p r3_kernel -p r3 -p r3_core --all-features
2427

28+
- name: Replace the non-local crate documentation with redirect pages to docs.rs
29+
run: deno run -A scripts/externalize-non-local-docs.ts -y
30+
2531
- name: Generate Badge
2632
run: |
2733
rev=`git show-ref --head HEAD | cut -b 1-7`
@@ -46,7 +52,6 @@ jobs:
4652
folder: output
4753
single-commit: true
4854

49-
5055
# Otherwise, put it on the Actions page to allow download
5156
- name: Archive the generated documentation
5257
if: github.ref != 'refs/heads/🦆'

scripts/externalize-non-local-docs.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// This [Deno] program scans `target/doc` directory and replaces the non-local
2+
// crate documentation with redirect pages to docs.rs.
3+
//
4+
// [Deno]: https://deno.land/
5+
//
6+
// Usage: deno run -A scripts/externalize-non-local-docs.ts
7+
import { parse as parseFlags } from "https://deno.land/std@0.125.0/flags/mod.ts";
8+
import { parse as parseToml } from "https://deno.land/std@0.125.0/encoding/toml.ts";
9+
import * as path from "https://deno.land/std@0.125.0/path/mod.ts";
10+
import * as log from "https://deno.land/std@0.125.0/log/mod.ts";
11+
import { walk } from "https://deno.land/std@0.125.0/fs/walk.ts";
12+
import * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts";
13+
14+
const parsedArgs = parseFlags(Deno.args, {
15+
"alias": {
16+
h: "help",
17+
w: "workspace",
18+
y: "apply",
19+
},
20+
"string": [
21+
"workspace",
22+
],
23+
});
24+
25+
if (parsedArgs["help"]) {
26+
console.log("Arguments:");
27+
console.log(" -h --help Displays this message");
28+
console.log(" -w WORKSPACE --workspace=WORKSPACE");
29+
console.log(" Specifies the workspace directory. Defaults to `.` when unspecified.");
30+
console.log(" -y --apply Actually make modification");
31+
}
32+
33+
await log.setup({
34+
handlers: {
35+
console: new log.handlers.ConsoleHandler("DEBUG"),
36+
},
37+
38+
loggers: {
39+
default: {
40+
level: "INFO",
41+
handlers: ["console"],
42+
},
43+
},
44+
});
45+
46+
const logger = log.getLogger();
47+
48+
// This script is quite destructive, so as a safety measure, we don't make
49+
// changes unless we're expressly told to do so.
50+
const shouldModify: boolean = parsedArgs.y;
51+
52+
if (!shouldModify) {
53+
logger.warning("Performing a dry run because the `--apply` flag was not given");
54+
}
55+
56+
// Get the workspace metadata <https://doc.rust-lang.org/1.58.1/cargo/commands/cargo-metadata.html>
57+
const workspaceMetaJson = await (async () => {
58+
if ((parsedArgs.w || ".") !== ".") {
59+
logger.error("Unsupported: Operating on a workspace outside the current "
60+
+ "directory is not supported.");
61+
Deno.exit(1);
62+
}
63+
const process = Deno.run({
64+
cmd: [
65+
"cargo", "metadata", "--format-version=1", "--all-features",
66+
],
67+
stdout: "piped",
68+
});
69+
const [stdoutBytes, status] = await Promise.all([process.output(), process.status()]);
70+
if (!status.success) {
71+
Deno.exit(status.code);
72+
}
73+
return new TextDecoder().decode(stdoutBytes);
74+
})();
75+
const workspaceMeta: CargoMetadataV1 = JSON.parse(workspaceMetaJson);
76+
77+
type CargoMetadataV1 = {
78+
packages: PackageMetadataV1[],
79+
};
80+
type PackageMetadataV1 = {
81+
name: string,
82+
version: string,
83+
source: string | null,
84+
targets: {
85+
kind: string[],
86+
crate_types: string[],
87+
name: string,
88+
}[],
89+
};
90+
91+
// Calculate the mapping from crate names to docs.rs URLs
92+
const crateNameToPackage = new Map<string, PackageMetadataV1[]>();
93+
logger.debug("Constructing a mapping from crate names to packages");
94+
for (const pkg of workspaceMeta.packages) {
95+
logger.debug(` - ${pkg.name} ${pkg.version}`);
96+
97+
const libraryTarget = pkg.targets.find(t => t.kind.find(k => k == "lib" || k == "proc-macro"));
98+
if (libraryTarget == null) {
99+
logger.debug("It doesn't provide a library crate - ignoring");
100+
continue;
101+
}
102+
103+
const crateName = libraryTarget.name.replace(/-/g, '_');
104+
logger.debug(`Crate name = ${crateName}`);
105+
if (!crateNameToPackage.has(crateName)) {
106+
crateNameToPackage.set(crateName, []);
107+
}
108+
crateNameToPackage.get(crateName)!.push(pkg);
109+
}
110+
if (crateNameToPackage.size === 0) {
111+
logger.error("The crate name mapping is empty - something is wrong.");
112+
Deno.exit(1);
113+
}
114+
115+
// Scan the built documentation directory
116+
const docPath = path.join(parsedArgs.w || ".", "target/doc");
117+
118+
for await (const entry of Deno.readDir(docPath)) {
119+
if (entry.isDirectory && entry.name !== "src" && entry.name !== "implementors") {
120+
await processCrateDocumentation(
121+
path.join(docPath, entry.name),
122+
`/${entry.name}`,
123+
entry.name,
124+
);
125+
}
126+
}
127+
for await (const entry of Deno.readDir(path.join(docPath, "src"))) {
128+
if (entry.isDirectory) {
129+
await processCrateDocumentation(
130+
path.join(docPath, "src", entry.name),
131+
`/src/${entry.name}`,
132+
entry.name,
133+
);
134+
}
135+
}
136+
137+
async function processCrateDocumentation(docPath: string, relPath: string, crateName: string) {
138+
const packages = crateNameToPackage.get(crateName) ?? [];
139+
140+
// Ignore non-crates.io packages
141+
if (packages.find(p => p.source !== "registry+https://github.com/rust-lang/crates.io-index")) {
142+
logger.debug(`${docPath}: It might be a non-crates.io package, ignoring`);
143+
return;
144+
}
145+
146+
if (packages.length === 0) {
147+
logger.warning(`${docPath}: Unknown crate, ignoring`);
148+
return;
149+
}
150+
151+
if (crateName.startsWith("r3_")) {
152+
logger.warning(`${docPath}: It starts with 'r3_' but is about to ` +
153+
`be processed - something might be wrong.`);
154+
return;
155+
}
156+
157+
// If there are multiple candidates, choose the one with the highest version
158+
const pkg = packages.reduce((x, y) => semver.gt(x.version, y.version) ? x : y);
159+
if (packages.length > 1) {
160+
const candidates = JSON.stringify(packages.map(p => p.version));
161+
logger.info(`${docPath}: Choosing ${pkg.version} from the candidate(s) ${candidates}`);
162+
}
163+
164+
const externalDocBaseUrl = `https://docs.rs/${pkg.name}/${pkg.version}${relPath}`;
165+
166+
logger.info(`${docPath}: Externalizing the documentation to '${externalDocBaseUrl}'`);
167+
168+
const files = [];
169+
for await (const entry of walk(docPath, { includeDirs: false })) {
170+
if (!entry.name.endsWith('.html')) {
171+
continue;
172+
}
173+
logger.debug(`${docPath}: ${entry.path}`);
174+
if (!entry.path.startsWith(docPath)) {
175+
throw new Error();
176+
}
177+
files.push(entry.path);
178+
}
179+
logger.info(`${docPath}: Replacing ${files.length} file(s) with redirect pages`);
180+
181+
logger.debug(`${docPath}: Removing the directory`);
182+
if (shouldModify) {
183+
await Deno.remove(docPath, { recursive: true });
184+
}
185+
186+
for (const filePath of files) {
187+
const url = externalDocBaseUrl + filePath.substring(docPath.length);
188+
logger.debug(`${docPath}: ${filePath}${url}`);
189+
if (shouldModify) {
190+
await Deno.mkdir(path.dirname(filePath), { recursive: true });
191+
await Deno.writeTextFile(filePath, redirectingHtmlCode(url));
192+
}
193+
}
194+
}
195+
196+
function redirectingHtmlCode(url: string): string {
197+
return `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${url}">`;
198+
}

0 commit comments

Comments
 (0)