]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/html/render/write_shared.rs
85f63c985b37663d1e7d42137de337cac8ba1aed
[rust.git] / src / librustdoc / html / render / write_shared.rs
1 use std::ffi::OsStr;
2 use std::fs::{self, File};
3 use std::io::prelude::*;
4 use std::io::{self, BufReader};
5 use std::path::{Component, Path, PathBuf};
6 use std::rc::Rc;
7 use std::sync::LazyLock as Lazy;
8
9 use itertools::Itertools;
10 use rustc_data_structures::flock;
11 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
12 use serde::ser::SerializeSeq;
13 use serde::{Serialize, Serializer};
14
15 use super::{collect_paths_for_type, ensure_trailing_slash, Context, BASIC_KEYWORDS};
16 use crate::clean::Crate;
17 use crate::config::{EmitType, RenderOptions};
18 use crate::docfs::PathError;
19 use crate::error::Error;
20 use crate::html::{layout, static_files};
21 use crate::{try_err, try_none};
22
23 static FILES_UNVERSIONED: Lazy<FxHashMap<&str, &[u8]>> = Lazy::new(|| {
24     map! {
25         "FiraSans-Regular.woff2" => static_files::fira_sans::REGULAR,
26         "FiraSans-Medium.woff2" => static_files::fira_sans::MEDIUM,
27         "FiraSans-LICENSE.txt" => static_files::fira_sans::LICENSE,
28         "SourceSerif4-Regular.ttf.woff2" => static_files::source_serif_4::REGULAR,
29         "SourceSerif4-Bold.ttf.woff2" => static_files::source_serif_4::BOLD,
30         "SourceSerif4-It.ttf.woff2" => static_files::source_serif_4::ITALIC,
31         "SourceSerif4-LICENSE.md" => static_files::source_serif_4::LICENSE,
32         "SourceCodePro-Regular.ttf.woff2" => static_files::source_code_pro::REGULAR,
33         "SourceCodePro-Semibold.ttf.woff2" => static_files::source_code_pro::SEMIBOLD,
34         "SourceCodePro-It.ttf.woff2" => static_files::source_code_pro::ITALIC,
35         "SourceCodePro-LICENSE.txt" => static_files::source_code_pro::LICENSE,
36         "NanumBarunGothic.ttf.woff2" => static_files::nanum_barun_gothic::REGULAR,
37         "NanumBarunGothic-LICENSE.txt" => static_files::nanum_barun_gothic::LICENSE,
38         "LICENSE-MIT.txt" => static_files::LICENSE_MIT,
39         "LICENSE-APACHE.txt" => static_files::LICENSE_APACHE,
40         "COPYRIGHT.txt" => static_files::COPYRIGHT,
41     }
42 });
43
44 enum SharedResource<'a> {
45     /// This file will never change, no matter what toolchain is used to build it.
46     ///
47     /// It does not have a resource suffix.
48     Unversioned { name: &'static str },
49     /// This file may change depending on the toolchain.
50     ///
51     /// It has a resource suffix.
52     ToolchainSpecific { basename: &'static str },
53     /// This file may change for any crate within a build, or based on the CLI arguments.
54     ///
55     /// This differs from normal invocation-specific files because it has a resource suffix.
56     InvocationSpecific { basename: &'a str },
57 }
58
59 impl SharedResource<'_> {
60     fn extension(&self) -> Option<&OsStr> {
61         use SharedResource::*;
62         match self {
63             Unversioned { name }
64             | ToolchainSpecific { basename: name }
65             | InvocationSpecific { basename: name } => Path::new(name).extension(),
66         }
67     }
68
69     fn path(&self, cx: &Context<'_>) -> PathBuf {
70         match self {
71             SharedResource::Unversioned { name } => cx.dst.join(name),
72             SharedResource::ToolchainSpecific { basename } => cx.suffix_path(basename),
73             SharedResource::InvocationSpecific { basename } => cx.suffix_path(basename),
74         }
75     }
76
77     fn should_emit(&self, emit: &[EmitType]) -> bool {
78         if emit.is_empty() {
79             return true;
80         }
81         let kind = match self {
82             SharedResource::Unversioned { .. } => EmitType::Unversioned,
83             SharedResource::ToolchainSpecific { .. } => EmitType::Toolchain,
84             SharedResource::InvocationSpecific { .. } => EmitType::InvocationSpecific,
85         };
86         emit.contains(&kind)
87     }
88 }
89
90 impl Context<'_> {
91     fn suffix_path(&self, filename: &str) -> PathBuf {
92         // We use splitn vs Path::extension here because we might get a filename
93         // like `style.min.css` and we want to process that into
94         // `style-suffix.min.css`.  Path::extension would just return `css`
95         // which would result in `style.min-suffix.css` which isn't what we
96         // want.
97         let (base, ext) = filename.split_once('.').unwrap();
98         let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
99         self.dst.join(&filename)
100     }
101
102     fn write_shared(
103         &self,
104         resource: SharedResource<'_>,
105         contents: impl 'static + Send + AsRef<[u8]>,
106         emit: &[EmitType],
107     ) -> Result<(), Error> {
108         if resource.should_emit(emit) {
109             self.shared.fs.write(resource.path(self), contents)
110         } else {
111             Ok(())
112         }
113     }
114
115     fn write_minify(
116         &self,
117         resource: SharedResource<'_>,
118         contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
119         minify: bool,
120         emit: &[EmitType],
121     ) -> Result<(), Error> {
122         if minify {
123             let contents = contents.as_ref();
124             let contents = if resource.extension() == Some(OsStr::new("css")) {
125                 minifier::css::minify(contents)
126                     .map_err(|e| {
127                         Error::new(format!("failed to minify CSS file: {}", e), resource.path(self))
128                     })?
129                     .to_string()
130             } else {
131                 minifier::js::minify(contents).to_string()
132             };
133             self.write_shared(resource, contents, emit)
134         } else {
135             self.write_shared(resource, contents, emit)
136         }
137     }
138 }
139
140 pub(super) fn write_shared(
141     cx: &mut Context<'_>,
142     krate: &Crate,
143     search_index: String,
144     options: &RenderOptions,
145 ) -> Result<(), Error> {
146     // Write out the shared files. Note that these are shared among all rustdoc
147     // docs placed in the output directory, so this needs to be a synchronized
148     // operation with respect to all other rustdocs running around.
149     let lock_file = cx.dst.join(".lock");
150     let _lock = try_err!(flock::Lock::new(&lock_file, true, true, true), &lock_file);
151
152     // Minified resources are usually toolchain resources. If they're not, they should use `cx.write_minify` directly.
153     fn write_minify(
154         basename: &'static str,
155         contents: impl 'static + Send + AsRef<str> + AsRef<[u8]>,
156         cx: &Context<'_>,
157         options: &RenderOptions,
158     ) -> Result<(), Error> {
159         cx.write_minify(
160             SharedResource::ToolchainSpecific { basename },
161             contents,
162             options.enable_minification,
163             &options.emit,
164         )
165     }
166
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)
170     };
171
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)
176     };
177
178     // Given "foo.svg", return e.g. "url(\"foo1.58.0.svg\")"
179     fn ver_url(cx: &Context<'_>, basename: &'static str) -> String {
180         format!(
181             "url(\"{}\")",
182             SharedResource::ToolchainSpecific { basename }
183                 .path(cx)
184                 .file_name()
185                 .unwrap()
186                 .to_str()
187                 .unwrap()
188         )
189     }
190
191     // We use the AUTOREPLACE mechanism to inject into our static JS and CSS certain
192     // values that are only known at doc build time. Since this mechanism is somewhat
193     // surprising when reading the code, please limit it to rustdoc.css.
194     write_minify(
195         "rustdoc.css",
196         static_files::RUSTDOC_CSS
197             .replace(
198                 "/* AUTOREPLACE: */url(\"toggle-minus.svg\")",
199                 &ver_url(cx, "toggle-minus.svg"),
200             )
201             .replace("/* AUTOREPLACE: */url(\"toggle-plus.svg\")", &ver_url(cx, "toggle-plus.svg"))
202             .replace("/* AUTOREPLACE: */url(\"down-arrow.svg\")", &ver_url(cx, "down-arrow.svg")),
203         cx,
204         options,
205     )?;
206
207     // Add all the static files. These may already exist, but we just
208     // overwrite them anyway to make sure that they're fresh and up-to-date.
209     write_minify("settings.css", static_files::SETTINGS_CSS, cx, options)?;
210     write_minify("noscript.css", static_files::NOSCRIPT_CSS, cx, options)?;
211
212     // To avoid "light.css" to be overwritten, we'll first run over the received themes and only
213     // then we'll run over the "official" styles.
214     let mut themes: FxHashSet<String> = FxHashSet::default();
215
216     for entry in &cx.shared.style_files {
217         let theme = entry.basename()?;
218         let extension =
219             try_none!(try_none!(entry.path.extension(), &entry.path).to_str(), &entry.path);
220
221         // Handle the official themes
222         match theme.as_str() {
223             "light" => write_minify("light.css", static_files::themes::LIGHT, cx, options)?,
224             "dark" => write_minify("dark.css", static_files::themes::DARK, cx, options)?,
225             "ayu" => write_minify("ayu.css", static_files::themes::AYU, cx, options)?,
226             _ => {
227                 // Handle added third-party themes
228                 let filename = format!("{}.{}", theme, extension);
229                 write_crate(&filename, &|| Ok(try_err!(fs::read(&entry.path), &entry.path)))?;
230             }
231         };
232
233         themes.insert(theme.to_owned());
234     }
235
236     if (*cx.shared).layout.logo.is_empty() {
237         write_toolchain("rust-logo.svg", static_files::RUST_LOGO_SVG)?;
238     }
239     if (*cx.shared).layout.favicon.is_empty() {
240         write_toolchain("favicon.svg", static_files::RUST_FAVICON_SVG)?;
241         write_toolchain("favicon-16x16.png", static_files::RUST_FAVICON_PNG_16)?;
242         write_toolchain("favicon-32x32.png", static_files::RUST_FAVICON_PNG_32)?;
243     }
244     write_toolchain("wheel.svg", static_files::WHEEL_SVG)?;
245     write_toolchain("clipboard.svg", static_files::CLIPBOARD_SVG)?;
246     write_toolchain("down-arrow.svg", static_files::DOWN_ARROW_SVG)?;
247     write_toolchain("toggle-minus.svg", static_files::TOGGLE_MINUS_PNG)?;
248     write_toolchain("toggle-plus.svg", static_files::TOGGLE_PLUS_PNG)?;
249
250     let mut themes: Vec<&String> = themes.iter().collect();
251     themes.sort();
252
253     write_minify("main.js", static_files::MAIN_JS, cx, options)?;
254     write_minify("search.js", static_files::SEARCH_JS, cx, options)?;
255     write_minify("settings.js", static_files::SETTINGS_JS, cx, options)?;
256
257     if cx.include_sources {
258         write_minify("source-script.js", static_files::sidebar::SOURCE_SCRIPT, cx, options)?;
259     }
260
261     write_minify("storage.js", static_files::STORAGE_JS, cx, options)?;
262
263     if cx.shared.layout.scrape_examples_extension {
264         cx.write_minify(
265             SharedResource::InvocationSpecific { basename: "scrape-examples.js" },
266             static_files::SCRAPE_EXAMPLES_JS,
267             options.enable_minification,
268             &options.emit,
269         )?;
270     }
271
272     if let Some(ref css) = cx.shared.layout.css_file_extension {
273         let buffer = try_err!(fs::read_to_string(css), css);
274         // This varies based on the invocation, so it can't go through the write_minify wrapper.
275         cx.write_minify(
276             SharedResource::InvocationSpecific { basename: "theme.css" },
277             buffer,
278             options.enable_minification,
279             &options.emit,
280         )?;
281     }
282     write_minify("normalize.css", static_files::NORMALIZE_CSS, cx, options)?;
283     for (name, contents) in &*FILES_UNVERSIONED {
284         cx.write_shared(SharedResource::Unversioned { name }, contents, &options.emit)?;
285     }
286
287     /// Read a file and return all lines that match the `"{crate}":{data},` format,
288     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
289     ///
290     /// This forms the payload of files that look like this:
291     ///
292     /// ```javascript
293     /// var data = {
294     /// "{crate1}":{data},
295     /// "{crate2}":{data}
296     /// };
297     /// use_data(data);
298     /// ```
299     ///
300     /// The file needs to be formatted so that *only crate data lines start with `"`*.
301     fn collect(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
302         let mut ret = Vec::new();
303         let mut krates = Vec::new();
304
305         if path.exists() {
306             let prefix = format!("\"{}\"", krate);
307             for line in BufReader::new(File::open(path)?).lines() {
308                 let line = line?;
309                 if !line.starts_with('"') {
310                     continue;
311                 }
312                 if line.starts_with(&prefix) {
313                     continue;
314                 }
315                 if line.ends_with(',') {
316                     ret.push(line[..line.len() - 1].to_string());
317                 } else {
318                     // No comma (it's the case for the last added crate line)
319                     ret.push(line.to_string());
320                 }
321                 krates.push(
322                     line.split('"')
323                         .find(|s| !s.is_empty())
324                         .map(|s| s.to_owned())
325                         .unwrap_or_else(String::new),
326                 );
327             }
328         }
329         Ok((ret, krates))
330     }
331
332     /// Read a file and return all lines that match the <code>"{crate}":{data},\</code> format,
333     /// and return a tuple `(Vec<DataString>, Vec<CrateNameString>)`.
334     ///
335     /// This forms the payload of files that look like this:
336     ///
337     /// ```javascript
338     /// var data = JSON.parse('{\
339     /// "{crate1}":{data},\
340     /// "{crate2}":{data}\
341     /// }');
342     /// use_data(data);
343     /// ```
344     ///
345     /// The file needs to be formatted so that *only crate data lines start with `"`*.
346     fn collect_json(path: &Path, krate: &str) -> io::Result<(Vec<String>, Vec<String>)> {
347         let mut ret = Vec::new();
348         let mut krates = Vec::new();
349
350         if path.exists() {
351             let prefix = format!("\"{}\"", krate);
352             for line in BufReader::new(File::open(path)?).lines() {
353                 let line = line?;
354                 if !line.starts_with('"') {
355                     continue;
356                 }
357                 if line.starts_with(&prefix) {
358                     continue;
359                 }
360                 if line.ends_with(",\\") {
361                     ret.push(line[..line.len() - 2].to_string());
362                 } else {
363                     // Ends with "\\" (it's the case for the last added crate line)
364                     ret.push(line[..line.len() - 1].to_string());
365                 }
366                 krates.push(
367                     line.split('"')
368                         .find(|s| !s.is_empty())
369                         .map(|s| s.to_owned())
370                         .unwrap_or_else(String::new),
371                 );
372             }
373         }
374         Ok((ret, krates))
375     }
376
377     use std::ffi::OsString;
378
379     #[derive(Debug)]
380     struct Hierarchy {
381         elem: OsString,
382         children: FxHashMap<OsString, Hierarchy>,
383         elems: FxHashSet<OsString>,
384     }
385
386     impl Hierarchy {
387         fn new(elem: OsString) -> Hierarchy {
388             Hierarchy { elem, children: FxHashMap::default(), elems: FxHashSet::default() }
389         }
390
391         fn to_json_string(&self) -> String {
392             let mut subs: Vec<&Hierarchy> = self.children.values().collect();
393             subs.sort_unstable_by(|a, b| a.elem.cmp(&b.elem));
394             let mut files = self
395                 .elems
396                 .iter()
397                 .map(|s| format!("\"{}\"", s.to_str().expect("invalid osstring conversion")))
398                 .collect::<Vec<_>>();
399             files.sort_unstable();
400             let subs = subs.iter().map(|s| s.to_json_string()).collect::<Vec<_>>().join(",");
401             let dirs = if subs.is_empty() && files.is_empty() {
402                 String::new()
403             } else {
404                 format!(",[{}]", subs)
405             };
406             let files = files.join(",");
407             let files = if files.is_empty() { String::new() } else { format!(",[{}]", files) };
408             format!(
409                 "[\"{name}\"{dirs}{files}]",
410                 name = self.elem.to_str().expect("invalid osstring conversion"),
411                 dirs = dirs,
412                 files = files
413             )
414         }
415     }
416
417     if cx.include_sources {
418         let mut hierarchy = Hierarchy::new(OsString::new());
419         for source in cx
420             .shared
421             .local_sources
422             .iter()
423             .filter_map(|p| p.0.strip_prefix(&cx.shared.src_root).ok())
424         {
425             let mut h = &mut hierarchy;
426             let mut elems = source
427                 .components()
428                 .filter_map(|s| match s {
429                     Component::Normal(s) => Some(s.to_owned()),
430                     _ => None,
431                 })
432                 .peekable();
433             loop {
434                 let cur_elem = elems.next().expect("empty file path");
435                 if elems.peek().is_none() {
436                     h.elems.insert(cur_elem);
437                     break;
438                 } else {
439                     let e = cur_elem.clone();
440                     h = h.children.entry(cur_elem.clone()).or_insert_with(|| Hierarchy::new(e));
441                 }
442             }
443         }
444
445         let dst = cx.dst.join(&format!("source-files{}.js", cx.shared.resource_suffix));
446         let make_sources = || {
447             let (mut all_sources, _krates) =
448                 try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
449             all_sources.push(format!(
450                 r#""{}":{}"#,
451                 &krate.name(cx.tcx()),
452                 hierarchy
453                     .to_json_string()
454                     // All these `replace` calls are because we have to go through JS string for JSON content.
455                     .replace('\\', r"\\")
456                     .replace('\'', r"\'")
457                     // We need to escape double quotes for the JSON.
458                     .replace("\\\"", "\\\\\"")
459             ));
460             all_sources.sort();
461             let mut v = String::from("var sourcesIndex = JSON.parse('{\\\n");
462             v.push_str(&all_sources.join(",\\\n"));
463             v.push_str("\\\n}');\ncreateSourceSidebar();\n");
464             Ok(v.into_bytes())
465         };
466         write_crate("source-files.js", &make_sources)?;
467     }
468
469     // Update the search index and crate list.
470     let dst = cx.dst.join(&format!("search-index{}.js", cx.shared.resource_suffix));
471     let (mut all_indexes, mut krates) =
472         try_err!(collect_json(&dst, krate.name(cx.tcx()).as_str()), &dst);
473     all_indexes.push(search_index);
474     krates.push(krate.name(cx.tcx()).to_string());
475     krates.sort();
476
477     // Sort the indexes by crate so the file will be generated identically even
478     // with rustdoc running in parallel.
479     all_indexes.sort();
480     write_crate("search-index.js", &|| {
481         let mut v = String::from("var searchIndex = JSON.parse('{\\\n");
482         v.push_str(&all_indexes.join(",\\\n"));
483         v.push_str(
484             r#"\
485 }');
486 if (typeof window !== 'undefined' && window.initSearch) {window.initSearch(searchIndex)};
487 if (typeof exports !== 'undefined') {exports.searchIndex = searchIndex};
488 "#,
489         );
490         Ok(v.into_bytes())
491     })?;
492
493     write_crate("crates.js", &|| {
494         let krates = krates.iter().map(|k| format!("\"{}\"", k)).join(",");
495         Ok(format!("window.ALL_CRATES = [{}];", krates).into_bytes())
496     })?;
497
498     if options.enable_index_page {
499         if let Some(index_page) = options.index_page.clone() {
500             let mut md_opts = options.clone();
501             md_opts.output = cx.dst.clone();
502             md_opts.external_html = (*cx.shared).layout.external_html.clone();
503
504             crate::markdown::render(&index_page, md_opts, cx.shared.edition())
505                 .map_err(|e| Error::new(e, &index_page))?;
506         } else {
507             let shared = Rc::clone(&cx.shared);
508             let dst = cx.dst.join("index.html");
509             let page = layout::Page {
510                 title: "Index of crates",
511                 css_class: "mod",
512                 root_path: "./",
513                 static_root_path: shared.static_root_path.as_deref(),
514                 description: "List of crates",
515                 keywords: BASIC_KEYWORDS,
516                 resource_suffix: &shared.resource_suffix,
517             };
518
519             let content = format!(
520                 "<h1 class=\"fqn\">List of all crates</h1><ul class=\"all-items\">{}</ul>",
521                 krates
522                     .iter()
523                     .map(|s| {
524                         format!(
525                             "<li><a href=\"{}index.html\">{}</a></li>",
526                             ensure_trailing_slash(s),
527                             s
528                         )
529                     })
530                     .collect::<String>()
531             );
532             let v = layout::render(&shared.layout, &page, "", content, &shared.style_files);
533             shared.fs.write(dst, v)?;
534         }
535     }
536
537     // Update the list of all implementors for traits
538     let dst = cx.dst.join("implementors");
539     let cache = cx.cache();
540     for (&did, imps) in &cache.implementors {
541         // Private modules can leak through to this phase of rustdoc, which
542         // could contain implementations for otherwise private types. In some
543         // rare cases we could find an implementation for an item which wasn't
544         // indexed, so we just skip this step in that case.
545         //
546         // FIXME: this is a vague explanation for why this can't be a `get`, in
547         //        theory it should be...
548         let (remote_path, remote_item_type) = match cache.exact_paths.get(&did) {
549             Some(p) => match cache.paths.get(&did).or_else(|| cache.external_paths.get(&did)) {
550                 Some((_, t)) => (p, t),
551                 None => continue,
552             },
553             None => match cache.external_paths.get(&did) {
554                 Some((p, t)) => (p, t),
555                 None => continue,
556             },
557         };
558
559         struct Implementor {
560             text: String,
561             synthetic: bool,
562             types: Vec<String>,
563         }
564
565         impl Serialize for Implementor {
566             fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
567             where
568                 S: Serializer,
569             {
570                 let mut seq = serializer.serialize_seq(None)?;
571                 seq.serialize_element(&self.text)?;
572                 if self.synthetic {
573                     seq.serialize_element(&1)?;
574                     seq.serialize_element(&self.types)?;
575                 }
576                 seq.end()
577             }
578         }
579
580         let implementors = imps
581             .iter()
582             .filter_map(|imp| {
583                 // If the trait and implementation are in the same crate, then
584                 // there's no need to emit information about it (there's inlining
585                 // going on). If they're in different crates then the crate defining
586                 // the trait will be interested in our implementation.
587                 //
588                 // If the implementation is from another crate then that crate
589                 // should add it.
590                 if imp.impl_item.item_id.krate() == did.krate || !imp.impl_item.item_id.is_local() {
591                     None
592                 } else {
593                     Some(Implementor {
594                         text: imp.inner_impl().print(false, cx).to_string(),
595                         synthetic: imp.inner_impl().kind.is_auto(),
596                         types: collect_paths_for_type(imp.inner_impl().for_.clone(), cache),
597                     })
598                 }
599             })
600             .collect::<Vec<_>>();
601
602         // Only create a js file if we have impls to add to it. If the trait is
603         // documented locally though we always create the file to avoid dead
604         // links.
605         if implementors.is_empty() && !cache.paths.contains_key(&did) {
606             continue;
607         }
608
609         let implementors = format!(
610             r#""{}":{}"#,
611             krate.name(cx.tcx()),
612             serde_json::to_string(&implementors).expect("failed serde conversion"),
613         );
614
615         let mut mydst = dst.clone();
616         for part in &remote_path[..remote_path.len() - 1] {
617             mydst.push(part.to_string());
618         }
619         cx.shared.ensure_dir(&mydst)?;
620         mydst.push(&format!("{}.{}.js", remote_item_type, remote_path[remote_path.len() - 1]));
621
622         let (mut all_implementors, _) =
623             try_err!(collect(&mydst, krate.name(cx.tcx()).as_str()), &mydst);
624         all_implementors.push(implementors);
625         // Sort the implementors by crate so the file will be generated
626         // identically even with rustdoc running in parallel.
627         all_implementors.sort();
628
629         let mut v = String::from("(function() {var implementors = {\n");
630         v.push_str(&all_implementors.join(",\n"));
631         v.push_str("\n};");
632         v.push_str(
633             "if (window.register_implementors) {\
634                  window.register_implementors(implementors);\
635              } else {\
636                  window.pending_implementors = implementors;\
637              }",
638         );
639         v.push_str("})()");
640         cx.shared.fs.write(mydst, v)?;
641     }
642     Ok(())
643 }