| 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); | |
| } |