xtask_lib/tasks/
bundle.rs

1//! `cargo xtask bundle` — package a `KoboRoot.tgz` for device installation.
2//!
3//! Creates one or more `.tgz` archives that can be placed in the `.kobo/`
4//! directory on a Kobo device to install or update Cadmus.
5//!
6//! ## Output files
7//!
8//! | Flag | Output | Contents |
9//! |------|--------|----------|
10//! | *(default)* | `bundle/KoboRoot-nm.tgz` | Cadmus + NickelMenu |
11//! | `--no-nickel` | `bundle/KoboRoot.tgz` | Cadmus only |
12//! | `--test` | `bundle/KoboRoot-nm-test.tgz` | Test build + NickelMenu |
13//! | `--test --no-nickel` | `bundle/KoboRoot-test.tgz` | Test build only |
14//!
15//! ## NickelMenu
16//!
17//! NickelMenu is downloaded from GitHub Releases and cached in
18//! `.cache/nickelmenu/`.
19//! The version is controlled by [`NICKEL_VERSION`].  Pass `--skip-download`
20//! to use a previously cached archive.
21
22use std::path::Path;
23
24use anyhow::{Context, Result, bail};
25use clap::Args;
26
27use super::util::{fs, github, http, workspace};
28/// The NickelMenu release version to bundle.
29pub const NICKEL_VERSION: &str = "0.6.0";
30
31/// Arguments for `cargo xtask bundle`.
32#[derive(Debug, Args)]
33pub struct BundleArgs {
34    /// Create a bundle without NickelMenu.
35    #[arg(long)]
36    pub no_nickel: bool,
37
38    /// Bundle the test build (installs to `.adds/cadmus-tst`).
39    #[arg(long)]
40    pub test: bool,
41
42    /// Use a cached NickelMenu archive instead of downloading.
43    #[arg(long)]
44    pub skip_download: bool,
45}
46
47/// Packages the distribution directory into a `KoboRoot.tgz`.
48///
49/// # Errors
50///
51/// Returns an error if `dist/` does not exist, the NickelMenu download fails,
52/// or archive creation fails.
53pub fn run(args: BundleArgs) -> Result<()> {
54    let root = workspace::root()?;
55
56    let dist_dir = root.join("dist");
57    if !dist_dir.exists() {
58        bail!("dist/ not found. Run `cargo xtask dist` first.");
59    }
60
61    let bundle_dir = root.join("bundle");
62    if bundle_dir.exists() {
63        std::fs::remove_dir_all(&bundle_dir).context("failed to remove existing bundle/")?;
64    }
65
66    if args.no_nickel {
67        create_bundle_cadmus_only(&root, args.test)?;
68    } else {
69        let archive = ensure_nickel_menu(&root, args.skip_download)?;
70        create_bundle_with_nickel(&root, &archive, args.test)?;
71    }
72
73    Ok(())
74}
75
76/// Returns the path to the NickelMenu archive, downloading it if necessary.
77fn ensure_nickel_menu(root: &Path, skip_download: bool) -> Result<std::path::PathBuf> {
78    let cache_dir = root.join(".cache/nickelmenu");
79    let archive = cache_dir.join(format!("NickelMenu-{NICKEL_VERSION}-KoboRoot.tgz"));
80
81    if archive.exists() {
82        println!("Using cached NickelMenu v{NICKEL_VERSION}");
83        return Ok(archive);
84    }
85
86    if skip_download {
87        bail!(
88            "NickelMenu archive not found at {}.\n\
89             Remove --skip-download to auto-download.",
90            archive.display()
91        );
92    }
93
94    download_nickel_menu(&cache_dir, &archive)?;
95    Ok(archive)
96}
97
98/// Downloads the NickelMenu release archive from GitHub with checksum verification.
99fn download_nickel_menu(cache_dir: &Path, archive: &Path) -> Result<()> {
100    std::fs::create_dir_all(cache_dir)?;
101
102    println!("Downloading NickelMenu v{NICKEL_VERSION}…");
103
104    let asset = github::fetch_release_asset(
105        "pgaskin/NickelMenu",
106        &format!("v{NICKEL_VERSION}"),
107        "KoboRoot.tgz",
108    )?;
109
110    match asset.sha256() {
111        Some(expected) => {
112            http::download_verified(&asset.browser_download_url, archive, expected)?;
113        }
114        None => {
115            http::download(&asset.browser_download_url, archive)
116                .context("failed to download NickelMenu archive")?;
117        }
118    }
119
120    println!("Downloaded NickelMenu to {}", archive.display());
121    Ok(())
122}
123
124/// Creates a bundle containing only Cadmus (no NickelMenu).
125fn create_bundle_cadmus_only(root: &Path, test: bool) -> Result<()> {
126    let bundle_dir = root.join("bundle");
127    let (adds_subdir, archive_name) = if test {
128        ("cadmus-tst", "KoboRoot-test.tgz")
129    } else {
130        ("cadmus", "KoboRoot.tgz")
131    };
132
133    let install_dir = bundle_dir.join("mnt/onboard/.adds").join(adds_subdir);
134    std::fs::create_dir_all(&install_dir)?;
135
136    fs::copy_dir_all(&root.join("dist"), &install_dir)?;
137
138    let archive = bundle_dir.join(archive_name);
139    fs::create_tarball(&archive, &bundle_dir, &["mnt"])?;
140
141    std::fs::remove_dir_all(bundle_dir.join("mnt"))?;
142
143    println!("Bundle created: bundle/{archive_name}");
144    println!("Place this file in the .kobo directory on your Kobo device");
145    Ok(())
146}
147
148/// Creates a bundle that merges Cadmus with NickelMenu.
149fn create_bundle_with_nickel(root: &Path, nickel_archive: &Path, test: bool) -> Result<()> {
150    let bundle_dir = root.join("bundle");
151    std::fs::create_dir_all(&bundle_dir)?;
152
153    fs::extract_tarball(nickel_archive, &bundle_dir)?;
154
155    let adds_src = bundle_dir.join("mnt/onboard/.adds");
156    let adds_dst = bundle_dir.join(".adds");
157    std::fs::rename(&adds_src, &adds_dst)?;
158    std::fs::remove_dir_all(bundle_dir.join("mnt"))?;
159
160    let (adds_subdir, nm_config, archive_name) = if test {
161        ("cadmus-tst", "cadmus-tst", "KoboRoot-nm-test.tgz")
162    } else {
163        ("cadmus", "cadmus", "KoboRoot-nm.tgz")
164    };
165
166    fs::copy_dir_all(&root.join("dist"), &adds_dst.join(adds_subdir))?;
167
168    std::fs::copy(
169        root.join(format!("contrib/NickelMenu/{nm_config}")),
170        adds_dst.join(format!("nm/{nm_config}")),
171    )?;
172
173    let final_adds = bundle_dir.join("mnt/onboard/.adds");
174    std::fs::create_dir_all(bundle_dir.join("mnt/onboard"))?;
175    std::fs::rename(&adds_dst, &final_adds)?;
176
177    let archive = bundle_dir.join(archive_name);
178    fs::create_tarball(&archive, &bundle_dir, &["usr", "mnt"])?;
179
180    std::fs::remove_dir_all(bundle_dir.join("usr")).ok();
181    std::fs::remove_dir_all(bundle_dir.join("mnt"))?;
182
183    println!("Bundle created: bundle/{archive_name}");
184    println!("Place this file in the .kobo directory on your Kobo device");
185    Ok(())
186}