blob: 54105240282054b7dfc6365a8e8aae8d2a1c4c73 [file]
use std::env;
use std::path::{Path, PathBuf};
use clap::{ArgMatches, Command, arg, crate_version};
use mdbook_driver::MDBook;
use mdbook_driver::errors::Result as Result3;
use mdbook_i18n_helpers::preprocessors::Gettext;
use mdbook_spec::Spec;
use mdbook_trpl::{Figure, Listing, Note};
fn main() {
let crate_version = concat!("v", crate_version!());
let filter = tracing_subscriber::EnvFilter::builder()
.with_env_var("MDBOOK_LOG")
.with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into())
.from_env_lossy();
tracing_subscriber::fmt()
.without_time()
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
.with_writer(std::io::stderr)
.with_env_filter(filter)
.with_target(std::env::var_os("MDBOOK_LOG").is_some())
.init();
// env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let d_arg = arg!(-d --"dest-dir" <DEST_DIR>
"The output directory for your book\n(Defaults to ./book when omitted)")
.required(false)
.value_parser(clap::value_parser!(PathBuf));
let l_arg = arg!(-l --"lang" <LANGUAGE>
"The output language")
.required(false)
.value_parser(clap::value_parser!(String));
let root_arg = arg!(--"rust-root" <ROOT_DIR>
"Path to the root of the rust source tree")
.required(false)
.value_parser(clap::value_parser!(PathBuf));
let dir_arg = arg!([dir] "Root directory for the book\n\
(Defaults to the current directory when omitted)")
.value_parser(clap::value_parser!(PathBuf));
// Note: we don't parse this into a `PathBuf` because it is comma separated
// strings *and* we will ultimately pass it into `MDBook::test()`, which
// accepts `Vec<&str>`. Although it is a bit annoying that `-l/--lang` and
// `-L/--library-path` are so close, this is the same set of arguments we
// would pass when invoking mdbook on the CLI, so making them match when
// invoking rustbook makes for good consistency.
let library_path_arg = arg!(
-L --"library-path" <PATHS>
"A comma-separated list of directories to add to the crate search\n\
path when building tests"
)
.required(false)
.value_parser(parse_library_paths);
let matches = Command::new("rustbook")
.about("Build a book with mdBook")
.author("Steve Klabnik <steve@steveklabnik.com>")
.version(crate_version)
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("build")
.about("Build the book from the markdown files")
.arg(d_arg)
.arg(l_arg)
.arg(root_arg)
.arg(&dir_arg),
)
.subcommand(
Command::new("test")
.about("Tests that a book's Rust code samples compile")
.arg(dir_arg)
.arg(library_path_arg),
)
.get_matches();
// Check which subcommand the user ran...
match matches.subcommand() {
Some(("build", sub_matches)) => {
if let Err(e) = build(sub_matches) {
handle_error(e);
}
}
Some(("test", sub_matches)) => {
if let Err(e) = test(sub_matches) {
handle_error(e);
}
}
_ => unreachable!(),
};
}
fn build(args: &ArgMatches) -> Result3<()> {
let book_dir = get_book_dir(args);
let dest_dir = args.get_one::<PathBuf>("dest-dir");
let lang = args.get_one::<String>("lang");
let rust_root = args.get_one::<PathBuf>("rust-root");
let book = load_book(&book_dir, dest_dir, lang, rust_root.cloned())?;
book.build()
}
fn test(args: &ArgMatches) -> Result3<()> {
let book_dir = get_book_dir(args);
let mut book = load_book(&book_dir, None, None, None)?;
let library_paths = args
.try_get_one::<Vec<String>>("library-path")?
.map(|v| v.iter().map(|s| s.as_str()).collect::<Vec<&str>>())
.unwrap_or_default();
book.test(library_paths)
}
fn get_book_dir(args: &ArgMatches) -> PathBuf {
if let Some(p) = args.get_one::<PathBuf>("dir") {
// Check if path is relative from current dir, or absolute...
if p.is_relative() { env::current_dir().unwrap().join(p) } else { p.to_path_buf() }
} else {
env::current_dir().unwrap()
}
}
fn load_book(
book_dir: &Path,
dest_dir: Option<&PathBuf>,
lang: Option<&String>,
rust_root: Option<PathBuf>,
) -> Result3<MDBook> {
let mut book = MDBook::load(book_dir)?;
book.config.set("output.html.input-404", "").unwrap();
book.config.set("output.html.hash-files", true).unwrap();
if let Some(lang) = lang {
let gettext = Gettext;
book.with_preprocessor(gettext);
book.config.set("book.language", lang).unwrap();
}
// Set this to allow us to catch bugs in advance.
book.config.build.create_missing = false;
if let Some(dest_dir) = dest_dir {
book.config.build.build_dir = dest_dir.into();
}
// NOTE: Replacing preprocessors using this technique causes error
// messages to be displayed when the original preprocessor doesn't work
// (but it otherwise succeeds).
//
// This should probably be fixed in mdbook to remove the existing
// preprocessor, or this should modify the config and use
// MDBook::load_with_config.
if book.config.contains_key("preprocessor.trpl-note") {
book.with_preprocessor(Note);
}
if book.config.contains_key("preprocessor.trpl-listing") {
book.with_preprocessor(Listing);
}
if book.config.contains_key("preprocessor.trpl-figure") {
book.with_preprocessor(Figure);
}
if book.config.contains_key("preprocessor.spec") {
book.with_preprocessor(Spec::new(rust_root)?);
}
Ok(book)
}
fn parse_library_paths(input: &str) -> Result<Vec<String>, String> {
Ok(input.split(",").map(String::from).collect())
}
fn handle_error(error: mdbook_driver::errors::Error) -> ! {
eprintln!("Error: {}", error);
for cause in error.chain().skip(1) {
eprintln!("\tCaused By: {}", cause);
}
std::process::exit(101);
}