Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add case insensitive file systems support in rustdoc #83612

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions src/bootstrap/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,9 +734,24 @@ impl Step for RustdocGUI {
}

let out_dir = builder.test_out(self.target).join("rustdoc-gui");
let mut command = builder.rustdoc_cmd(self.compiler);
command.arg("src/test/rustdoc-gui/lib.rs").arg("-o").arg(&out_dir);
builder.run(&mut command);

for file in fs::read_dir("src/test/rustdoc-gui").unwrap() {
let file = file.unwrap();
let file_name = file.file_name();

if !file_name.to_str().unwrap().ends_with(".rs") {
continue;
}

let mut command = builder.rustdoc_cmd(self.compiler);
command
.arg(&Path::new("src/test/rustdoc-gui").join(file_name))
.arg("-o")
.arg(&out_dir)
.arg("-Zunstable-options")
.arg("--generate-case-insensitive");
builder.run(&mut command);
}

for file in fs::read_dir("src/test/rustdoc-gui").unwrap() {
let file = file.unwrap();
Expand All @@ -750,7 +765,7 @@ impl Step for RustdocGUI {
command
.arg("src/tools/rustdoc-gui/tester.js")
.arg("--doc-folder")
.arg(out_dir.join("test_docs"))
.arg(&out_dir)
.arg("--test-file")
.arg(file_path);
builder.run(&mut command);
Expand Down
5 changes: 5 additions & 0 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ crate struct RenderOptions {
crate document_hidden: bool,
/// If `true`, generate a JSON file in the crate folder instead of HTML redirection files.
crate generate_redirect_map: bool,
/// If this option is set to `true`, HTML files will be generated as it would on a
/// case-insensitive file system.
crate generate_case_insensitive: bool,
crate unstable_features: rustc_feature::UnstableFeatures,
}

Expand Down Expand Up @@ -580,6 +583,7 @@ impl Options {
let document_hidden = matches.opt_present("document-hidden-items");
let run_check = matches.opt_present("check");
let generate_redirect_map = matches.opt_present("generate-redirect-map");
let generate_case_insensitive = matches.opt_present("generate-case-insensitive");

let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format);

Expand Down Expand Up @@ -638,6 +642,7 @@ impl Options {
document_private,
document_hidden,
generate_redirect_map,
generate_case_insensitive,
unstable_features: rustc_feature::UnstableFeatures::from_environment(
crate_name.as_deref(),
),
Expand Down
81 changes: 80 additions & 1 deletion src/librustdoc/formats/renderer.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_middle::ty::TyCtxt;
use rustc_span::{edition::Edition, Symbol};

use std::fs;
use std::io::Write;
use std::path::Path;

use crate::clean;
use crate::config::RenderOptions;
use crate::error::Error;
use crate::formats::cache::Cache;
use crate::html::render::print_item::item_path;

/// Allows for different backends to rustdoc to be used with the `run_format()` function. Each
/// backend renderer has hooks for initialization, documenting an item, entering and exiting a
Expand All @@ -26,6 +32,7 @@ crate trait FormatRenderer<'tcx>: Sized {
edition: Edition,
cache: Cache,
tcx: TyCtxt<'tcx>,
case_insensitive_conflicts: Option<FxHashSet<String>>,
) -> Result<(Self, clean::Crate), Error>;

/// Make a new renderer to render a child of the item currently being rendered.
Expand All @@ -52,6 +59,71 @@ crate trait FormatRenderer<'tcx>: Sized {
fn cache(&self) -> &Cache;
}

fn handle_module(dst: &Path, item: &clean::Item, paths_map: &mut FxHashMap<String, usize>) {
// modules are special because they add a namespace. We also need to
// recurse into the items of the module as well.
let name = item.name.as_ref().unwrap().to_string();
if name.is_empty() {
panic!("Unexpected module with empty name");
}
let module = match *item.kind {
clean::StrippedItem(box clean::ModuleItem(ref m)) | clean::ModuleItem(ref m) => m,
_ => unreachable!(),
};
let mod_path = dst.join(&name);
for it in &module.items {
if it.is_mod() {
handle_module(&mod_path, it, paths_map);
} else if it.name.is_some() && !it.is_extern_crate() {
let name = it.name.as_ref().unwrap();
let item_type = it.type_();
let file_name = &item_path(item_type, &name.as_str());
let insensitive_path = mod_path.join(file_name).display().to_string();

let entry = paths_map.entry(insensitive_path.to_lowercase()).or_insert(0);
*entry += 1;
}
}
}

fn build_case_insensitive_map(
krate: &clean::Crate,
options: &RenderOptions,
) -> Option<FxHashSet<String>> {
let mut paths_map: FxHashMap<String, usize> = FxHashMap::default();

handle_module(&options.output, &krate.module, &mut paths_map);
Some(paths_map.into_iter().filter(|(_, count)| *count > 1).map(|(path, _)| path).collect())
}

fn check_if_case_insensitive(dst: &Path) -> bool {
fn create_and_write(dst: &Path, content: &str) {
if let Ok(mut f) = fs::OpenOptions::new().write(true).create(true).truncate(true).open(dst)
{
// Ignoring potential errors.
let _ = f.write(content.as_bytes());
}
}
fn compare_content(dst: &Path, content: &str) -> bool {
fs::read_to_string(dst).unwrap_or_else(|_| String::new()).as_str() == content
}

let path1 = dst.join("___a.tmp");
let content1 = "a";
let path2 = dst.join("___A.tmp");
let content2 = "A";

create_and_write(&path1, content1);
create_and_write(&path1, content2);

let res = compare_content(&path1, content1) && compare_content(&path2, content2);
// We ignore the errors when removing the files.
let _ = fs::remove_file(&path1);
let _ = fs::remove_file(&path2);

res
}

/// Main method for rendering a crate.
crate fn run_format<'tcx, T: FormatRenderer<'tcx>>(
krate: clean::Crate,
Expand All @@ -63,9 +135,16 @@ crate fn run_format<'tcx, T: FormatRenderer<'tcx>>(
) -> Result<(), Error> {
let prof = &tcx.sess.prof;

let case_insensitive_conflicts =
if options.generate_case_insensitive || check_if_case_insensitive(&options.output) {
build_case_insensitive_map(&krate, &options)
} else {
None
};

let (mut format_renderer, krate) = prof
.extra_verbose_generic_activity("create_renderer", T::descr())
.run(|| T::init(krate, options, edition, cache, tcx))?;
.run(|| T::init(krate, options, edition, cache, tcx, case_insensitive_conflicts))?;

// Render the crate documentation
let crate_name = krate.name;
Expand Down
36 changes: 36 additions & 0 deletions src/librustdoc/html/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,42 @@ crate fn render<T: Print, S: Print>(
)
}

/// Since this is a "conflict file" on case insensitive file system, it'll be loaded by JS instead
/// of being loaded directly. Which is why we need to modify its output a bit.
crate fn conflict_layout<T: Print, S>(page: &Page<'_>, sidebar: S, t: T) -> String
where
S: FnOnce(&mut Buffer) -> Option<String>,
{
let content = Buffer::html().to_display(t).to_string();
let mut sidebar_buf = Buffer::html();
let script_src = sidebar(&mut sidebar_buf);
format!(
"\
document.getElementById('main').innerHTML = \"{content}\";\
document.getElementsByClassName('sidebar')[0].innerHTML += \"{sidebar}\";\
document.title = \"{title}\";\
window.initSidebarVars();\
rustdocInit();{script}",
content = content.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"),
title = page.title.replace("\\", "\\\\").replace("\"", "\\\""),
sidebar = sidebar_buf
.into_inner()
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n"),
script = if let Some(script_src) = script_src {
format!(
"var script = document.createElement('script');\
script.src = {:?};\
document.body.appendChild(script);",
script_src,
)
Comment on lines +249 to +254
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be dynamic with createElement('script')? Couldn't you just add the JS to the page directly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could, but that would make the page a lot heavier (minimum twice the normal size since there at least two "items" to be loaded).

Copy link
Member

@jyn514 jyn514 Mar 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not my point - the idea here is you want to put the JS in a separate file so the main page is smaller, right? Why not add <script src="{script_src}"> rather than add that dynamically?

} else {
String::new()
},
)
}

crate fn redirect(url: &str) -> String {
// <script> triggers a redirect before refresh, so this is fine.
format!(
Expand Down
Loading