Skip to content

Commit 03ba7d9

Browse files
authored
Merge pull request #2797 from ehuss/optional-preprocessor
Add `optional` field for preprocessors
2 parents 5d44ef9 + d7892f5 commit 03ba7d9

File tree

14 files changed

+160
-88
lines changed

14 files changed

+160
-88
lines changed

crates/mdbook-driver/src/builtin_preprocessors/cmd.rs

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{Context, Result, ensure};
22
use log::{debug, trace, warn};
33
use mdbook_core::book::Book;
44
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
5-
use std::io::{self, Write};
5+
use std::io::Write;
66
use std::path::PathBuf;
77
use std::process::{Child, Stdio};
88

@@ -34,12 +34,18 @@ pub struct CmdPreprocessor {
3434
name: String,
3535
cmd: String,
3636
root: PathBuf,
37+
optional: bool,
3738
}
3839

3940
impl CmdPreprocessor {
4041
/// Create a new `CmdPreprocessor`.
41-
pub fn new(name: String, cmd: String, root: PathBuf) -> CmdPreprocessor {
42-
CmdPreprocessor { name, cmd, root }
42+
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
43+
CmdPreprocessor {
44+
name,
45+
cmd,
46+
root,
47+
optional,
48+
}
4349
}
4450

4551
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
@@ -75,18 +81,29 @@ impl Preprocessor for CmdPreprocessor {
7581
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
7682
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
7783

78-
let mut child = cmd
84+
let mut child = match cmd
7985
.stdin(Stdio::piped())
8086
.stdout(Stdio::piped())
8187
.stderr(Stdio::inherit())
8288
.current_dir(&self.root)
8389
.spawn()
84-
.with_context(|| {
85-
format!(
86-
"Unable to start the \"{}\" preprocessor. Is it installed?",
87-
self.name()
88-
)
89-
})?;
90+
{
91+
Ok(c) => c,
92+
Err(e) => {
93+
crate::handle_command_error(
94+
e,
95+
self.optional,
96+
"preprocessor",
97+
"preprocessor",
98+
&self.name,
99+
&self.cmd,
100+
)?;
101+
// This should normally not be reached, since the validation
102+
// for NotFound should have already happened when running the
103+
// "supports" command.
104+
return Ok(book);
105+
}
106+
};
90107

91108
self.write_input_to_child(&mut child, &book, ctx);
92109

@@ -114,46 +131,37 @@ impl Preprocessor for CmdPreprocessor {
114131
})
115132
}
116133

