1 use std::cell::RefCell;
2 use std::fs::{self, File};
3 use std::io::prelude::*;
4 use std::io::{self, BufReader};
5 use std::path::{Component, Path};
6 use std::rc::{Rc, Weak};
8 use itertools::Itertools;
9 use rustc_data_structures::flock;
10 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
11 use serde::ser::SerializeSeq;
12 use serde::{Serialize, Serializer};
14 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
15 use crate::clean::Crate;
16 use crate::config::{EmitType, RenderOptions};
17 use crate::docfs::PathError;
18 use crate::error::Error;
19 use crate::html::{layout, static_files};
20 use crate::{try_err, try_none};
22 /// Rustdoc writes out two kinds of shared files:
23 /// - Static files, which are embedded in the rustdoc binary and are written with a
24 /// filename that includes a hash of their contents. These will always have a new
25 /// URL if the contents change, so they are safe to cache with the
26 /// `Cache-Control: immutable` directive. They are written under the static.files/
27 /// directory and are written when --emit-type is empty (default) or contains
28 /// "toolchain-specific". If using the --static-root-path flag, it should point
29 /// to a URL path prefix where each of these filenames can be fetched.
30 /// - Invocation specific files. These are generated based on the crate(s) being
31 /// documented. Their filenames need to be predictable without knowing their
32 /// contents, so they do not include a hash in their filename and are not safe to
33 /// cache with `Cache-Control: immutable`. They include the contents of the
34 /// --resource-suffix flag and are emitted when --emit-type is empty (default)
35 /// or contains "invocation-specific".
36 pub(super) fn write_shared(
40 options: &RenderOptions,
41 ) -> Result<(), Error> {
42 // Write out the shared files. Note that these are shared among all rustdoc
43 // docs placed in the output directory, so this needs to be a synchronized
44 // operation with respect to all other rustdocs running around.
45 let lock_file = cx.dst.join(".lock");
46 let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
48 // InvocationSpecific resources should always be dynamic.
49 let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
50 let content = make_content()?;
51 if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) {
52 let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix);
53 cx.shared.fs.write(cx.dst.join(output_filename), content)
61 .create_dir_all(cx.dst.join("static.files"))
62 .map_err(|e| PathError::new(e, "static.files"))?;
64 // Handle added third-party themes
65 for entry in &cx.shared.style_files {
66 let theme = entry.basename()?;
68 try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
70 // Skip the official themes. They are written below as part of STATIC_FILES_LIST.
71 if matches!(theme.as_str(), "light" | "dark" | "ayu") {
75 let bytes = try_err!(fs::read(&entry.path), &entry.path);
76 let filename = format!("{}{}.{}", theme, cx.shared.resource_suffix, extension);
77 cx.shared.fs.write(cx.dst.join(filename), bytes)?;
80 // When the user adds their own CSS files with --extend-css, we write that as an
81 // invocation-specific file (that is, with a resource suffix).
82 if let Some(ref css) = cx.shared.layout.css_file_extension {
83 let buffer = try_err!(fs::read_to_string(css), css);
84 let path = static_files::suffix_path("theme.css", &cx.shared.resource_suffix);
85 cx.shared.fs.write(cx.dst.join(path), buffer)?;
88 if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
89 let static_dir = cx.dst.join(Path::new("static.files"));
90 static_files::for_each(|f: &static_files::StaticFile| {
91 let filename = static_dir.join(f.output_filename());
92 cx.shared.fs.write(filename, f.minified())
96 /// Read a file and return all lines that match the `"{crate}":{data},` format,
97 /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
99 /// This forms the payload of files that look like this:
103 /// "{crate1}":{data},
104 /// "{crate2}":{data}
109 /// The file needs to be formatted so that *only crate data lines start with `"`*.
110 fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
111 let mut ret = Vec::new();
112 let mut krates = Vec::new();
115 let prefix = format!("\"{}\"", krate);
116 for line in BufReader::new(File::open(path)?).lines() {
118 if !line.starts_with('"') {
121 if line.starts_with(&prefix) {
124 if line.ends_with(',') {
125 ret.push(line[..line.len() - 1].to_string());
127 // No comma (it's the case for the last added crate line)
128 ret.push(line.to_string());
132 .find(|s| !s.is_empty())
133 .map(|s| s.to_owned())
134 .unwrap_or_else(String::new),
141 /// Read a file and return all lines that match the <code>"{crate}":{data},\</code> format,
142 /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
144 /// This forms the payload of files that look like this:
147 /// var data = JSON.parse('{\
148 /// "{crate1}":{data},\
149 /// "{crate2}":{data}\
154 /// The file needs to be formatted so that *only crate data lines start with `"`*.
155 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
156 let mut ret = Vec::new();
157 let mut krates = Vec::new();
160 let prefix = format!("\"{}\"", krate);
161 for line in BufReader::new(File::open(path)?).lines() {
163 if !line.starts_with('"') {
166 if line.starts_with(&prefix) {
169 if line.ends_with(",\\") {
170 ret.push(line[..line.len() - 2].to_string());
172 // Ends with "\\" (it's the case for the last added crate line)
173 ret.push(line[..line.len() - 1].to_string());
177 .find(|s| !s.is_empty())
178 .map(|s| s.to_owned())
179 .unwrap_or_else(String::new),
186 use std::ffi::OsString;
188 #[derive(Debug, Default)]
192 children: RefCell<FxHashMap<OsString, Rc<Self>>>,
193 elems: RefCell<FxHashSet<OsString>>,
197 fn with_parent(elem: OsString, parent: &Rc<Self>) -> Self {
198 Self { elem, parent: Rc::downgrade(parent), ..Self::default() }
201 fn to_json_string(&self) -> String {
202 let borrow = self.children.borrow();
203 let mut subs: Vec<_> = borrow.values().collect();
204 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
209 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
210 .collect::<Vec<_>>();
211 files.sort_unstable();
212 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
213 let dirs = if subs.is_empty() && files.is_empty() {
216 format!(",[{}]", subs)
218 let files = files.join(",");
219 let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
221 "[\"{name}\"{dirs}{files}]",
222 name = self.elem.to_str().expect("invalid osstring conversion"),
228 fn add_path(self: &Rc<Self>, path: &Path) {
229 let mut h = Rc::clone(&self);
232 .filter_map(|s| match s {
233 Component::Normal(s) => Some(s.to_owned()),
234 Component::ParentDir => Some(OsString::from("..")),
239 let cur_elem = elems.next().expect("empty file path");
240 if cur_elem == ".." {
241 if let Some(parent) = h.parent.upgrade() {
246 if elems.peek().is_none() {
247 h.elems.borrow_mut().insert(cur_elem);
250 let entry = Rc::clone(
253 .entry(cur_elem.clone())
254 .or_insert_with(|| Rc::new(Self::with_parent(cur_elem, &h))),
262 if cx.include_sources {
263 let hierarchy = Rc::new(Hierarchy::default());
268 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
270 hierarchy.add_path(source);
272 let hierarchy = Rc::try_unwrap(hierarchy).unwrap();
273 let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
274 let make_sources = || {
275 let (mut all_sources, _krates) =
276 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
277 all_sources.push(format!(
279 &krate.name(cx.tcx()),
282 // All these `replace` calls are because we have to go through JS string for JSON content.
283 .replace('\\', r"\\")
284 .replace('\'', r"\'")
285 // We need to escape double quotes for the JSON.
286 .replace("\\\"", "\\\\\"")
289 let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
290 v.push_str(&all_sources.join(",\\\n"));
291 v.push_str("\\\n}');\ncreateSourceSidebar();\n");
294 write_invocation_specific("source-files.js", &make_sources)?;
297 // Update the search index and crate list.
298 let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
299 let (mut all_indexes, mut krates) =
300 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
301 all_indexes.push(search_index);
302 krates.push(krate.name(cx.tcx()).to_string());
305 // Sort the indexes by crate so the file will be generated identically even
306 // with rustdoc running in parallel.
308 write_invocation_specific("search-index.js", &|| {
309 let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
310 v.push_str(&all_indexes.join(",\\\n"));
314 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
315 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
321 write_invocation_specific("crates.js", &|| {
322 let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
323 Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
326 if options.enable_index_page {
327 if let Some(index_page) = options.index_page.clone() {
328 let mut md_opts = options.clone();
329 md_opts.output = cx.dst.clone();
330 md_opts.external_html = (*cx.shared).layout.external_html.clone();
332 crate::markdown::render(&index_page, md_opts, cx.shared.edition())
333 .map_err(|e| Error::new(e, &index_page))?;
335 let shared = Rc::clone(&cx.shared);
336 let dst = cx.dst.join("index.html");
337 let page = layout::Page {
338 title: "Index of crates",
341 static_root_path: shared.static_root_path.as_deref(),
342 description: "List of crates",
343 keywords: BASIC_KEYWORDS,
344 resource_suffix: &shared.resource_suffix,
347 let content = format!(
348 "<h1>List of all crates</h1><ul class=\"all-items\">{}</ul>",
353 "<li><a href=\"{}index.html\">{}</a></li>",
354 ensure_trailing_slash(s),
360 let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
361 shared.fs.write(dst, v)?;
365 // Update the list of all implementors for traits
366 let dst = cx.dst.join("implementors");
367 let cache = cx.cache();
368 for (&did, imps) in &cache.implementors {
369 // Private modules can leak through to this phase of rustdoc, which
370 // could contain implementations for otherwise private types. In some
371 // rare cases we could find an implementation for an item which wasn't
372 // indexed, so we just skip this step in that case.
374 // FIXME: this is a vague explanation for why this can't be a `get`, in
375 // theory it should be...
376 let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
377 Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
378 Some((_, t)) => (p, t),
381 None => match cache.external_paths.get(&did) {
382 Some((p, t)) => (p, t),
393 impl Serialize for Implementor {
394 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
398 let mut seq = serializer.serialize_seq(None)?;
399 seq.serialize_element(&self.text)?;
401 seq.serialize_element(&1)?;
402 seq.serialize_element(&self.types)?;
408 let implementors = imps
411 // If the trait and implementation are in the same crate, then
412 // there's no need to emit information about it (there's inlining
413 // going on). If they're in different crates then the crate defining
414 // the trait will be interested in our implementation.
416 // If the implementation is from another crate then that crate
418 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
422 text: imp.inner_impl().print(false, cx).to_string(),
423 synthetic: imp.inner_impl().kind.is_auto(),
424 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
428 .collect::<Vec<_>>();
430 // Only create a js file if we have impls to add to it. If the trait is
431 // documented locally though we always create the file to avoid dead
433 if implementors.is_empty() && !cache.paths.contains_key(&did) {
437 let implementors = format!(
439 krate.name(cx.tcx()),
440 serde_json::to_string(&implementors).expect("failed serde conversion"),
443 let mut mydst = dst.clone();
444 for part in &remote_path[..remote_path.len() - 1] {
445 mydst.push(part.to_string());
447 cx.shared.ensure_dir(&mydst)?;
448 mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
450 let (mut all_implementors, _) =
451 try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
452 all_implementors.push(implementors);
453 // Sort the implementors by crate so the file will be generated
454 // identically even with rustdoc running in parallel.
455 all_implementors.sort();
457 let mut v = String::from("(function() {var implementors = {\n");
458 v.push_str(&all_implementors.join(",\n"));
461 "if (window.register_implementors) {\
462 window.register_implementors(implementors);\
464 window.pending_implementors = implementors;\
468 cx.shared.fs.write(mydst, v)?;