This changes preprocessors so that: - Relative paths in the `command` value are relative to the book root. - The process current directory is the book root. This makes it so that it isn't dependent on the directory where `mdbook` is executed. Fixes https://github.com/rust-lang/mdBook/issues/1424
132 lines
4.6 KiB
Rust
132 lines
4.6 KiB
Rust
//! Built-in renderers.
|
|
//!
|
|
//! The HTML renderer can be found in the [`mdbook_html`] crate.
|
|
|
|
use anyhow::{Context, Result, bail};
|
|
use log::{error, info, trace, warn};
|
|
use mdbook_renderer::{RenderContext, Renderer};
|
|
use std::fs;
|
|
use std::io::{self, ErrorKind};
|
|
use std::process::Stdio;
|
|
|
|
pub use self::markdown_renderer::MarkdownRenderer;
|
|
|
|
mod markdown_renderer;
|
|
|
|
/// A generic renderer which will shell out to an arbitrary executable.
|
|
///
|
|
/// # Rendering Protocol
|
|
///
|
|
/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn
|
|
/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess
|
|
/// as a JSON string (using `serde_json`).
|
|
///
|
|
/// > **Note:** The command used doesn't necessarily need to be a single
|
|
/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass
|
|
/// > in command line arguments, so there's no reason why it couldn't be
|
|
/// > `python /path/to/renderer --from mdbook --to epub`.
|
|
///
|
|
/// Anything the subprocess writes to `stdin` or `stdout` will be passed through
|
|
/// to the user. While this gives the renderer maximum flexibility to output
|
|
/// whatever it wants, to avoid spamming users it is recommended to avoid
|
|
/// unnecessary output.
|
|
///
|
|
/// To help choose the appropriate output level, the `RUST_LOG` environment
|
|
/// variable will be passed through to the subprocess, if set.
|
|
///
|
|
/// If the subprocess wishes to indicate that rendering failed, it should exit
|
|
/// with a non-zero return code.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct CmdRenderer {
|
|
name: String,
|
|
cmd: String,
|
|
}
|
|
|
|
impl CmdRenderer {
|
|
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
|
|
pub fn new(name: String, cmd: String) -> CmdRenderer {
|
|
CmdRenderer { name, cmd }
|
|
}
|
|
}
|
|
|
|
impl CmdRenderer {
|
|
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
|
|
if let ErrorKind::NotFound = error.kind() {
|
|
// 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) {
|
|
Ok(Some(value)) => value,
|
|
Err(e) => bail!("expected bool for `{optional_key}`: {e}"),
|
|
Ok(None) => 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 `{0}` wasn't found, is the \"{1}\" backend installed? \
|
|
If you want to ignore this error when the \"{1}\" backend is not installed, \
|
|
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
|
|
self.cmd, self.name
|
|
);
|
|
}
|
|
}
|
|
Err(error).with_context(|| "Unable to start the backend")?
|
|
}
|
|
}
|
|
|
|
impl Renderer for CmdRenderer {
|
|
fn name(&self) -> &str {
|
|
&self.name
|
|
}
|
|
|
|
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
|
info!("Invoking the \"{}\" renderer", self.name);
|
|
|
|
let _ = fs::create_dir_all(&ctx.destination);
|
|
|
|
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
|
let mut child = match cmd
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::inherit())
|
|
.stderr(Stdio::inherit())
|
|
.current_dir(&ctx.destination)
|
|
.spawn()
|
|
{
|
|
Ok(c) => c,
|
|
Err(e) => return self.handle_render_command_error(ctx, e),
|
|
};
|
|
|
|
let mut stdin = child.stdin.take().expect("Child has stdin");
|
|
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
|
// Looks like the backend hung up before we could finish
|
|
// sending it the render context. Log the error and keep going
|
|
warn!("Error writing the RenderContext to the backend, {}", e);
|
|
}
|
|
|
|
// explicitly close the `stdin` file handle
|
|
drop(stdin);
|
|
|
|
let status = child
|
|
.wait()
|
|
.with_context(|| "Error waiting for the backend to complete")?;
|
|
|
|
trace!("{} exited with output: {:?}", self.cmd, status);
|
|
|
|
if !status.success() {
|
|
error!("Renderer exited with non-zero return code.");
|
|
bail!("The \"{}\" renderer failed", self.name);
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|