3 use std
::fs
::{self, File}
;
4 use std
::io
::prelude
::*;
5 use std
::io
::{self, BufReader}
;
6 use std
::lazy
::SyncLazy
as Lazy
;
7 use std
::path
::{Component, Path, PathBuf}
;
9 use itertools
::Itertools
;
10 use rustc_data_structures
::flock
;
11 use rustc_data_structures
::fx
::{FxHashMap, FxHashSet}
;
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}
;
21 static FILES_UNVERSIONED
: Lazy
<FxHashMap
<&str, &[u8]>> = Lazy
::new(|| {
23 "FiraSans-Regular.woff2" => static_files
::fira_sans
::REGULAR2
,
24 "FiraSans-Medium.woff2" => static_files
::fira_sans
::MEDIUM2
,
25 "FiraSans-Regular.woff" => static_files
::fira_sans
::REGULAR
,
26 "FiraSans-Medium.woff" => static_files
::fira_sans
::MEDIUM
,
27 "FiraSans-LICENSE.txt" => static_files
::fira_sans
::LICENSE
,
28 "SourceSerif4-Regular.ttf.woff2" => static_files
::source_serif_4
::REGULAR2
,
29 "SourceSerif4-Bold.ttf.woff2" => static_files
::source_serif_4
::BOLD2
,
30 "SourceSerif4-It.ttf.woff2" => static_files
::source_serif_4
::ITALIC2
,
31 "SourceSerif4-Regular.ttf.woff" => static_files
::source_serif_4
::REGULAR
,
32 "SourceSerif4-Bold.ttf.woff" => static_files
::source_serif_4
::BOLD
,
33 "SourceSerif4-It.ttf.woff" => static_files
::source_serif_4
::ITALIC
,
34 "SourceSerif4-LICENSE.md" => static_files
::source_serif_4
::LICENSE
,
35 "SourceCodePro-Regular.ttf.woff2" => static_files
::source_code_pro
::REGULAR2
,
36 "SourceCodePro-Semibold.ttf.woff2" => static_files
::source_code_pro
::SEMIBOLD2
,
37 "SourceCodePro-It.ttf.woff2" => static_files
::source_code_pro
::ITALIC2
,
38 "SourceCodePro-Regular.ttf.woff" => static_files
::source_code_pro
::REGULAR
,
39 "SourceCodePro-Semibold.ttf.woff" => static_files
::source_code_pro
::SEMIBOLD
,
40 "SourceCodePro-It.ttf.woff" => static_files
::source_code_pro
::ITALIC
,
41 "SourceCodePro-LICENSE.txt" => static_files
::source_code_pro
::LICENSE
,
42 "noto-sans-kr-v13-korean-regular.woff" => static_files
::noto_sans_kr
::REGULAR
,
43 "noto-sans-kr-v13-korean-regular-LICENSE.txt" => static_files
::noto_sans_kr
::LICENSE
,
44 "LICENSE-MIT.txt" => static_files
::LICENSE_MIT
,
45 "LICENSE-APACHE.txt" => static_files
::LICENSE_APACHE
,
46 "COPYRIGHT.txt" => static_files
::COPYRIGHT
,
50 enum SharedResource
<'a
> {
51 /// This file will never change, no matter what toolchain is used to build it.
53 /// It does not have a resource suffix.
54 Unversioned { name: &'static str }
,
55 /// This file may change depending on the toolchain.
57 /// It has a resource suffix.
58 ToolchainSpecific { basename: &'static str }
,
59 /// This file may change for any crate within a build, or based on the CLI arguments.
61 /// This differs from normal invocation-specific files because it has a resource suffix.
62 InvocationSpecific { basename: &'a str }
,
65 impl SharedResource
<'_
> {
66 fn extension(&self) -> Option
<&OsStr
> {
67 use SharedResource
::*;
70 | ToolchainSpecific { basename: name }
71 | InvocationSpecific { basename: name }
=> Path
::new(name
).extension(),
75 fn path(&self, cx
: &Context
<'_
>) -> PathBuf
{
77 SharedResource
::Unversioned { name }
=> cx
.dst
.join(name
),
78 SharedResource
::ToolchainSpecific { basename }
=> cx
.suffix_path(basename
),
79 SharedResource
::InvocationSpecific { basename }
=> cx
.suffix_path(basename
),
83 fn should_emit(&self, emit
: &[EmitType
]) -> bool
{
87 let kind
= match self {
88 SharedResource
::Unversioned { .. }
=> EmitType
::Unversioned
,
89 SharedResource
::ToolchainSpecific { .. }
=> EmitType
::Toolchain
,
90 SharedResource
::InvocationSpecific { .. }
=> EmitType
::InvocationSpecific
,
97 fn suffix_path(&self, filename
: &str) -> PathBuf
{
98 // We use splitn vs Path::extension here because we might get a filename
99 // like `style.min.css` and we want to process that into
100 // `style-suffix.min.css`. Path::extension would just return `css`
101 // which would result in `style.min-suffix.css` which isn't what we
103 let (base
, ext
) = filename
.split_once('
.'
).unwrap();
104 let filename
= format
!("{}{}.{}", base
, self.shared
.resource_suffix
, ext
);
105 self.dst
.join(&filename
)
108 fn write_shared
<C
: AsRef
<[u8]>>(
110 resource
: SharedResource
<'_
>,
113 ) -> Result
<(), Error
> {
114 if resource
.should_emit(emit
) {
115 self.shared
.fs
.write(resource
.path(self), contents
)
123 resource
: SharedResource
<'_
>,
127 ) -> Result
<(), Error
> {
129 let contents
= if minify
{
130 tmp
= if resource
.extension() == Some(&OsStr
::new("css")) {
131 minifier
::css
::minify(contents
).map_err(|e
| {
132 Error
::new(format
!("failed to minify CSS file: {}", e
), resource
.path(self))
135 minifier
::js
::minify(contents
)
142 self.write_shared(resource
, contents
, emit
)
146 pub(super) fn write_shared(
149 search_index
: String
,
150 options
: &RenderOptions
,
151 ) -> Result
<(), Error
> {
152 // Write out the shared files. Note that these are shared among all rustdoc
153 // docs placed in the output directory, so this needs to be a synchronized
154 // operation with respect to all other rustdocs running around.
155 let lock_file
= cx
.dst
.join(".lock");
156 let _lock
= try_err
!(flock
::Lock
::new(&lock_file
, true, true, true), &lock_file
);
158 // The weird `: &_` is to work around a borrowck bug: https://github.com/rust-lang/rust/issues/41078#issuecomment-293646723
159 let write_minify
= |p
, c
: &_
| {
161 SharedResource
::ToolchainSpecific { basename: p }
,
163 options
.enable_minification
,
167 // Toolchain resources should never be dynamic.
168 let write_toolchain
= |p
: &'
static _
, c
: &'
static _
| {
169 cx
.write_shared(SharedResource
::ToolchainSpecific { basename: p }
, c
, &options
.emit
)
172 // Crate resources should always be dynamic.
173 let write_crate
= |p
: &_
, make_content
: &dyn Fn() -> Result
<Vec
<u8>, Error
>| {
174 let content
= make_content()?
;
175 cx
.write_shared(SharedResource
::InvocationSpecific { basename: p }
, content
, &options
.emit
)
178 // Add all the static files. These may already exist, but we just
179 // overwrite them anyway to make sure that they're fresh and up-to-date.
180 write_minify("rustdoc.css", static_files
::RUSTDOC_CSS
)?
;
181 write_minify("settings.css", static_files
::SETTINGS_CSS
)?
;
182 write_minify("noscript.css", static_files
::NOSCRIPT_CSS
)?
;
184 // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
185 // then we'll run over the "official" styles.
186 let mut themes
: FxHashSet
<String
> = FxHashSet
::default();
188 for entry
in &cx
.shared
.style_files
{
189 let theme
= try_none
!(try_none
!(entry
.path
.file_stem(), &entry
.path
).to_str(), &entry
.path
);
191 try_none
!(try_none
!(entry
.path
.extension(), &entry
.path
).to_str(), &entry
.path
);
193 // Handle the official themes
195 "light" => write_minify("light.css", static_files
::themes
::LIGHT
)?
,
196 "dark" => write_minify("dark.css", static_files
::themes
::DARK
)?
,
197 "ayu" => write_minify("ayu.css", static_files
::themes
::AYU
)?
,
199 // Handle added third-party themes
200 let filename
= format
!("{}.{}", theme
, extension
);
201 write_crate(&filename
, &|| Ok(try_err
!(fs
::read(&entry
.path
), &entry
.path
)))?
;
205 themes
.insert(theme
.to_owned());
208 if (*cx
.shared
).layout
.logo
.is_empty() {
209 write_toolchain("rust-logo.png", static_files
::RUST_LOGO
)?
;
211 if (*cx
.shared
).layout
.favicon
.is_empty() {
212 write_toolchain("favicon.svg", static_files
::RUST_FAVICON_SVG
)?
;
213 write_toolchain("favicon-16x16.png", static_files
::RUST_FAVICON_PNG_16
)?
;
214 write_toolchain("favicon-32x32.png", static_files
::RUST_FAVICON_PNG_32
)?
;
216 write_toolchain("brush.svg", static_files
::BRUSH_SVG
)?
;
217 write_toolchain("wheel.svg", static_files
::WHEEL_SVG
)?
;
218 write_toolchain("clipboard.svg", static_files
::CLIPBOARD_SVG
)?
;
219 write_toolchain("down-arrow.svg", static_files
::DOWN_ARROW_SVG
)?
;
221 let mut themes
: Vec
<&String
> = themes
.iter().collect();
224 // FIXME: this should probably not be a toolchain file since it depends on `--theme`.
225 // But it seems a shame to copy it over and over when it's almost always the same.
226 // Maybe we can change the representation to move this out of main.js?
229 &static_files
::MAIN_JS
.replace(
230 "/* INSERT THEMES HERE */",
231 &format
!(" = {}", serde_json
::to_string(&themes
).unwrap()),
234 write_minify("search.js", static_files
::SEARCH_JS
)?
;
235 write_minify("settings.js", static_files
::SETTINGS_JS
)?
;
237 if cx
.shared
.include_sources
{
238 write_minify("source-script.js", static_files
::sidebar
::SOURCE_SCRIPT
)?
;
245 "var resourcesSuffix = \"{}\";{}",
246 cx
.shared
.resource_suffix
,
247 static_files
::STORAGE_JS
252 if let Some(ref css
) = cx
.shared
.layout
.css_file_extension
{
253 let buffer
= try_err
!(fs
::read_to_string(css
), css
);
254 // This varies based on the invocation, so it can't go through the write_minify wrapper.
256 SharedResource
::InvocationSpecific { basename: "theme.css" }
,
258 options
.enable_minification
,
262 write_minify("normalize.css", static_files
::NORMALIZE_CSS
)?
;
263 for (name
, contents
) in &*FILES_UNVERSIONED
{
264 cx
.write_shared(SharedResource
::Unversioned { name }
, contents
, &options
.emit
)?
;
267 fn collect(path
: &Path
, krate
: &str, key
: &str) -> io
::Result
<(Vec
<String
>, Vec
<String
>)> {
268 let mut ret
= Vec
::new();
269 let mut krates
= Vec
::new();
272 let prefix
= format
!(r
#"{}["{}"]"#, key, krate);
273 for line
in BufReader
::new(File
::open(path
)?
).lines() {
275 if !line
.starts_with(key
) {
278 if line
.starts_with(&prefix
) {
281 ret
.push(line
.to_string());
283 line
[key
.len() + 2..]
286 .map(|s| s.to_owned())
287 .unwrap_or_else(String::new),
294 fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
295 let mut ret = Vec::new();
296 let mut krates = Vec::new();
299 let prefix = format!("\"{}
\"", krate);
300 for line in BufReader::new(File::open(path)?).lines() {
302 if !line.starts_with('"'
) {
305 if line
.starts_with(&prefix
) {
308 if line
.ends_with(",\\") {
309 ret
.push(line
[..line
.len() - 2].to_string());
311 // Ends with "\\" (it's the case for the last added crate line)
312 ret
.push(line
[..line
.len() - 1].to_string());
316 .find(|s| !s.is_empty())
317 .map(|s| s.to_owned())
318 .unwrap_or_else(String::new),
325 use std::ffi::OsString;
330 children: FxHashMap<OsString, Hierarchy>,
331 elems: FxHashSet<OsString>,
335 fn new(elem: OsString) -> Hierarchy {
336 Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
339 fn to_json_string(&self) -> String {
340 let mut subs: Vec<&Hierarchy> = self.children.values().collect();
341 subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
345 .map(|s| format!("\"{}
\"", s.to_str().expect("invalid osstring conversion
")))
346 .collect::<Vec<_>>();
347 files.sort_unstable();
348 let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
350 if subs.is_empty() { String::new() } else { format!(",\"dirs\":[{}]", subs
) };
351 let files
= files
.join(",");
353 if files
.is_empty() { String::new() }
else { format!(",\"files\":[{}
]", files) };
355 "{{\"name\":\"{name}
\"{dirs}{files}
}}",
356 name = self.elem.to_str().expect("invalid osstring conversion
"),
363 if cx.shared.include_sources {
364 let mut hierarchy = Hierarchy::new(OsString::new());
369 .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
371 let mut h = &mut hierarchy;
372 let mut elems = source
374 .filter_map(|s| match s {
375 Component::Normal(s) => Some(s.to_owned()),
380 let cur_elem = elems.next().expect("empty file path
");
381 if elems.peek().is_none() {
382 h.elems.insert(cur_elem);
385 let e = cur_elem.clone();
386 h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
391 let dst = cx.dst.join(&format!("source
-files{}
.js
", cx.shared.resource_suffix));
392 let make_sources = || {
393 let (mut all_sources, _krates) =
394 try_err!(collect(&dst, &krate.name.as_str(), "sourcesIndex
"), &dst);
395 all_sources.push(format!(
396 "sourcesIndex
[\"{}
\"] = {}
;",
398 hierarchy.to_json_string()
402 "var N
= null
;var sourcesIndex
= {{}
};\n{}
\ncreateSourceSidebar
();\n",
403 all_sources.join("\n")
407 write_crate("source
-files
.js
", &make_sources)?;
410 // Update the search index and crate list.
411 let dst = cx.dst.join(&format!("search
-index{}
.js
", cx.shared.resource_suffix));
412 let (mut all_indexes, mut krates) = try_err!(collect_json(&dst, &krate.name.as_str()), &dst);
413 all_indexes.push(search_index);
414 krates.push(krate.name.to_string());
417 // Sort the indexes by crate so the file will be generated identically even
418 // with rustdoc running in parallel.
420 write_crate("search
-index
.js
", &|| {
421 let mut v = String::from("var searchIndex
= JSON
.parse('
{\\\n");
422 v.push_str(&all_indexes.join(",\\\n"));
423 v.push_str("\\\n}'
);\nif
(window
.initSearch
) {window.initSearch(searchIndex)}
;");
427 write_crate("crates
.js
", &|| {
428 let krates = krates.iter().map(|k| format!("\"{}
\"", k)).join(",");
429 Ok(format!("window
.ALL_CRATES
= [{}
];", krates).into_bytes())
432 if options.enable_index_page {
433 if let Some(index_page) = options.index_page.clone() {
434 let mut md_opts = options.clone();
435 md_opts.output = cx.dst.clone();
436 md_opts.external_html = (*cx.shared).layout.external_html.clone();
438 crate::markdown::render(&index_page, md_opts, cx.shared.edition())
439 .map_err(|e| Error::new(e, &index_page))?;
441 let dst = cx.dst.join("index
.html
");
442 let page = layout::Page {
443 title: "Index of crates
",
446 static_root_path: cx.shared.static_root_path.as_deref(),
447 description: "List of crates
",
448 keywords: BASIC_KEYWORDS,
449 resource_suffix: &cx.shared.resource_suffix,
451 static_extra_scripts: &[],
454 let content = format!(
456 <span class
=\"in-band
\">List of all crates
</span
>\
457 </h1
><ul class
=\"crate mod\">{}
</ul
>",
462 "<li
><a class
=\"crate mod\" href
=\"{}index
.html
\">{}
</a
></li
>",
463 ensure_trailing_slash(s),
469 let v = layout::render(
470 &cx.shared.templates,
475 &cx.shared.style_files,
477 cx.shared.fs.write(&dst, v.as_bytes())?;
481 // Update the list of all implementors for traits
482 let dst = cx.dst.join("implementors
");
483 for (&did, imps) in &cx.cache.implementors {
484 // Private modules can leak through to this phase of rustdoc, which
485 // could contain implementations for otherwise private types. In some
486 // rare cases we could find an implementation for an item which wasn't
487 // indexed, so we just skip this step in that case.
489 // FIXME: this is a vague explanation for why this can't be a `get`, in
490 // theory it should be...
491 let &(ref remote_path, remote_item_type) = match cx.cache.paths.get(&did) {
493 None => match cx.cache.external_paths.get(&did) {
506 let implementors = imps
509 // If the trait and implementation are in the same crate, then
510 // there's no need to emit information about it (there's inlining
511 // going on). If they're in different crates then the crate defining
512 // the trait will be interested in our implementation.
514 // If the implementation is from another crate then that crate
516 if imp.impl_item.def_id.krate() == did.krate || !imp.impl_item.def_id.is_local() {
520 text: imp.inner_impl().print(false, cx).to_string(),
521 synthetic: imp.inner_impl().synthetic,
522 types: collect_paths_for_type(imp.inner_impl().for_.clone(), cx.cache()),
526 .collect::<Vec<_>>();
528 // Only create a js file if we have impls to add to it. If the trait is
529 // documented locally though we always create the file to avoid dead
531 if implementors.is_empty() && !cx.cache.paths.contains_key(&did) {
535 let implementors = format!(
536 r#"implementors
["{}"] = {}
;"#,
538 serde_json::to_string(&implementors).unwrap()
541 let mut mydst = dst.clone();
542 for part in &remote_path[..remote_path.len() - 1] {
545 cx.shared.ensure_dir(&mydst)?;
546 mydst.push(&format!("{}
.{}
.js
", remote_item_type, remote_path[remote_path.len() - 1]));
548 let (mut all_implementors, _) =
549 try_err!(collect(&mydst, &krate.name.as_str(), "implementors
"), &mydst);
550 all_implementors.push(implementors);
551 // Sort the implementors by crate so the file will be generated
552 // identically even with rustdoc running in parallel.
553 all_implementors.sort();
555 let mut v = String::from("(function() {var implementors = {}
;\n");
556 for implementor in &all_implementors {
557 writeln!(v, "{}
", *implementor).unwrap();
560 "if (window
.register_implementors
) {\
561 window
.register_implementors(implementors
);\
563 window
.pending_implementors
= implementors
;\
567 cx.shared.fs.write(&mydst, &v)?;