Clean up some fs-related utilities

This does a little cleanup around the usage of filesystem functions:

- Add `mdbook_core::utils::fs::read_to_string` as a wrapper around
  `std::fs::read_to_string` to provide better error messages. Use
  this wherever a file is read.
- Add `mdbook_core::utils::fs::create_dir_all` as a wrapper around
  `std::fs::create_dir_all` to provide better error messages. Use
  this wherever a file is read.
- Replace `mdbook_core::utils::fs::write_file` with `write` to mirror
  the `std::fs::write` API.
- Remove `mdbook_core::utils::fs::create_file`. It was generally not
  used anymore.
- Scrub the usage of `std::fs` to use the new wrappers. This doesn't
  remove it 100%, but it is now significantly reduced.
This commit is contained in:
Eric Huss 2025-09-20 17:05:33 -07:00
parent f24221a1d7
commit 797112ef36
13 changed files with 158 additions and 253 deletions

View file

@ -43,14 +43,11 @@
//! # run().unwrap() //! # run().unwrap()
//! ``` //! ```
use crate::utils::TomlExt; use crate::utils::{TomlExt, fs, log_backtrace};
use crate::utils::log_backtrace;
use anyhow::{Context, Error, Result, bail}; use anyhow::{Context, Error, Result, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::env; use std::env;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
use toml::Value; use toml::Value;
@ -113,13 +110,8 @@ impl Default for Config {
impl Config { impl Config {
/// Load the configuration file from disk. /// Load the configuration file from disk.
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> { pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
let mut buffer = String::new(); let cfg = fs::read_to_string(config_file)?;
File::open(config_file) Config::from_str(&cfg)
.with_context(|| "Unable to open the configuration file")?
.read_to_string(&mut buffer)
.with_context(|| "Couldn't read the file")?;
Config::from_str(&buffer)
} }
/// Updates the `Config` from the available environment variables. /// Updates the `Config` from the available environment variables.

View file

@ -1,16 +1,38 @@
//! Filesystem utilities and helpers. //! Filesystem utilities and helpers.
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::fs::{self, File}; use std::fs;
use std::io::Write;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use tracing::{debug, trace}; use tracing::debug;
/// Write the given data to a file, creating it first if necessary /// Reads a file into a string.
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> { ///
let path = build_dir.join(filename); /// Equivalent to [`std::fs::read_to_string`] with better error messages.
pub fn read_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
fs::read_to_string(path).with_context(|| format!("failed to read `{}`", path.display()))
}
create_file(&path)?.write_all(content).map_err(Into::into) /// Writes a file to disk.
///
/// Equivalent to [`std::fs::write`] with better error messages. This will
/// also create the parent directory if it doesn't exist.
pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<()> {
let path = path.as_ref();
debug!("Writing `{}`", path.display());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
fs::write(path, contents.as_ref())
.with_context(|| format!("failed to write `{}`", path.display()))
}
/// Equivalent to [`std::fs::create_dir_all`] with better error messages.
pub fn create_dir_all(p: impl AsRef<Path>) -> Result<()> {
let p = p.as_ref();
fs::create_dir_all(p)
.with_context(|| format!("failed to create directory `{}`", p.display()))?;
Ok(())
} }
/// Takes a path and returns a path containing just enough `../` to point to /// Takes a path and returns a path containing just enough `../` to point to
@ -48,30 +70,19 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
}) })
} }
/// This function creates a file and returns it. But before creating the file /// Removes all the content of a directory but not the directory itself.
/// it checks every directory in the path to see if it exists,
/// and if it does not it will be created.
pub fn create_file(path: &Path) -> Result<File> {
debug!("Creating {}", path.display());
// Construct path
if let Some(p) = path.parent() {
trace!("Parent directory is: {:?}", p);
fs::create_dir_all(p)?;
}
File::create(path).map_err(Into::into)
}
/// Removes all the content of a directory but not the directory itself
pub fn remove_dir_content(dir: &Path) -> Result<()> { pub fn remove_dir_content(dir: &Path) -> Result<()> {
for item in fs::read_dir(dir)?.flatten() { for item in fs::read_dir(dir)
.with_context(|| format!("failed to read directory `{}`", dir.display()))?
.flatten()
{
let item = item.path(); let item = item.path();
if item.is_dir() { if item.is_dir() {
fs::remove_dir_all(item)?; fs::remove_dir_all(&item)
.with_context(|| format!("failed to remove `{}`", item.display()))?;
} else { } else {
fs::remove_file(item)?; fs::remove_file(&item)
.with_context(|| format!("failed to remove `{}`", item.display()))?;
} }
} }
Ok(()) Ok(())
@ -162,7 +173,7 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
let mut reader = File::open(from)?; let mut reader = std::fs::File::open(from)?;
let metadata = reader.metadata()?; let metadata = reader.metadata()?;
if !metadata.is_file() { if !metadata.is_file() {
anyhow::bail!( anyhow::bail!(
@ -198,8 +209,9 @@ fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::copy_files_except_ext; use super::*;
use std::{fs, io::Result, path::Path}; use std::io::Result;
use std::path::Path;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> { fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(src: P, dst: Q) -> Result<()> {
@ -219,38 +231,18 @@ mod tests {
}; };
// Create a couple of files // Create a couple of files
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) { write(tmp.path().join("file.txt"), "").unwrap();
panic!("Could not create file.txt: {err}"); write(tmp.path().join("file.md"), "").unwrap();
} write(tmp.path().join("file.png"), "").unwrap();
if let Err(err) = fs::File::create(tmp.path().join("file.md")) { write(tmp.path().join("sub_dir/file.png"), "").unwrap();
panic!("Could not create file.md: {err}"); write(tmp.path().join("sub_dir_exists/file.txt"), "").unwrap();
}
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
panic!("Could not create file.png: {err}");
}
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
panic!("Could not create sub_dir: {err}");
}
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
panic!("Could not create sub_dir/file.png: {err}");
}
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
panic!("Could not create sub_dir_exists: {err}");
}
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
panic!("Could not create sub_dir_exists/file.txt: {err}");
}
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) { if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
panic!("Could not symlink file.png: {err}"); panic!("Could not symlink file.png: {err}");
} }
// Create output dir // Create output dir
if let Err(err) = fs::create_dir(tmp.path().join("output")) { create_dir_all(tmp.path().join("output")).unwrap();
panic!("Could not create output: {err}"); create_dir_all(tmp.path().join("output/sub_dir_exists")).unwrap();
}
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
panic!("Could not create output/sub_dir_exists: {err}");
}
if let Err(e) = if let Err(e) =
copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"]) copy_files_except_ext(tmp.path(), &tmp.path().join("output"), true, None, &["md"])

