Repository: rust-lang/mdBook (2025-08-18T23:29:21Z)

Issue:
dest-dir should be relative to the current dir and not to the book root
`mdbook build -d ./my_out_dir guide` generates the book in `guide/my_out_dir` and not in the expected `my_out_dir`. `mdbook build -d /tmp/out_dir guide` otherwise doesn't build the book in `guide/tmp/out_dir`, but in `/tmp/out_dir`. 

This can lead to extremely confusing situation, e.g. `mdbook build -d ./out_dir guide` and `mdbook build -d $(pwd)/out_dir guide` build to different locations. 


Discussion:
Fixing this would be a (minor) breaking change -- it could break people's CI. I forgot to change this for 0.2, but hopefully adding it to a milestone will help me remember.
+1. I find this extremely confusing.
Old one, but definitely very confusing.  



Patch:
diff --git a/crates/mdbook-core/src/config.rs b/crates/mdbook-core/src/config.rs
index c53d03a3e1..771204822b 100644
--- a/crates/mdbook-core/src/config.rs
+++ b/crates/mdbook-core/src/config.rs
@@ -366,7 +366,7 @@ impl TextDirection {
 #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
 #[non_exhaustive]
 pub struct BuildConfig {
-    /// Where to put built artefacts relative to the book's root directory.
+    /// Where to put built artifacts relative to the book's root directory.
     pub build_dir: PathBuf,
     /// Should non-existent markdown files specified in `SUMMARY.md` be created
     /// if they don't exist?
diff --git a/guide/src/cli/build.md b/guide/src/cli/build.md
index 36e053fd7c..a342741798 100644
--- a/guide/src/cli/build.md
+++ b/guide/src/cli/build.md
@@ -30,7 +30,7 @@ your default web browser after building it.
 #### `--dest-dir`
 
 The `--dest-dir` (`-d`) option allows you to change the output directory for the
-book. Relative paths are interpreted relative to the book's root directory. If
+book. Relative paths are interpreted relative to the current directory. If
 not specified it will default to the value of the `build.build-dir` key in
 `book.toml`, or to `./book`.
 
diff --git a/guide/src/cli/clean.md b/guide/src/cli/clean.md
index f09328054a..a2f94b62b4 100644
--- a/guide/src/cli/clean.md
+++ b/guide/src/cli/clean.md
@@ -20,7 +20,7 @@ mdbook clean path/to/book
 
 The `--dest-dir` (`-d`) option allows you to override the book's output
 directory, which will be deleted by this command. Relative paths are interpreted
-relative to the book's root directory. If not specified it will default to the
+relative to the current directory. If not specified it will default to the
 value of the `build.build-dir` key in `book.toml`, or to `./book`.
 
 ```bash
diff --git a/guide/src/cli/serve.md b/guide/src/cli/serve.md
index 4603df8e76..43aa42ecfa 100644
--- a/guide/src/cli/serve.md
+++ b/guide/src/cli/serve.md
@@ -40,7 +40,7 @@ default web browser after starting the server.
 #### `--dest-dir`
 
 The `--dest-dir` (`-d`) option allows you to change the output directory for the
-book. Relative paths are interpreted relative to the book's root directory. If
+book. Relative paths are interpreted relative to the current directory. If
 not specified it will default to the value of the `build.build-dir` key in
 `book.toml`, or to `./book`.
 
diff --git a/guide/src/cli/watch.md b/guide/src/cli/watch.md
index be2f5be450..364dd9e571 100644
--- a/guide/src/cli/watch.md
+++ b/guide/src/cli/watch.md
@@ -23,7 +23,7 @@ your default web browser.
 #### `--dest-dir`
 
 The `--dest-dir` (`-d`) option allows you to change the output directory for the
-book. Relative paths are interpreted relative to the book's root directory. If
+book. Relative paths are interpreted relative to the current directory. If
 not specified it will default to the value of the `build.build-dir` key in
 `book.toml`, or to `./book`.
 
diff --git a/src/cmd/build.rs b/src/cmd/build.rs
index a04adc5f07..b4ac48574b 100644
--- a/src/cmd/build.rs
+++ b/src/cmd/build.rs
@@ -2,7 +2,6 @@ use super::command_prelude::*;
 use crate::{get_book_dir, open};
 use anyhow::Result;
 use mdbook_driver::MDBook;
-use std::path::PathBuf;
 
 // Create clap subcommand arguments
 pub fn make_subcommand() -> Command {
@@ -18,9 +17,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
     let book_dir = get_book_dir(args);
     let mut book = MDBook::load(book_dir)?;
 
-    if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
-        book.config.build.build_dir = dest_dir.into();
-    }
+    set_dest_dir(args, &mut book);
 
     book.build()?;
 
diff --git a/src/cmd/clean.rs b/src/cmd/clean.rs
index a07bb2ae6e..1bea7da043 100644
--- a/src/cmd/clean.rs
+++ b/src/cmd/clean.rs
@@ -21,7 +21,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
     let book = MDBook::load(book_dir)?;
 
     let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") {
-        Some(dest_dir) => dest_dir.into(),
+        Some(dest_dir) => std::env::current_dir()
+            .expect("current dir should be valid")
+            .join(dest_dir),
         None => book.root.join(&book.config.build.build_dir),
     };
 
diff --git a/src/cmd/command_prelude.rs b/src/cmd/command_prelude.rs
index d5df3af9f2..b2a91ae972 100644
--- a/src/cmd/command_prelude.rs
+++ b/src/cmd/command_prelude.rs
@@ -1,6 +1,7 @@
 //! Helpers for building the command-line arguments for commands.
 
 pub use clap::{Arg, ArgMatches, Command, arg};
+use mdbook_driver::MDBook;
 use std::path::PathBuf;
 
 pub trait CommandExt: Sized {
@@ -15,7 +16,7 @@ pub trait CommandExt: Sized {
                 .value_parser(clap::value_parser!(PathBuf))
                 .help(
                     "Output directory for the book\n\
-                    Relative paths are interpreted relative to the book's root directory.\n\
+                    Relative paths are interpreted relative to the current directory.\n\
                     If omitted, mdBook uses build.build-dir from book.toml \
                     or defaults to `./book`.",
                 ),
@@ -57,3 +58,12 @@ impl CommandExt for Command {
         self.arg(arg)
     }
 }
+
+pub fn set_dest_dir(args: &ArgMatches, book: &mut MDBook) {
+    if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
+        let build_dir = std::env::current_dir()
+            .expect("current dir should be valid")
+            .join(dest_dir);
+        book.config.build.build_dir = build_dir;
+    }
+}
diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs
index 5fc20f9ac0..6d8a06167d 100644
--- a/src/cmd/serve.rs
+++ b/src/cmd/serve.rs
@@ -62,9 +62,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
         book.config
             .set("output.html.live-reload-endpoint", LIVE_RELOAD_ENDPOINT)
             .expect("live-reload-endpoint update failed");
-        if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
-            book.config.build.build_dir = dest_dir.into();
-        }
+        set_dest_dir(args, book);
         // Override site-url for local serving of the 404 file
         book.config.set("output.html.site-url", "/").unwrap();
     };
diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs
index 4f63750567..bd9c19b233 100644
--- a/src/cmd/watch.rs
+++ b/src/cmd/watch.rs
@@ -38,9 +38,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
     let mut book = MDBook::load(&book_dir)?;
 
     let update_config = |book: &mut MDBook| {
-        if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
-            book.config.build.build_dir = dest_dir.into();
-        }
+        set_dest_dir(args, book);
     };
     update_config(&mut book);
 


Test patch:
diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs
index 6bbf061d22..7b6cfc4d3d 100644
--- a/tests/testsuite/build.rs
+++ b/tests/testsuite/build.rs
@@ -66,3 +66,22 @@ fn book_toml_isnt_required() {
         str![[r##"<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>"##]],
     );
 }
+
+// Dest dir relative path behavior.
+#[test]
+fn dest_dir_relative_path() {
+    let mut test = BookTest::from_dir("build/basic_build");
+    let current_dir = test.dir.join("work");
+    std::fs::create_dir_all(&current_dir).unwrap();
+    test.run("build", |cmd| {
+        cmd.args(&["--dest-dir", "foo", ".."])
+            .current_dir(&current_dir)
+            .expect_stderr(str![[r#"
+[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
+[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend
+[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/work/foo`
+
+"#]]);
+    });
+    assert!(current_dir.join("foo/index.html").exists());
+}