117-
fn supports_renderer(&self, renderer: &str) -> bool {
134+
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
118135
debug!(
119136
"Checking if the \"{}\" preprocessor supports \"{}\"",
120137
self.name(),
121138
renderer
122139
);
123140

124-
let mut cmd = match crate::compose_command(&self.cmd, &self.root) {
125-
Ok(c) => c,
126-
Err(e) => {
127-
warn!(
128-
"Unable to create the command for the \"{}\" preprocessor, {}",
129-
self.name(),
130-
e
131-
);
132-
return false;
133-
}
134-
};
141+
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
135142

136-
let outcome = cmd
143+
match cmd
137144
.arg("supports")
138145
.arg(renderer)
139146
.stdin(Stdio::null())
140147
.stdout(Stdio::inherit())
141148
.stderr(Stdio::inherit())
142149
.current_dir(&self.root)
143150
.status()
144-
.map(|status| status.code() == Some(0));
145-
146-
if let Err(ref e) = outcome {
147-
if e.kind() == io::ErrorKind::NotFound {
148-
warn!(
149-
"The command wasn't found, is the \"{}\" preprocessor installed?",
150-
self.name
151-
);
152-
warn!("\tCommand: {}", self.cmd);
151+
{
152+
Ok(status) => Ok(status.code() == Some(0)),
153+
Err(e) => {
154+
crate::handle_command_error(
155+
e,
156+
self.optional,
157+
"preprocessor",
158+
"preprocessor",
159+
&self.name,
160+
&self.cmd,
161+
)?;
162+
Ok(false)
153163
}
154164
}
155-
156-
outcome.unwrap_or(false)
157165
}
158166
}
159167

@@ -171,7 +179,12 @@ mod tests {
171179
#[test]
172180
fn round_trip_write_and_parse_input() {
173181
let md = guide();
174-
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string(), md.root.clone());
182+
let cmd = CmdPreprocessor::new(
183+
"test".to_string(),
184+
"test".to_string(),
185+
md.root.clone(),
186+
false,
187+
);
175188
let ctx = PreprocessorContext::new(
176189
md.root.clone(),
177190
md.config.clone(),

crates/mdbook-driver/src/builtin_renderers/mod.rs

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use anyhow::{Context, Result, bail};
66
use log::{error, info, trace, warn};
77
use mdbook_renderer::{RenderContext, Renderer};
88
use std::fs;
9-
use std::io::{self, ErrorKind};
109
use std::process::Stdio;
1110

1211
pub use self::markdown_renderer::MarkdownRenderer;
@@ -49,41 +48,6 @@ impl CmdRenderer {
4948
}
5049
}
5150

52-
impl CmdRenderer {
53-
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
54-
if let ErrorKind::NotFound = error.kind() {
55-
// Look for "output.{self.name}.optional".
56-
// If it exists and is true, treat this as a warning.
57-
// Otherwise, fail the build.
58-
59-
let optional_key = format!("output.{}.optional", self.name);
60-
61-
let is_optional = match ctx.config.get(&optional_key) {
62-
Ok(Some(value)) => value,
63-
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
64-
Ok(None) => false,
65-
};
66-
67-
if is_optional {
68-
warn!(
69-
"The command `{}` for backend `{}` was not found, \
70-
but was marked as optional.",
71-
self.cmd, self.name
72-
);
73-
return Ok(());
74-
} else {
75-
error!(
76-
"The command `{0}` wasn't found, is the \"{1}\" backend installed? \
77-
If you want to ignore this error when the \"{1}\" backend is not installed, \
78-
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
79-
self.cmd, self.name
80-
);
81-
}
82-
}
83-
Err(error).with_context(|| "Unable to start the backend")?
84-
}
85-
}
86-
8751
impl Renderer for CmdRenderer {
8852
fn name(&self) -> &str {
8953
&self.name
@@ -92,6 +56,13 @@ impl Renderer for CmdRenderer {
9256
fn render(&self, ctx: &RenderContext) -> Result<()> {
9357
info!("Invoking the \"{}\" renderer", self.name);
9458

59+
let optional_key = format!("output.{}.optional", self.name);
60+
let optional = match ctx.config.get(&optional_key) {
61+
Ok(Some(value)) => value,
62+
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
63+
Ok(None) => false,
64+
};
65+
9566
let _ = fs::create_dir_all(&ctx.destination);
9667

9768
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
@@ -103,7 +74,11 @@ impl Renderer for CmdRenderer {
10374
.spawn()
10475
{
10576
Ok(c) => c,
106-
Err(e) => return self.handle_render_command_error(ctx, e),
77+
Err(e) => {
78+
return crate::handle_command_error(
79+
e, optional, "output", "backend", &self.name, &self.cmd,
80+
);
81+
}
10782
};
10883

10984
let mut stdin = child.stdin.take().expect("Child has stdin");

crates/mdbook-driver/src/lib.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ pub mod init;
6464
mod load;
6565
mod mdbook;
6666

67-
use anyhow::{Result, bail};
67+
use anyhow::{Context, Result, bail};
68+
use log::{error, warn};
6869
pub use mdbook::MDBook;
6970
pub use mdbook_core::{book, config, errors};
7071
use shlex::Shlex;
@@ -95,3 +96,30 @@ fn compose_command(cmd: &str, root: &Path) -> Result<Command> {
9596

9697
Ok(cmd)
9798
}
99+
100+
/// Handles a failure for a preprocessor or renderer.
101+
fn handle_command_error(
102+
error: std::io::Error,
103+
optional: bool,
104+
key: &str,
105+
what: &str,
106+
name: &str,
107+
cmd: &str,
108+
) -> Result<()> {
109+
if let std::io::ErrorKind::NotFound = error.kind() {
110+
if optional {
111+
warn!(
112+
"The command `{cmd}` for {what} `{name}` was not found, \
113+
but is marked as optional.",
114+
);
115+
return Ok(());
116+
} else {
117+
error!(
118+
"The command `{cmd}` wasn't found, is the `{name}` {what} installed? \
119+
If you want to ignore this error when the `{name}` {what} is not installed, \
120+
set `optional = true` in the `[{key}.{name}]` section of the book.toml configuration file.",
121+
);
122+
}
123+
}
124+
Err(error).with_context(|| format!("Unable to run the {what} `{name}`"))?
125+
}

crates/mdbook-driver/src/mdbook.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ struct PreprocessorConfig {
437437
before: Vec<String>,
438438
#[serde(default)]
439439
after: Vec<String>,
440+
#[serde(default)]
441+
optional: bool,
440442
}
441443

442444
/// Look at the `MDBook` and try to figure out what preprocessors to run.
@@ -513,7 +515,12 @@ fn determine_preprocessors(config: &Config, root: &Path) -> Result<Vec<Box<dyn P
513515
.command
514516
.to_owned()
515517
.unwrap_or_else(|| format!("mdbook-{name}"));
516-
Box::new(CmdPreprocessor::new(name, command, root.to_owned()))
518+
Box::new(CmdPreprocessor::new(
519+
name,
520+
command,
521+
root.to_owned(),
522+
table.optional,
523+
))
517524
}
518525
};
519526
preprocessors.push(preprocessor);
@@ -542,7 +549,7 @@ fn preprocessor_should_run(
542549
) -> Result<bool> {
543550
// default preprocessors should be run by default (if supported)
544551
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
545-
return Ok(preprocessor.supports_renderer(renderer.name()));
552+
return preprocessor.supports_renderer(renderer.name());
546553
}
547554

548555
let key = format!("preprocessor.{}.renderers", preprocessor.name());
@@ -552,7 +559,7 @@ fn preprocessor_should_run(
552559
Ok(Some(explicit_renderers)) => {
553560
Ok(explicit_renderers.iter().any(|name| name == renderer_name))
554561
}
555-
Ok(None) => Ok(preprocessor.supports_renderer(renderer_name)),
562+
Ok(None) => preprocessor.supports_renderer(renderer_name),
556563
Err(e) => bail!("failed to get `{key}`: {e}"),
557564
}
558565
}

crates/mdbook-driver/src/mdbook/tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,8 @@ impl Preprocessor for BoolPreprocessor {
247247
unimplemented!()
248248
}
249249

250-
fn supports_renderer(&self, _renderer: &str) -> bool {
251-
self.0
250+
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
251+
Ok(self.0)
252252
}
253253
}
254254

crates/mdbook-preprocessor/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ pub trait Preprocessor {
3939
/// particular renderer.
4040
///
4141
/// By default, always returns `true`.
42-
fn supports_renderer(&self, _renderer: &str) -> bool {
43-
true
42+
fn supports_renderer(&self, _renderer: &str) -> Result<bool> {
43+
Ok(true)
4444
}
4545
}
4646

examples/nop-preprocessor.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
5959
let renderer = sub_args
6060
.get_one::<String>("renderer")
6161
.expect("Required argument");
62-
let supported = pre.supports_renderer(renderer);
62+
let supported = pre.supports_renderer(renderer).unwrap();
6363

6464
// Signal whether the renderer is supported by exiting with 1 or 0.
6565
if supported {
@@ -105,8 +105,8 @@ mod nop_lib {
105105
Ok(book)
106106
}
107107

108-
fn supports_renderer(&self, renderer: &str) -> bool {
109-
renderer != "not-supported"
108+
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
109+
Ok(renderer != "not-supported")
110110
}
111111
}
112112

guide/src/format/configuration/preprocessors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ be overridden by adding a `command` field.
6464
command = "python random.py"
6565
```
6666

67+
### Optional preprocessors
68+
69+
If you enable a preprocessor that isn't installed, the default behavior is to throw an error.
70+
This behavior can be changed by marking the preprocessor as optional:
71+
72+
```toml
73+
[preprocessor.example]
74+
optional = true
75+
```
76+
77+
This demotes the error to a warning.
78+
6779
## Require A Certain Order
6880

6981
The order in which preprocessors are run can be controlled with the `before` and `after` fields.

0 commit comments

Comments
 (0)