View file

@ -5,9 +5,9 @@ use self::take_lines::{
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mdbook_core::book::{Book, BookItem}; use mdbook_core::book::{Book, BookItem};
use mdbook_core::static_regex; use mdbook_core::static_regex;
use mdbook_core::utils::fs;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use regex::{CaptureMatches, Captures}; use regex::{CaptureMatches, Captures};
use std::fs;
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo}; use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::{error, warn}; use tracing::{error, warn};

View file

@ -1,7 +1,6 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mdbook_core::utils; use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer}; use mdbook_renderer::{RenderContext, Renderer};
use std::fs;
use tracing::trace; use tracing::trace;
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful /// A renderer to output the Markdown after the preprocessors have run. Mostly useful
@ -27,17 +26,16 @@ impl Renderer for MarkdownRenderer {
let book = &ctx.book; let book = &ctx.book;
if destination.exists() { if destination.exists() {
utils::fs::remove_dir_content(destination) fs::remove_dir_content(destination)
.with_context(|| "Unable to remove stale Markdown output")?; .with_context(|| "Unable to remove stale Markdown output")?;
} }
trace!("markdown render"); trace!("markdown render");
for ch in book.chapters() { for ch in book.chapters() {
utils::fs::write_file( let path = ctx
&ctx.destination, .destination
ch.path.as_ref().expect("Checked path exists before"), .join(ch.path.as_ref().expect("Checked path exists before"));
ch.content.as_bytes(), fs::write(path, &ch.content)?;
)?;
} }
fs::create_dir_all(destination) fs::create_dir_all(destination)

View file

@ -3,8 +3,8 @@
//! The HTML renderer can be found in the [`mdbook_html`] crate. //! The HTML renderer can be found in the [`mdbook_html`] crate.
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer}; use mdbook_renderer::{RenderContext, Renderer};
use std::fs;
use std::process::Stdio; use std::process::Stdio;
use tracing::{error, info, trace, warn}; use tracing::{error, info, trace, warn};

View file

