]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
rustdoc: add hash to filename of toolchain files
[rust.git] / src / librustdoc / html / render / write_shared.rs
1 use std::fs::{self, File};
2 use std::io::prelude::*;
3 use std::io::{self, BufReader};
4 use std::path::{Component, Path};
5 use std::rc::Rc;
6
7 use itertools::Itertools;
8 use rustc_data_structures::flock;
9 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
10 use serde::ser::SerializeSeq;
11 use serde::{Serialize, Serializer};
12
13 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
14 use crate::clean::Crate;
15 use crate::config::{EmitType, RenderOptions};
16 use crate::docfs::PathError;
17 use crate::error::Error;
18 use crate::html::{layout, static_files};
19 use crate::{try_err, try_none};
20
21 /// Rustdoc writes out two kinds of shared files:
22 ///  - Static files, which are embedded in the rustdoc binary and are written with a
23 ///    filename that includes a hash of their contents. These will always have a new
24 ///    URL if the contents change, so they are safe to cache with the
25 ///    `Cache-Control: immutable` directive. They are written under the static.files/
26 ///    directory and are written when --emit-type is empty (default) or contains
27 ///    "toolchain-specific".
28 ///  - Invocation specific files. These are generated based on the crate(s) being
29 ///    documented. Their filenames need to be predictable without knowing their
30 ///    contents, so they do not include a hash in their filename and are not safe to
31 ///    cache with `Cache-Control: immutable`. They include the contents of the
32 ///    --resource-suffix flag and are emitted when --emit-type is empty (default)
33 ///    or contains "invocation-specific".
34 pub(super) fn write_shared(
35     cx: &mut Context<'_>,
36     krate: &Crate,
37     search_index: String,
38     options: &RenderOptions,
39 ) -> Result<(), Error> {
40     // Write out the shared files. Note that these are shared among all rustdoc
41     // docs placed in the output directory, so this needs to be a synchronized
42     // operation with respect to all other rustdocs running around.
43     let lock_file = cx.dst.join(".lock");
44     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
45
46     // InvocationSpecific resources should always be dynamic.
47     let write_invocation_specific = |p: &str, make_content: &dyn Fn() -> Result<Vec<u8>, Error>| {
48         let content = make_content()?;
49         if options.emit.is_empty() || options.emit.contains(&EmitType::InvocationSpecific) {
50             let output_filename = static_files::suffix_path(p, &cx.shared.resource_suffix);
51             cx.shared.fs.write(cx.dst.join(output_filename), content)
52         } else {
53             Ok(())
54         }
55     };
56
57     cx.shared
58         .fs
59         .create_dir_all(cx.dst.join("static.files"))
60         .map_err(|e| PathError::new(e, "static.files"))?;
61
62     // Handle added third-party themes
63     for entry in &cx.shared.style_files {
64         let theme = entry.basename()?;
65         let extension =
66             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
67
68         // Skip the official themes. They are written below as part of STATIC_FILES_LIST.
69         if matches!(theme.as_str(), "light" | "dark" | "ayu") {
70             continue;
71         }
72
73         let bytes = try_err!(fs::read(&entry.path), &entry.path);
74         let filename = format!("{}{}.{}", theme, cx.shared.resource_suffix, extension);
75         cx.shared.fs.write(cx.dst.join(filename), bytes)?;
76     }
77
78     // When the user adds their own CSS files with --extend-css, we write that as an
79     // invocation-specific file (that is, with a resource suffix).
80     if let Some(ref css) = cx.shared.layout.css_file_extension {
81         let buffer = try_err!(fs::read_to_string(css), css);
82         let path = static_files::suffix_path("theme.css", &cx.shared.resource_suffix);
83         cx.shared.fs.write(cx.dst.join(path), buffer)?;
84     }
85
86     if options.emit.is_empty() || options.emit.contains(&EmitType::Toolchain) {
87         for f in static_files::STATIC_FILES_LIST {
88             let filename = static_files::static_filename(f.filename, f.bytes);
89             cx.shared.fs.write(cx.dst.join(filename), f.minified())?;
90         }
91     }
92
93     /// Read a file and return all lines that match the `"{crate}":{data},` format,
94     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
95     ///
96     /// This forms the payload of files that look like this:
97     ///
98     /// ```javascript
99     /// var data = {
100     /// "{crate1}":{data},
101     /// "{crate2}":{data}
102     /// };
103     /// use_data(data);
104     /// ```
105     ///
106     /// The file needs to be formatted so that *only crate data lines start with `"`*.
107     fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
108         let mut ret = Vec::new();
109         let mut krates = Vec::new();
110
111         if path.exists() {
112             let prefix = format!("\"{}\"", krate);
113             for line in BufReader::new(File::open(path)?).lines() {
114                 let line = line?;
115                 if !line.starts_with('"') {
116                     continue;
117                 }
118                 if line.starts_with(&prefix) {
119                     continue;
120                 }
121                 if line.ends_with(',') {
122                     ret.push(line[..line.len() - 1].to_string());
123                 } else {
124                     // No comma (it's the case for the last added crate line)
125                     ret.push(line.to_string());
126                 }
127                 krates.push(
128                     line.split('"')
129                         .find(|s| !s.is_empty())
130                         .map(|s| s.to_owned())
131                         .unwrap_or_else(String::new),
132                 );
133             }
134         }
135         Ok((ret, krates))
136     }
137
138     /// Read a file and return all lines that match the <code>"{crate}":{data},\</code> format,
139     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
140     ///
141     /// This forms the payload of files that look like this:
142     ///
143     /// ```javascript
144     /// var data = JSON.parse('{\
145     /// "{crate1}":{data},\
146     /// "{crate2}":{data}\
147     /// }');
148     /// use_data(data);
149     /// ```
150     ///
151     /// The file needs to be formatted so that *only crate data lines start with `"`*.
152     fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
153         let mut ret = Vec::new();
154         let mut krates = Vec::new();
155
156         if path.exists() {
157             let prefix = format!("\"{}\"", krate);
158             for line in BufReader::new(File::open(path)?).lines() {
159                 let line = line?;
160                 if !line.starts_with('"') {
161                     continue;
162                 }
163                 if line.starts_with(&prefix) {
164                     continue;
165                 }
166                 if line.ends_with(",\\") {
167                     ret.push(line[..line.len() - 2].to_string());
168                 } else {
169                     // Ends with "\\" (it's the case for the last added crate line)
170                     ret.push(line[..line.len() - 1].to_string());
171                 }
172                 krates.push(
173                     line.split('"')
174                         .find(|s| !s.is_empty())
175                         .map(|s| s.to_owned())
176                         .unwrap_or_else(String::new),
177                 );
178             }
179         }
180         Ok((ret, krates))
181     }
182
183     use std::ffi::OsString;
184
185     #[derive(Debug)]
186     struct Hierarchy {
187         elem: OsString,
188         children: FxHashMap<OsString, Hierarchy>,
189         elems: FxHashSet<OsString>,
190     }
191
192     impl Hierarchy {
193         fn new(elem: OsString) -> Hierarchy {
194             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
195         }
196
197         fn to_json_string(&self) -> String {
198             let mut subs: Vec<&Hierarchy> = self.children.values().collect();
199             subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
200             let mut files = self
201                 .elems
202                 .iter()
203                 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
204                 .collect::<Vec<_>>();
205             files.sort_unstable();
206             let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
207             let dirs = if subs.is_empty() && files.is_empty() {
208                 String::new()
209             } else {
210                 format!(",[{}]", subs)
211             };
212             let files = files.join(",");
213             let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
214             format!(
215                 "[\"{name}\"{dirs}{files}]",
216                 name = self.elem.to_str().expect("invalid osstring conversion"),
217                 dirs = dirs,
218                 files = files
219             )
220         }
221     }
222
223     if cx.include_sources {
224         let mut hierarchy = Hierarchy::new(OsString::new());
225         for source in cx
226             .shared
227             .local_sources
228             .iter()
229             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
230         {
231             let mut h = &mut hierarchy;
232             let mut elems = source
233                 .components()
234                 .filter_map(|s| match s {
235                     Component::Normal(s) => Some(s.to_owned()),
236                     _ => None,
237                 })
238                 .peekable();
239             loop {
240                 let cur_elem = elems.next().expect("empty file path");
241                 if elems.peek().is_none() {
242                     h.elems.insert(cur_elem);
243                     break;
244                 } else {
245                     let e = cur_elem.clone();
246                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
247                 }
248             }
249         }
250
251         let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
252         let make_sources = || {
253             let (mut all_sources, _krates) =
254                 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
255             all_sources.push(format!(
256                 r#""{}":{}"#,
257                 &krate.name(cx.tcx()),
258                 hierarchy
259                     .to_json_string()
260                     // All these `replace` calls are because we have to go through JS string for JSON content.
261                     .replace('\\', r"\\")
262                     .replace('\'', r"\'")
263                     // We need to escape double quotes for the JSON.
264                     .replace("\\\"", "\\\\\"")
265             ));
266             all_sources.sort();
267             let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
268             v.push_str(&all_sources.join(",\\\n"));
269             v.push_str("\\\n}');\ncreateSourceSidebar();\n");
270             Ok(v.into_bytes())
271         };
272         write_invocation_specific("source-files.js", &make_sources)?;
273     }
274
275     // Update the search index and crate list.
276     let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
277     let (mut all_indexes, mut krates) =
278         try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
279     all_indexes.push(search_index);
280     krates.push(krate.name(cx.tcx()).to_string());
281     krates.sort();
282
283     // Sort the indexes by crate so the file will be generated identically even
284     // with rustdoc running in parallel.
285     all_indexes.sort();
286     write_invocation_specific("search-index.js", &|| {
287         let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
288         v.push_str(&all_indexes.join(",\\\n"));
289         v.push_str(
290             r#"\
291 }');
292 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
293 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
294 "#,
295         );
296         Ok(v.into_bytes())
297     })?;
298
299     write_invocation_specific("crates.js", &|| {
300         let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
301         Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
302     })?;
303
304     if options.enable_index_page {
305         if let Some(index_page) = options.index_page.clone() {
306             let mut md_opts = options.clone();
307             md_opts.output = cx.dst.clone();
308             md_opts.external_html = (*cx.shared).layout.external_html.clone();
309
310             crate::markdown::render(&index_page, md_opts, cx.shared.edition())
311                 .map_err(|e| Error::new(e, &index_page))?;
312         } else {
313             let shared = Rc::clone(&cx.shared);
314             let dst = cx.dst.join("index.html");
315             let page = layout::Page {
316                 title: "Index of crates",
317                 css_class: "mod",
318                 root_path: "./",
319                 static_root_path: shared.static_root_path.as_deref(),
320                 description: "List of crates",
321                 keywords: BASIC_KEYWORDS,
322                 resource_suffix: &shared.resource_suffix,
323             };
324
325             let content = format!(
326                 "<h1 class=\"fqn\">List of all crates</h1><ul class=\"all-items\">{}</ul>",
327                 krates
328                     .iter()
329                     .map(|s| {
330                         format!(
331                             "<li><a href=\"{}index.html\">{}</a></li>",
332                             ensure_trailing_slash(s),
333                             s
334                         )
335                     })
336                     .collect::<String>()
337             );
338             let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
339             shared.fs.write(dst, v)?;
340         }
341     }
342
343     // Update the list of all implementors for traits
344     let dst = cx.dst.join("implementors");
345     let cache = cx.cache();
346     for (&did, imps) in &cache.implementors {
347         // Private modules can leak through to this phase of rustdoc, which
348         // could contain implementations for otherwise private types. In some
349         // rare cases we could find an implementation for an item which wasn't
350         // indexed, so we just skip this step in that case.
351         //
352         // FIXME: this is a vague explanation for why this can't be a `get`, in
353         //        theory it should be...
354         let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
355             Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
356                 Some((_, t)) => (p, t),
357                 None => continue,
358             },
359             None => match cache.external_paths.get(&did) {
360                 Some((p, t)) => (p, t),
361                 None => continue,
362             },
363         };
364
365         struct Implementor {
366             text: String,
367             synthetic: bool,
368             types: Vec<String>,
369         }
370
371         impl Serialize for Implementor {
372             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
373             where
374                 S: Serializer,
375             {
376                 let mut seq = serializer.serialize_seq(None)?;
377                 seq.serialize_element(&self.text)?;
378                 if self.synthetic {
379                     seq.serialize_element(&1)?;
380                     seq.serialize_element(&self.types)?;
381                 }
382                 seq.end()
383             }
384         }
385
386         let implementors = imps
387             .iter()
388             .filter_map(|imp| {
389                 // If the trait and implementation are in the same crate, then
390                 // there's no need to emit information about it (there's inlining
391                 // going on). If they're in different crates then the crate defining
392                 // the trait will be interested in our implementation.
393                 //
394                 // If the implementation is from another crate then that crate
395                 // should add it.
396                 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
397                     None
398                 } else {
399                     Some(Implementor {
400                         text: imp.inner_impl().print(false, cx).to_string(),
401                         synthetic: imp.inner_impl().kind.is_auto(),
402                         types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
403                     })
404                 }
405             })
406             .collect::<Vec<_>>();
407
408         // Only create a js file if we have impls to add to it. If the trait is
409         // documented locally though we always create the file to avoid dead
410         // links.
411         if implementors.is_empty() && !cache.paths.contains_key(&did) {
412             continue;
413         }
414
415         let implementors = format!(
416             r#""{}":{}"#,
417             krate.name(cx.tcx()),
418             serde_json::to_string(&implementors).expect("failed serde conversion"),
419         );
420
421         let mut mydst = dst.clone();
422         for part in &remote_path[..remote_path.len() - 1] {
423             mydst.push(part.to_string());
424         }
425         cx.shared.ensure_dir(&mydst)?;
426         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
427
428         let (mut all_implementors, _) =
429             try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
430         all_implementors.push(implementors);
431         // Sort the implementors by crate so the file will be generated
432         // identically even with rustdoc running in parallel.
433         all_implementors.sort();
434
435         let mut v = String::from("(function() {var implementors = {\n");
436         v.push_str(&all_implementors.join(",\n"));
437         v.push_str("\n};");
438         v.push_str(
439             "if (window.register_implementors) {\
440                  window.register_implementors(implementors);\
441              } else {\
442                  window.pending_implementors = implementors;\
443              }",
444         );
445         v.push_str("})()");
446         cx.shared.fs.write(mydst, v)?;
447     }
448     Ok(())
449 }