Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed zero exit code when plugin not found #1122

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions book-example/src/for_developers/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,36 @@ generation or a warning).
All environment variables are passed through to the backend, allowing you to use
the usual `RUST_LOG` to control logging verbosity.

## Handling missing backends

If you enable a backend that isn't installed, the default behavior is to throw an error:

```text
The command wasn't found, is the "wordcount" backend installed?
```

This behavior can be changed by marking the backend as optional.

```diff
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]

[output.html]

[output.wordcount]
command = "python /path/to/wordcount.py"
+ optional = true
```

This demotes the error to a warning, and it will instead look like this:

```text
The command was not found, but was marked as optional.
Command: wordcount
```


## Wrapping Up

Expand Down
14 changes: 10 additions & 4 deletions book-example/src/format/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,11 +306,17 @@ specify which preprocessors should run before the Markdown renderer.
A custom renderer can be enabled by adding a `[output.foo]` table to your
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
rendering.
rendering. See the [alternative backends] chapter for more detail.

Custom renderers will have access to all configuration within their table
(i.e. anything under `[output.foo]`), and the command to be invoked can be
manually specified with the `command` field.
The custom renderer has access to all the fields within its table (i.e.
anything under `[output.foo]`). mdBook checks for two common fields:

- **command:** The command to execute for this custom renderer. Defaults to
the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`).
- **optional:** If `true`, then the command will be ignored if it is not
installed, otherwise mdBook will fail with an error. Defaults to `false`.

[alternative backends]: ../for_developers/backends.md

## Environment Variables

Expand Down
50 changes: 38 additions & 12 deletions src/renderer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ mod markdown_renderer;

use shlex::Shlex;
use std::fs;
use std::io::{self, Read};
use std::io::{self, ErrorKind, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};

use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
use toml::Value;

/// An arbitrary `mdbook` backend.
///
Expand Down Expand Up @@ -149,6 +150,41 @@ impl CmdRenderer {
}
}

impl CmdRenderer {
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
match error.kind() {
ErrorKind::NotFound => {
// Look for "output.{self.name}.optional".
// If it exists and is true, treat this as a warning.
// Otherwise, fail the build.

let optional_key = format!("output.{}.optional", self.name);

let is_optional = match ctx.config.get(&optional_key) {
Some(Value::Boolean(value)) => *value,
_ => false,
};

if is_optional {
warn!(
"The command `{}` for backend `{}` was not found, \
but was marked as optional.",
self.cmd, self.name
);
return Ok(());
} else {
error!(
"The command `{}` wasn't found, is the `{}` backend installed?",
self.cmd, self.name
);
}
}
_ => {}
}
Err(error).chain_err(|| "Unable to start the backend")?
}
}

impl Renderer for CmdRenderer {
fn name(&self) -> &str {
&self.name
Expand All @@ -168,17 +204,7 @@ impl Renderer for CmdRenderer {
.spawn()
{
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!(
"The command wasn't found, is the \"{}\" backend installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
Err(e) => return self.handle_render_command_error(ctx, e),
};

{
Expand Down
29 changes: 22 additions & 7 deletions tests/alternative_backends.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,33 @@ use tempfile::{Builder as TempFileBuilder, TempDir};

#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);

md.build().unwrap();
}

#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);

md.build().unwrap_err();
}

#[test]
fn missing_backends_arent_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn");
fn missing_backends_are_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
assert!(md.build().is_err());
}

#[test]
fn missing_optional_backends_are_not_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
assert!(md.build().is_ok());
}

#[test]
fn alternate_backend_with_arguments() {
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!");
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);

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

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

assert!(!out_file.exists());
md.build().unwrap();
Expand All @@ -66,14 +71,24 @@ fn backends_receive_render_context_via_stdin() {
assert!(got.is_ok());
}

fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
fn dummy_book_with_backend(
name: &str,
command: &str,
backend_is_optional: bool,
) -> (MDBook, TempDir) {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();

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

if backend_is_optional {
config
.set(format!("output.{}.optional", name), true)
.unwrap();
}

let md = MDBook::init(temp.path())
.with_config(config)
.build()
Expand Down