@ -1,14 +1,11 @@
//! Support for initializing a new book. //! Support for initializing a new book.
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use super::MDBook; use super::MDBook;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mdbook_core::config::Config; use mdbook_core::config::Config;
use mdbook_core::utils::fs::write_file; use mdbook_core::utils::fs;
use mdbook_html::theme; use mdbook_html::theme;
use std::path::PathBuf;
use tracing::{debug, error, info, trace}; use tracing::{debug, error, info, trace};
/// A helper for setting up a new book and its directory structure. /// A helper for setting up a new book and its directory structure.
@ -104,8 +101,7 @@ impl BookBuilder {
let cfg = let cfg =
toml::to_string(&self.config).with_context(|| "Unable to serialize the config")?; toml::to_string(&self.config).with_context(|| "Unable to serialize the config")?;
std::fs::write(&book_toml, cfg) fs::write(&book_toml, cfg)?;
.with_context(|| format!("Unable to write config to {book_toml:?}"))?;
Ok(()) Ok(())
} }
@ -115,61 +111,32 @@ impl BookBuilder {
let html_config = self.config.html_config().unwrap_or_default(); let html_config = self.config.html_config().unwrap_or_default();
let themedir = html_config.theme_dir(&self.root); let themedir = html_config.theme_dir(&self.root);
if !themedir.exists() { fs::write(themedir.join("book.js"), theme::JS)?;
debug!( fs::write(themedir.join("favicon.png"), theme::FAVICON_PNG)?;
"{} does not exist, creating the directory", fs::write(themedir.join("favicon.svg"), theme::FAVICON_SVG)?;
themedir.display() fs::write(themedir.join("highlight.css"), theme::HIGHLIGHT_CSS)?;
); fs::write(themedir.join("highlight.js"), theme::HIGHLIGHT_JS)?;
fs::create_dir(&themedir)?; fs::write(themedir.join("index.hbs"), theme::INDEX)?;
}
let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
let cssdir = themedir.join("css"); let cssdir = themedir.join("css");
if !cssdir.exists() {
fs::create_dir(&cssdir)?;
}
let mut general_css = File::create(cssdir.join("general.css"))?;
general_css.write_all(theme::GENERAL_CSS)?;
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
chrome_css.write_all(theme::CHROME_CSS)?;
fs::write(cssdir.join("general.css"), theme::GENERAL_CSS)?;
fs::write(cssdir.join("chrome.css"), theme::CHROME_CSS)?;
fs::write(cssdir.join("variables.css"), theme::VARIABLES_CSS)?;
if html_config.print.enable { if html_config.print.enable {
let mut print_css = File::create(cssdir.join("print.css"))?; fs::write(cssdir.join("print.css"), theme::PRINT_CSS)?;
print_css.write_all(theme::PRINT_CSS)?;
} }
let mut variables_css = File::create(cssdir.join("variables.css"))?; let fonts_dir = themedir.join("fonts");
variables_css.write_all(theme::VARIABLES_CSS)?; fs::write(fonts_dir.join("fonts.css"), theme::fonts::CSS)?;
let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON_PNG)?;
let mut favicon = File::create(themedir.join("favicon.svg"))?;
favicon.write_all(theme::FAVICON_SVG)?;
let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?;
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
write_file(&themedir.join("fonts"), "fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES { for (file_name, contents) in theme::fonts::LICENSES {
write_file(&themedir, file_name, contents)?; fs::write(themedir.join(file_name), contents)?;
} }
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() { for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
write_file(&themedir, file_name, contents)?; fs::write(themedir.join(file_name), contents)?;
} }
write_file( fs::write(
&themedir, themedir.join(theme::fonts::SOURCE_CODE_PRO.0),
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1, theme::fonts::SOURCE_CODE_PRO.1,
)?; )?;
@ -177,12 +144,10 @@ impl BookBuilder {
} }
fn build_gitignore(&self) -> Result<()> { fn build_gitignore(&self) -> Result<()> {
debug!("Creating .gitignore"); fs::write(
self.root.join(".gitignore"),
let mut f = File::create(self.root.join(".gitignore"))?; format!("{}", self.config.build.build_dir.display()),
)?;
writeln!(f, "{}", self.config.build.build_dir.display())?;
Ok(()) Ok(())
} }
@ -193,14 +158,14 @@ impl BookBuilder {
let summary = src_dir.join("SUMMARY.md"); let summary = src_dir.join("SUMMARY.md");
if !summary.exists() { if !summary.exists() {
trace!("No summary found creating stub summary and chapter_1.md."); trace!("No summary found creating stub summary and chapter_1.md.");
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?; fs::write(
writeln!(f, "# Summary")?; summary,
writeln!(f)?; "# Summary\n\
writeln!(f, "- [Chapter 1](./chapter_1.md)")?; \n\
- [Chapter 1](./chapter_1.md)\n",
)?;
let chapter_1 = src_dir.join("chapter_1.md"); fs::write(src_dir.join("chapter_1.md"), "# Chapter 1\n")?;
let mut f = File::create(chapter_1).with_context(|| "Unable to create chapter_1.md")?;
writeln!(f, "# Chapter 1")?;
} else { } else {
trace!("Existing summary found, no need to create stub files."); trace!("Existing summary found, no need to create stub files.");
} }

View file

@ -1,10 +1,8 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mdbook_core::book::{Book, BookItem, Chapter}; use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::BuildConfig; use mdbook_core::config::BuildConfig;
use mdbook_core::utils::escape_html; use mdbook_core::utils::{escape_html, fs};
use mdbook_summary::{Link, Summary, SummaryItem, parse_summary}; use mdbook_summary::{Link, Summary, SummaryItem, parse_summary};
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path; use std::path::Path;
use tracing::debug; use tracing::debug;
@ -13,11 +11,7 @@ pub(crate) fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result
let src_dir = src_dir.as_ref(); let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md"); let summary_md = src_dir.join("SUMMARY.md");
let mut summary_content = String::new(); let summary_content = fs::read_to_string(&summary_md)?;
File::open(&summary_md)
.with_context(|| format!("Couldn't open SUMMARY.md in {src_dir:?} directory"))?
.read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content) let summary = parse_summary(&summary_content)
.with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?; .with_context(|| format!("Summary parsing failed for file={summary_md:?}"))?;
@ -47,12 +41,8 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
} }
} }
debug!("Creating missing file {}", filename.display()); debug!("Creating missing file {}", filename.display());
let mut f = File::create(&filename).with_context(|| {
format!("Unable to create missing file: {}", filename.display())
})?;
let title = escape_html(&link.name); let title = escape_html(&link.name);
writeln!(f, "# {title}")?; fs::write(&filename, format!("# {title}\n"))?;
} }
} }
@ -116,13 +106,8 @@ fn load_chapter<P: AsRef<Path>>(
src_dir.join(link_location) src_dir.join(link_location)
}; };
let mut f = File::open(&location) let mut content = std::fs::read_to_string(&location)
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?; .with_context(|| format!("failed to read chapter `{}`", link_location.display()))?;
let mut content = String::new();
f.read_to_string(&mut content).with_context(|| {
format!("Unable to read \"{}\" ({})", link.name, location.display())
})?;
if content.as_bytes().starts_with(b"\xef\xbb\xbf") { if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
content.replace_range(..3, ""); content.replace_range(..3, "");
@ -174,10 +159,7 @@ And here is some \
let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap(); let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp.path().join("chapter_1.md"); let chapter_path = temp.path().join("chapter_1.md");
File::create(&chapter_path) fs::write(&chapter_path, DUMMY_SRC).unwrap();
.unwrap()
.write_all(DUMMY_SRC.as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path); let link = Link::new("Chapter 1", chapter_path);
@ -189,11 +171,7 @@ And here is some \
let (mut root, temp_dir) = dummy_link(); let (mut root, temp_dir) = dummy_link();
let second_path = temp_dir.path().join("second.md"); let second_path = temp_dir.path().join("second.md");
fs::write(&second_path, "Hello World!").unwrap();
File::create(&second_path)
.unwrap()
.write_all(b"Hello World!")
.unwrap();
let mut second = Link::new("Nested Chapter 1", &second_path); let mut second = Link::new("Nested Chapter 1", &second_path);
second.number = Some(SectionNumber::new([1, 2])); second.number = Some(SectionNumber::new([1, 2]));
@ -224,10 +202,7 @@ And here is some \
let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap(); let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
let chapter_path = temp_dir.path().join("chapter_1.md"); let chapter_path = temp_dir.path().join("chapter_1.md");
File::create(&chapter_path) fs::write(&chapter_path, format!("\u{feff}{DUMMY_SRC}")).unwrap();
.unwrap()
.write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
.unwrap();
let link = Link::new("Chapter 1", chapter_path); let link = Link::new("Chapter 1", chapter_path);
@ -307,7 +282,7 @@ And here is some \
fn cant_load_chapters_when_the_link_is_a_directory() { fn cant_load_chapters_when_the_link_is_a_directory() {
let (_, temp) = dummy_link(); let (_, temp) = dummy_link();
let dir = temp.path().join("nested"); let dir = temp.path().join("nested");
fs::create_dir(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();
let mut summary = Summary::default(); let mut summary = Summary::default();
let link = Link::new("nested", dir); let link = Link::new("nested", dir);
@ -326,8 +301,8 @@ And here is some \
assert!(got.is_err()); assert!(got.is_err());
let error_message = got.err().unwrap().to_string(); let error_message = got.err().unwrap().to_string();
let expected = format!( let expected = format!(
r#"Couldn't open SUMMARY.md in {:?} directory"#, r#"failed to read `{}`"#,
temp_dir.path() temp_dir.path().join("SUMMARY.md").display()
); );
assert_eq!(error_message, expected); assert_eq!(error_message, expected);
} }

View file

@ -8,14 +8,14 @@ use anyhow::{Context, Error, Result, bail};
use indexmap::IndexMap; use indexmap::IndexMap;
use mdbook_core::book::{Book, BookItem, BookItems}; use mdbook_core::book::{Book, BookItem, BookItems};
use mdbook_core::config::{Config, RustEdition}; use mdbook_core::config::{Config, RustEdition};
use mdbook_core::utils; use mdbook_core::utils::fs;
use mdbook_html::HtmlHandlebars; use mdbook_html::HtmlHandlebars;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use mdbook_renderer::{RenderContext, Renderer}; use mdbook_renderer::{RenderContext, Renderer};
use mdbook_summary::Summary; use mdbook_summary::Summary;
use serde::Deserialize; use serde::Deserialize;
use std::ffi::OsString; use std::ffi::OsString;
use std::io::{IsTerminal, Write}; use std::io::IsTerminal;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
use tempfile::Builder as TempFileBuilder; use tempfile::Builder as TempFileBuilder;
@ -292,8 +292,7 @@ impl MDBook {
// write preprocessed file to tempdir // write preprocessed file to tempdir
let path = temp_dir.path().join(chapter_path); let path = temp_dir.path().join(chapter_path);
let mut tmpf = utils::fs::create_file(&path)?; fs::write(&path, &ch.content)?;
tmpf.write_all(ch.content.as_bytes())?;
let mut cmd = Command::new("rustdoc"); let mut cmd = Command::new("rustdoc");
cmd.current_dir(temp_dir.path()) cmd.current_dir(temp_dir.path())

View file

@ -8,11 +8,10 @@ use anyhow::{Context, Result, bail};
use handlebars::Handlebars; use handlebars::Handlebars;
use mdbook_core::book::{Book, BookItem, Chapter}; use mdbook_core::book::{Book, BookItem, Chapter};
use mdbook_core::config::{BookConfig, Config, HtmlConfig}; use mdbook_core::config::{BookConfig, Config, HtmlConfig};
use mdbook_core::utils; use mdbook_core::utils::fs;
use mdbook_renderer::{RenderContext, Renderer}; use mdbook_renderer::{RenderContext, Renderer};
use serde_json::json; use serde_json::json;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::error; use tracing::error;
use tracing::{debug, info, trace, warn}; use tracing::{debug, info, trace, warn};
@ -84,10 +83,8 @@ impl HtmlHandlebars {
ctx.data.insert("content".to_owned(), json!(content)); ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name)); ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title)); ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert( ctx.data
"path_to_root".to_owned(), .insert("path_to_root".to_owned(), json!(fs::path_to_root(path)));
json!(utils::fs::path_to_root(path)),
);
if let Some(ref section) = ch.number { if let Some(ref section) = ch.number {
ctx.data ctx.data
.insert("section".to_owned(), json!(section.to_string())); .insert("section".to_owned(), json!(section.to_string()));
@ -123,8 +120,8 @@ impl HtmlHandlebars {
let rendered = ctx.handlebars.render("index", &ctx.data)?; let rendered = ctx.handlebars.render("index", &ctx.data)?;
// Write to file // Write to file
debug!("Creating {}", filepath.display()); let out_path = ctx.destination.join(filepath);
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?; fs::write(&out_path, rendered)?;
if prev_ch.is_none() { if prev_ch.is_none() {
ctx.data.insert("path".to_owned(), json!("index.md")); ctx.data.insert("path".to_owned(), json!("index.md"));
@ -132,7 +129,7 @@ impl HtmlHandlebars {
ctx.data.insert("is_index".to_owned(), json!(true)); ctx.data.insert("is_index".to_owned(), json!(true));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?; let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
debug!("Creating index.html from {}", ctx_path); debug!("Creating index.html from {}", ctx_path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?; fs::write(ctx.destination.join("index.html"), rendered_index)?;
} }
Ok(()) Ok(())
@ -146,18 +143,15 @@ impl HtmlHandlebars {
handlebars: &mut Handlebars<'_>, handlebars: &mut Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>, data: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<()> { ) -> Result<()> {
let destination = &ctx.destination;
let content_404 = if let Some(ref filename) = html_config.input_404 { let content_404 = if let Some(ref filename) = html_config.input_404 {
let path = src_dir.join(filename); let path = src_dir.join(filename);
std::fs::read_to_string(&path) fs::read_to_string(&path).with_context(|| "failed to read the 404 input file")?
.with_context(|| format!("unable to open 404 input file {path:?}"))?
} else { } else {
// 404 input not explicitly configured try the default file 404.md // 404 input not explicitly configured try the default file 404.md
let default_404_location = src_dir.join("404.md"); let default_404_location = src_dir.join("404.md");
if default_404_location.exists() { if default_404_location.exists() {
std::fs::read_to_string(&default_404_location).with_context(|| { fs::read_to_string(&default_404_location)
format!("unable to open 404 input file {default_404_location:?}") .with_context(|| "failed to read the 404 input file")?
})?
} else { } else {
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \ "# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
navigation bar or search to continue." navigation bar or search to continue."
@ -195,8 +189,8 @@ impl HtmlHandlebars {
data_404.insert("title".to_owned(), json!(title)); data_404.insert("title".to_owned(), json!(title));
let rendered = handlebars.render("index", &data_404)?; let rendered = handlebars.render("index", &data_404)?;
let output_file = html_config.get_404_output_file(); let output_file = ctx.destination.join(html_config.get_404_output_file());
utils::fs::write_file(destination, output_file, rendered.as_bytes())?; fs::write(output_file, rendered)?;
debug!("Creating 404.html ✓"); debug!("Creating 404.html ✓");
Ok(()) Ok(())
} }
@ -222,7 +216,7 @@ impl HtmlHandlebars {
data.insert("content".to_owned(), json!(print_content)); data.insert("content".to_owned(), json!(print_content));
data.insert( data.insert(
"path_to_root".to_owned(), "path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))), json!(fs::path_to_root(Path::new("print.md"))),
); );
debug!("Render template"); debug!("Render template");
@ -285,8 +279,7 @@ impl HtmlHandlebars {
fragment_map: &BTreeMap<String, String>, fragment_map: &BTreeMap<String, String>,
) -> Result<()> { ) -> Result<()> {
if let Some(parent) = original.parent() { if let Some(parent) = original.parent() {
std::fs::create_dir_all(parent) fs::create_dir_all(parent)?
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
} }
let js_map = serde_json::to_string(fragment_map)?; let js_map = serde_json::to_string(fragment_map)?;
@ -295,15 +288,13 @@ impl HtmlHandlebars {
"fragment_map": js_map, "fragment_map": js_map,
"url": destination, "url": destination,
}); });
let f = File::create(original)?; let rendered = handlebars.render("redirect", &ctx).with_context(|| {
handlebars format!(
.render_to_write("redirect", &ctx, f) "Unable to create a redirect file at `{}`",
.with_context(|| { original.display()
format!( )
"Unable to create a redirect file at \"{}\"", })?;
original.display() fs::write(original, rendered)?;
)
})?;
Ok(()) Ok(())
} }
@ -323,7 +314,7 @@ impl Renderer for HtmlHandlebars {
let build_dir = ctx.root.join(&ctx.config.build.build_dir); let build_dir = ctx.root.join(&ctx.config.build.build_dir);
if destination.exists() { if destination.exists() {
utils::fs::remove_dir_content(destination) fs::remove_dir_content(destination)
.with_context(|| "Unable to remove stale HTML output")?; .with_context(|| "Unable to remove stale HTML output")?;
} }
@ -406,20 +397,19 @@ impl Renderer for HtmlHandlebars {
data.insert("is_toc_html".to_owned(), json!(true)); data.insert("is_toc_html".to_owned(), json!(true));
data.insert("path".to_owned(), json!("toc.html")); data.insert("path".to_owned(), json!("toc.html"));
let rendered_toc = handlebars.render("toc_html", &data)?; let rendered_toc = handlebars.render("toc_html", &data)?;
utils::fs::write_file(destination, "toc.html", rendered_toc.as_bytes())?; fs::write(destination.join("toc.html"), rendered_toc)?;
debug!("Creating toc.html ✓"); debug!("Creating toc.html ✓");
data.remove("path"); data.remove("path");
data.remove("is_toc_html"); data.remove("is_toc_html");
} }
utils::fs::write_file( fs::write(
destination, destination.join(".nojekyll"),
".nojekyll",
b"This file makes sure that Github Pages doesn't process mdBook's output.\n", b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
)?; )?;
if let Some(cname) = &html_config.cname { if let Some(cname) = &html_config.cname {
utils::fs::write_file(destination, "CNAME", format!("{cname}\n").as_bytes())?; fs::write(destination.join("CNAME"), format!("{cname}\n"))?;
} }
for (i, chapter_tree) in chapter_trees.iter().enumerate() { for (i, chapter_tree) in chapter_trees.iter().enumerate() {
@ -446,7 +436,7 @@ impl Renderer for HtmlHandlebars {
let print_rendered = let print_rendered =
self.render_print_page(ctx, &handlebars, &mut data, chapter_trees)?; self.render_print_page(ctx, &handlebars, &mut data, chapter_trees)?;
utils::fs::write_file(destination, "print.html", print_rendered.as_bytes())?; fs::write(destination.join("print.html"), print_rendered)?;
debug!("Creating print.html ✓"); debug!("Creating print.html ✓");
} }
@ -454,7 +444,7 @@ impl Renderer for HtmlHandlebars {
.context("Unable to emit redirects")?; .context("Unable to emit redirects")?;
// Copy all remaining files, avoid a recursive copy from/to the book build dir // Copy all remaining files, avoid a recursive copy from/to the book build dir
utils::fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?; fs::copy_files_except_ext(&src_dir, destination, true, Some(&build_dir), &["md"])?;
info!("HTML book written to `{}`", destination.display()); info!("HTML book written to `{}`", destination.display());

View file

@ -5,10 +5,9 @@ use crate::theme::{self, Theme, playground_editor};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use mdbook_core::config::HtmlConfig; use mdbook_core::config::HtmlConfig;
use mdbook_core::static_regex; use mdbook_core::static_regex;
use mdbook_core::utils; use mdbook_core::utils::fs;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::debug; use tracing::debug;
@ -165,8 +164,10 @@ impl StaticFiles {
if let Some((name, suffix)) = parts { if let Some((name, suffix)) = parts {
if name != "" && suffix != "" { if name != "" && suffix != "" {
let mut digest = Sha256::new(); let mut digest = Sha256::new();
let mut input_file = File::open(input_location) let mut input_file =
.with_context(|| "open static file for hashing")?; std::fs::File::open(input_location).with_context(|| {
format!("failed to open `{filename}` for hashing")
})?;
let mut buf = vec![0; 1024]; let mut buf = vec![0; 1024];
loop { loop {
let amt = input_file let amt = input_file
@ -190,7 +191,6 @@ impl StaticFiles {
} }
pub(super) fn write_files(self, destination: &Path) -> Result<ResourceHelper> { pub(super) fn write_files(self, destination: &Path) -> Result<ResourceHelper> {
use mdbook_core::utils::fs::write_file;
use regex::bytes::Captures; use regex::bytes::Captures;
// The `{{ resource "name" }}` directive in static resources look like // The `{{ resource "name" }}` directive in static resources look like
// handlebars syntax, even if they technically aren't. // handlebars syntax, even if they technically aren't.
@ -207,7 +207,7 @@ impl StaticFiles {
.as_bytes(); .as_bytes();
let name = std::str::from_utf8(name).expect("resource name with invalid utf8"); let name = std::str::from_utf8(name).expect("resource name with invalid utf8");
let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name); let resource_filename = hash_map.get(name).map(|s| &s[..]).unwrap_or(name);
let path_to_root = utils::fs::path_to_root(filename); let path_to_root = fs::path_to_root(filename);
format!("{}{}", path_to_root, resource_filename) format!("{}{}", path_to_root, resource_filename)
.as_bytes() .as_bytes()
.to_owned() .to_owned()
@ -222,7 +222,8 @@ impl StaticFiles {
} else { } else {
Cow::Borrowed(&data[..]) Cow::Borrowed(&data[..])
}; };
write_file(destination, filename, &data)?; let path = destination.join(filename);
fs::write(path, &data)?;
} }
StaticFile::Additional { StaticFile::Additional {
input_location, input_location,
@ -239,11 +240,12 @@ impl StaticFiles {
.with_context(|| format!("Unable to create {}", parent.display()))?; .with_context(|| format!("Unable to create {}", parent.display()))?;
} }
if filename.ends_with(".css") || filename.ends_with(".js") { if filename.ends_with(".css") || filename.ends_with(".js") {
let data = fs::read(input_location)?; let data = fs::read_to_string(input_location)?;
let data = replace_all(&self.hash_map, &data, filename); let data = replace_all(&self.hash_map, data.as_bytes(), filename);
write_file(destination, filename, &data)?; let path = destination.join(filename);
fs::write(path, &data)?;
} else { } else {
fs::copy(input_location, &output_location).with_context(|| { std::fs::copy(input_location, &output_location).with_context(|| {
format!( format!(
"Unable to copy {} to {}", "Unable to copy {} to {}",
input_location.display(), input_location.display(),
@ -264,7 +266,7 @@ mod tests {
use super::*; use super::*;
use crate::theme::Theme; use crate::theme::Theme;
use mdbook_core::config::HtmlConfig; use mdbook_core::config::HtmlConfig;
use mdbook_core::utils::fs::write_file; use mdbook_core::utils::fs;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[test]
@ -295,9 +297,8 @@ mod tests {
let reference_js = Path::new("static-files-test-case-reference.js"); let reference_js = Path::new("static-files-test-case-reference.js");
let mut html_config = HtmlConfig::default(); let mut html_config = HtmlConfig::default();
html_config.additional_js.push(reference_js.to_owned()); html_config.additional_js.push(reference_js.to_owned());
write_file( fs::write(
temp_dir.path(), temp_dir.path().join(reference_js),
reference_js,
br#"{{ resource "book.js" }}"#, br#"{{ resource "book.js" }}"#,
) )
.unwrap(); .unwrap();
@ -305,7 +306,7 @@ mod tests {
static_files.hash_files().unwrap(); static_files.hash_files().unwrap();
static_files.write_files(temp_dir.path()).unwrap(); static_files.write_files(temp_dir.path()).unwrap();
// custom JS winds up referencing book.js // custom JS winds up referencing book.js
let reference_js_content = std::fs::read_to_string( let reference_js_content = fs::read_to_string(
temp_dir temp_dir
.path() .path()
.join("static-files-test-case-reference-635c9cdc.js"), .join("static-files-test-case-reference-635c9cdc.js"),
@ -313,8 +314,7 @@ mod tests {
.unwrap(); .unwrap();
assert_eq!("book-e3b0c442.js", reference_js_content); assert_eq!("book-e3b0c442.js", reference_js_content);
// book.js winds up empty // book.js winds up empty
let book_js_content = let book_js_content = fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
std::fs::read_to_string(temp_dir.path().join("book-e3b0c442.js")).unwrap();
assert_eq!("", book_js_content); assert_eq!("", book_js_content);
} }
} }

View file

@ -1,8 +1,6 @@
#![allow(missing_docs)] #![allow(missing_docs)]
use anyhow::Result; use anyhow::Result;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tracing::{info, warn}; use tracing::{info, warn};
@ -195,9 +193,7 @@ impl Default for Theme {
/// its contents. /// its contents.
fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> { fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result<()> {
let filename = filename.as_ref(); let filename = filename.as_ref();
let mut buffer = std::fs::read(filename)?;
let mut buffer = Vec::new();
File::open(filename)?.read_to_end(&mut buffer)?;
// We needed the buffer so we'd only overwrite the existing content if we // We needed the buffer so we'd only overwrite the existing content if we
// could successfully load the file into memory. // could successfully load the file into memory.
@ -254,7 +250,7 @@ mod tests {
// "touch" all of the special files so we have empty copies // "touch" all of the special files so we have empty copies
for file in &files { for file in &files {
File::create(&temp.path().join(file)).unwrap(); fs::File::create(&temp.path().join(file)).unwrap();
} }
let got = Theme::new(temp.path()); let got = Theme::new(temp.path());

View file

@ -1,6 +1,6 @@
//! Utility for building and running tests against mdbook. //! Utility for building and running tests against mdbook.
use anyhow::Context; use mdbook_core::utils::fs;
use mdbook_driver::MDBook; use mdbook_driver::MDBook;
use mdbook_driver::init::BookBuilder; use mdbook_driver::init::BookBuilder;
use snapbox::IntoData; use snapbox::IntoData;
@ -76,7 +76,7 @@ impl BookTest {
std::fs::remove_dir_all(&tmp) std::fs::remove_dir_all(&tmp)
.unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}")); .unwrap_or_else(|e| panic!("failed to remove {tmp:?}: {e:?}"));
} }
std::fs::create_dir_all(&tmp).unwrap_or_else(|e| panic!("failed to create {tmp:?}: {e:?}")); fs::create_dir_all(&tmp).unwrap();
tmp tmp
} }
@ -310,7 +310,7 @@ impl BookTest {
/// Change a file's contents in the given path. /// Change a file's contents in the given path.
pub fn change_file(&mut self, path: impl AsRef<Path>, body: &str) -> &mut Self { pub fn change_file(&mut self, path: impl AsRef<Path>, body: &str) -> &mut Self {
let path = self.dir.join(path); let path = self.dir.join(path);
std::fs::write(&path, body).unwrap_or_else(|e| panic!("failed to write {path:?}: {e:?}")); fs::write(&path, body).unwrap();
self self
} }
@ -342,9 +342,9 @@ impl BookTest {
let rs = self.dir.join(path).with_extension("rs"); let rs = self.dir.join(path).with_extension("rs");
let parent = rs.parent().unwrap(); let parent = rs.parent().unwrap();
if !parent.exists() { if !parent.exists() {
std::fs::create_dir_all(&parent).unwrap(); fs::create_dir_all(&parent).unwrap();
} }
std::fs::write(&rs, src).unwrap_or_else(|e| panic!("failed to write {rs:?}: {e:?}")); fs::write(&rs, src).unwrap();
let status = std::process::Command::new("rustc") let status = std::process::Command::new("rustc")
.arg(&rs) .arg(&rs)
.current_dir(&parent) .current_dir(&parent)
@ -551,9 +551,7 @@ fn assert(root: &Path) -> snapbox::Assert {
#[track_caller] #[track_caller]
pub fn read_to_string<P: AsRef<Path>>(path: P) -> String { pub fn read_to_string<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref(); let path = path.as_ref();
std::fs::read_to_string(path) fs::read_to_string(path).unwrap()
.with_context(|| format!("could not read file {path:?}"))
.unwrap()
} }
/// Returns the first path from the given glob pattern. /// Returns the first path from the given glob pattern.

View file

@ -24,7 +24,7 @@ fn basic_build() {
fn failure_on_missing_file() { fn failure_on_missing_file() {
BookTest::from_dir("build/missing_file").run("build", |cmd| { BookTest::from_dir("build/missing_file").run("build", |cmd| {
cmd.expect_failure().expect_stderr(str![[r#" cmd.expect_failure().expect_stderr(str![[r#"
ERROR Chapter file not found, ./chapter_1.md ERROR failed to read chapter `./chapter_1.md`
[TAB]Caused by: [NOT_FOUND] [TAB]Caused by: [NOT_FOUND]
"#]]); "#]]);