]> git.lizzy.rs Git - rust.git/blob - src/tools/rustbook/src/main.rs
01f324f0c5a0a653057e5c6690d39c274d7bb6a6
[rust.git] / src / tools / rustbook / src / main.rs
1 use clap::crate_version;
2
3 use std::env;
4 use std::path::{Path, PathBuf};
5
6 use clap::{App, AppSettings, ArgMatches, SubCommand};
7
8 use mdbook::errors::Result as Result3;
9 use mdbook::MDBook;
10
11 fn main() {
12     let d_message = "-d, --dest-dir=[dest-dir]
13 'The output directory for your book{n}(Defaults to ./book when omitted)'";
14     let dir_message = "[dir]
15 'A directory for your book{n}(Defaults to Current Directory when omitted)'";
16
17     let matches = App::new("rustbook")
18         .about("Build a book with mdBook")
19         .author("Steve Klabnik <steve@steveklabnik.com>")
20         .version(&*format!("v{}", crate_version!()))
21         .setting(AppSettings::SubcommandRequired)
22         .subcommand(
23             SubCommand::with_name("build")
24                 .about("Build the book from the markdown files")
25                 .arg_from_usage(d_message)
26                 .arg_from_usage(dir_message),
27         )
28         .subcommand(
29             SubCommand::with_name("test")
30                 .about("Tests that a book's Rust code samples compile")
31                 .arg_from_usage(dir_message),
32         )
33         .subcommand(
34             SubCommand::with_name("linkcheck")
35                 .about("Run linkcheck with mdBook 3")
36                 .arg_from_usage(dir_message),
37         )
38         .get_matches();
39
40     // Check which subcomamnd the user ran...
41     match matches.subcommand() {
42         ("build", Some(sub_matches)) => {
43             if let Err(e) = build(sub_matches) {
44                 handle_error(e);
45             }
46         }
47         ("test", Some(sub_matches)) => {
48             if let Err(e) = test(sub_matches) {
49                 handle_error(e);
50             }
51         }
52         ("linkcheck", Some(sub_matches)) => {
53             #[cfg(feature = "linkcheck")]
54             {
55                 let (diags, files) = linkcheck(sub_matches).expect("Error while linkchecking.");
56                 if !diags.is_empty() {
57                     let color = codespan_reporting::term::termcolor::ColorChoice::Auto;
58                     let mut writer =
59                         codespan_reporting::term::termcolor::StandardStream::stderr(color);
60                     let cfg = codespan_reporting::term::Config::default();
61
62                     for diag in diags {
63                         codespan_reporting::term::emit(&mut writer, &cfg, &files, &diag)
64                             .expect("Unable to emit linkcheck error.");
65                     }
66
67                     std::process::exit(101);
68                 }
69             }
70
71             #[cfg(not(feature = "linkcheck"))]
72             {
73                 // This avoids the `unused_binding` lint.
74                 println!(
75                     "mdbook-linkcheck is disabled, but arguments were passed: {:?}",
76                     sub_matches
77                 );
78             }
79         }
80         (_, _) => unreachable!(),
81     };
82 }
83
84 #[cfg(feature = "linkcheck")]
85 pub fn linkcheck(
86     args: &ArgMatches<'_>,
87 ) -> Result<(Vec<codespan_reporting::diagnostic::Diagnostic>, codespan::Files), failure::Error> {
88     use mdbook_linkcheck::Reason;
89
90     let book_dir = get_book_dir(args);
91     let src_dir = book_dir.join("src");
92     let book = MDBook::load(&book_dir).unwrap();
93     let linkck_cfg = mdbook_linkcheck::get_config(&book.config)?;
94     let mut files = codespan::Files::new();
95     let target_files = mdbook_linkcheck::load_files_into_memory(&book.book, &mut files);
96     let cache = mdbook_linkcheck::Cache::default();
97
98     let (links, incomplete) = mdbook_linkcheck::extract_links(target_files, &files);
99
100     let outcome =
101         mdbook_linkcheck::validate(&links, &linkck_cfg, &src_dir, &cache, &files, incomplete)?;
102
103     let mut is_real_error = false;
104
105     for link in outcome.invalid_links.iter() {
106         match &link.reason {
107             Reason::FileNotFound | Reason::TraversesParentDirectories => {
108                 is_real_error = true;
109             }
110             Reason::UnsuccessfulServerResponse(status) => {
111                 if status.is_client_error() {
112                     is_real_error = true;
113                 } else {
114                     eprintln!("Unsuccessful server response for link `{}`", link.link.uri);
115                 }
116             }
117             Reason::Client(err) => {
118                 if err.is_timeout() {
119                     eprintln!("Timeout for link `{}`", link.link.uri);
120                 } else if err.is_server_error() {
121                     eprintln!("Server error for link `{}`", link.link.uri);
122                 } else if !err.is_http() {
123                     eprintln!("Non-HTTP-related error for link: {} {}", link.link.uri, err);
124                 } else {
125                     is_real_error = true;
126                 }
127             }
128         }
129     }
130
131     if is_real_error {
132         Ok((outcome.generate_diagnostics(&files, linkck_cfg.warning_policy), files))
133     } else {
134         Ok((vec![], files))
135     }
136 }
137
138 // Build command implementation
139 pub fn build(args: &ArgMatches<'_>) -> Result3<()> {
140     let book_dir = get_book_dir(args);
141     let mut book = MDBook::load(&book_dir)?;
142
143     // Set this to allow us to catch bugs in advance.
144     book.config.build.create_missing = false;
145
146     if let Some(dest_dir) = args.value_of("dest-dir") {
147         book.config.build.build_dir = PathBuf::from(dest_dir);
148     }
149
150     book.build()?;
151
152     Ok(())
153 }
154
155 fn test(args: &ArgMatches<'_>) -> Result3<()> {
156     let book_dir = get_book_dir(args);
157     let mut book = MDBook::load(&book_dir)?;
158     book.test(vec![])
159 }
160
161 fn get_book_dir(args: &ArgMatches<'_>) -> PathBuf {
162     if let Some(dir) = args.value_of("dir") {
163         // Check if path is relative from current dir, or absolute...
164         let p = Path::new(dir);
165         if p.is_relative() { env::current_dir().unwrap().join(dir) } else { p.to_path_buf() }
166     } else {
167         env::current_dir().unwrap()
168     }
169 }
170
171 fn handle_error(error: mdbook::errors::Error) -> ! {
172     eprintln!("Error: {}", error);
173
174     for cause in error.iter().skip(1) {
175         eprintln!("\tCaused By: {}", cause);
176     }
177
178     ::std::process::exit(101);
179 }