1use std::{
2 fs,
3 path::{Path, PathBuf},
4 process::Command,
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{BookyardConfig, BookyardError, Result, build_catalog, error::io_error};
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct BuildPlan {
13 pub output_dir: PathBuf,
14 pub items: Vec<BuildItem>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct BuildItem {
19 pub id: String,
20 pub source_dir: PathBuf,
21 pub dest_dir: PathBuf,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct BuildOptions {
26 pub run_mdbook: bool,
27}
28
29impl Default for BuildOptions {
30 fn default() -> Self {
31 Self { run_mdbook: true }
32 }
33}
34
35pub fn create_build_plan(root: &Path, config: &BookyardConfig) -> Result<BuildPlan> {
36 config.validate()?;
37 let output_dir = root.join(&config.workspace.output);
38 let items = config
39 .books
40 .iter()
41 .map(|book| BuildItem {
42 id: book.id.clone(),
43 source_dir: root.join(&book.source),
44 dest_dir: output_dir.join("books").join(&book.id),
45 })
46 .collect();
47 Ok(BuildPlan { output_dir, items })
48}
49
50pub fn build_shelf(
51 root: &Path,
52 config: &BookyardConfig,
53 options: &BuildOptions,
54) -> Result<BuildPlan> {
55 let plan = create_build_plan(root, config)?;
56 fs::create_dir_all(&plan.output_dir).map_err(|source| io_error(&plan.output_dir, source))?;
57 fs::create_dir_all(plan.output_dir.join("books"))
58 .map_err(|source| io_error(plan.output_dir.join("books"), source))?;
59
60 for item in &plan.items {
61 if options.run_mdbook {
62 build_mdbook(item)?;
63 } else {
64 fs::create_dir_all(&item.dest_dir)
65 .map_err(|source| io_error(&item.dest_dir, source))?;
66 }
67 }
68
69 let catalog = build_catalog(config);
70 let catalog_json = serde_json::to_string_pretty(&catalog).expect("catalog is serializable");
71 let catalog_path = plan.output_dir.join("catalog.json");
72 fs::write(&catalog_path, catalog_json).map_err(|source| io_error(catalog_path, source))?;
73 Ok(plan)
74}
75
76fn build_mdbook(item: &BuildItem) -> Result<()> {
77 let status = Command::new("mdbook")
78 .arg("build")
79 .arg(&item.source_dir)
80 .arg("--dest-dir")
81 .arg(&item.dest_dir)
82 .status()
83 .map_err(|source| io_error("mdbook", source))?;
84 if status.success() {
85 Ok(())
86 } else {
87 Err(BookyardError::MdBookBuildFailed {
88 id: item.id.clone(),
89 status: status.to_string(),
90 })
91 }
92}