Skip to content

Commit 11f95f7

Browse files
authored
Merge pull request #1122 from segfaultsourcery/zero-exit-code-when-plugin-not-found-893
Fixed zero exit code when plugin not found
2 parents 712362f + 2732c5e commit 11f95f7

File tree

4 files changed

+100
-23
lines changed

4 files changed

+100
-23
lines changed

book-example/src/for_developers/backends.md

+30
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,36 @@ generation or a warning).
329329
All environment variables are passed through to the backend, allowing you to use
330330
the usual `RUST_LOG` to control logging verbosity.
331331

332+
## Handling missing backends
333+
334+
If you enable a backend that isn't installed, the default behavior is to throw an error:
335+
336+
```text
337+
The command wasn't found, is the "wordcount" backend installed?
338+
```
339+
340+
This behavior can be changed by marking the backend as optional.
341+
342+
```diff
343+
[book]
344+
title = "mdBook Documentation"
345+
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
346+
authors = ["Mathieu David", "Michael-F-Bryan"]
347+
348+
[output.html]
349+
350+
[output.wordcount]
351+
command = "python /path/to/wordcount.py"
352+
+ optional = true
353+
```
354+
355+
This demotes the error to a warning, and it will instead look like this:
356+
357+
```text
358+
The command was not found, but was marked as optional.
359+
Command: wordcount
360+
```
361+
332362

333363
## Wrapping Up
334364

book-example/src/format/config.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -306,11 +306,17 @@ specify which preprocessors should run before the Markdown renderer.
306306
A custom renderer can be enabled by adding a `[output.foo]` table to your
307307
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
308308
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
309-
rendering.
309+
rendering. See the [alternative backends] chapter for more detail.
310310

311-
Custom renderers will have access to all configuration within their table
312-
(i.e. anything under `[output.foo]`), and the command to be invoked can be
313-
manually specified with the `command` field.
311+
The custom renderer has access to all the fields within its table (i.e.
312+
anything under `[output.foo]`). mdBook checks for two common fields:
313+
314+
- **command:** The command to execute for this custom renderer. Defaults to
315+
the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`).
316+
- **optional:** If `true`, then the command will be ignored if it is not
317+
installed, otherwise mdBook will fail with an error. Defaults to `false`.
318+
319+
[alternative backends]: ../for_developers/backends.md
314320

315321
## Environment Variables
316322

src/renderer/mod.rs

+38-12
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ mod markdown_renderer;
1919

2020
use shlex::Shlex;
2121
use std::fs;
22-
use std::io::{self, Read};
22+
use std::io::{self, ErrorKind, Read};
2323
use std::path::PathBuf;
2424
use std::process::{Command, Stdio};
2525

2626
use crate::book::Book;
2727
use crate::config::Config;
2828
use crate::errors::*;
29+
use toml::Value;
2930

3031
/// An arbitrary `mdbook` backend.
3132
///
@@ -149,6 +150,41 @@ impl CmdRenderer {
149150
}
150151
}
151152

153+
impl CmdRenderer {
154+
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
155+
match error.kind() {
156+
ErrorKind::NotFound => {
157+
// Look for "output.{self.name}.optional".
158+
// If it exists and is true, treat this as a warning.
159+
// Otherwise, fail the build.
160+
161+
let optional_key = format!("output.{}.optional", self.name);
162+
163+
let is_optional = match ctx.config.get(&optional_key) {
164+
Some(Value::Boolean(value)) => *value,
165+
_ => false,
166+
};
167+
168+
if is_optional {
169+
warn!(
170+
"The command `{}` for backend `{}` was not found, \
171+
but was marked as optional.",
172+
self.cmd, self.name
173+
);
174+
return Ok(());
175+
} else {
176+
error!(
177+
"The command `{}` wasn't found, is the `{}` backend installed?",
178+
self.cmd, self.name
179+
);
180+
}
181+
}
182+
_ => {}
183+
}
184+
Err(error).chain_err(|| "Unable to start the backend")?
185+
}
186+
}
187+
152188
impl Renderer for CmdRenderer {
153189
fn name(&self) -> &str {
154190
&self.name
@@ -168,17 +204,7 @@ impl Renderer for CmdRenderer {
168204
.spawn()
169205
{
170206
Ok(c) => c,
171-
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
172-
warn!(
173-
"The command wasn't found, is the \"{}\" backend installed?",
174-
self.name
175-
);
176-
warn!("\tCommand: {}", self.cmd);
177-
return Ok(());
178-
}
179-
Err(e) => {
180-
return Err(e).chain_err(|| "Unable to start the backend")?;
181-
}
207+
Err(e) => return self.handle_render_command_error(ctx, e),
182208
};
183209

184210
{

tests/alternative_backends.rs

+22-7
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,33 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
88

99
#[test]
1010
fn passing_alternate_backend() {
11-
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
11+
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
1212

1313
md.build().unwrap();
1414
}
1515

1616
#[test]
1717
fn failing_alternate_backend() {
18-
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
18+
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
1919

2020
md.build().unwrap_err();
2121
}
2222

2323
#[test]
24-
fn missing_backends_arent_fatal() {
25-
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn");
24+
fn missing_backends_are_fatal() {
25+
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
26+
assert!(md.build().is_err());
27+
}
2628

29+
#[test]
30+
fn missing_optional_backends_are_not_fatal() {
31+
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
2732
assert!(md.build().is_ok());
2833
}
2934

3035
#[test]
3136
fn alternate_backend_with_arguments() {
32-
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!");
37+
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
3338

3439
md.build().unwrap();
3540
}
@@ -56,7 +61,7 @@ fn backends_receive_render_context_via_stdin() {
5661
let out_file = temp.path().join("out.txt");
5762
let cmd = tee_command(&out_file);
5863

59-
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd);
64+
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
6065

6166
assert!(!out_file.exists());
6267
md.build().unwrap();
@@ -66,14 +71,24 @@ fn backends_receive_render_context_via_stdin() {
6671
assert!(got.is_ok());
6772
}
6873

69-
fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
74+
fn dummy_book_with_backend(
75+
name: &str,
76+
command: &str,
77+
backend_is_optional: bool,
78+
) -> (MDBook, TempDir) {
7079
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
7180

7281
let mut config = Config::default();
7382
config
7483
.set(format!("output.{}.command", name), command)
7584
.unwrap();
7685

86+
if backend_is_optional {
87+
config
88+
.set(format!("output.{}.optional", name), true)
89+
.unwrap();
90+
}
91+
7792
let md = MDBook::init(temp.path())
7893
.with_config(config)
7994
.build()

0 commit comments

Comments
 (0)