xtask_lib/tasks/
build_kobo.rs

1//! `cargo xtask build-kobo` — cross-compile Cadmus for Kobo devices.
2//!
3//! 1. Optionally downloads and builds all thirdparty libraries from source
4//!    (`--slow` mode, required for CI).
5//! 2. Builds the `mupdf_wrapper` C library for the Kobo ARM target.
6//! 3. Runs `cargo build --release --target arm-unknown-linux-gnueabihf`.
7//!
8//! ## Platform requirement
9//!
10//! Cross-compilation requires the Linaro ARM toolchain
11//! (`arm-linux-gnueabihf-gcc` and friends) which is only available on Linux.
12//! The task exits with a clear error on macOS.
13//!
14//! ## Build modes
15//!
16//! | Mode | Description |
17//! |------|-------------|
18//! | fast (default) | Downloads pre-built `.so` files and MuPDF sources |
19//! | slow | Builds all thirdparty libraries from source |
20//! | slow + download-only | Downloads all thirdparty sources without building |
21//! | skip | Assumes `libs/` already exists; skips download entirely |
22
23use anyhow::{Context, Result, bail};
24use clap::Args;
25
26use super::util::{cmd, fs, github, http, mupdf_wrapper, thirdparty, workspace};
27
28/// BUILT_LIBRARY_COPIES maps built `.so` paths to their destination names in
29/// `libs/`.  The destination names here are the *base* names (e.g. `libz.so`);
30/// the actual SONAME is discovered at runtime via [`thirdparty::soname`].
31const BUILT_LIBRARY_COPIES: &[(&str, &str)] = &[
32    ("thirdparty/zlib/libz.so", "libz.so"),
33    ("thirdparty/bzip2/libbz2.so", "libbz2.so"),
34    ("thirdparty/libpng/.libs/libpng16.so", "libpng16.so"),
35    ("thirdparty/libjpeg/.libs/libjpeg.so", "libjpeg.so"),
36    (
37        "thirdparty/openjpeg/build/bin/libopenjp2.so",
38        "libopenjp2.so",
39    ),
40    ("thirdparty/jbig2dec/.libs/libjbig2dec.so", "libjbig2dec.so"),
41    (
42        "thirdparty/freetype2/objs/.libs/libfreetype.so",
43        "libfreetype.so",
44    ),
45    (
46        "thirdparty/harfbuzz/build/src/libharfbuzz.so",
47        "libharfbuzz.so",
48    ),
49    ("thirdparty/gumbo/.libs/libgumbo.so", "libgumbo.so"),
50    (
51        "thirdparty/djvulibre/libdjvu/.libs/libdjvulibre.so",
52        "libdjvulibre.so",
53    ),
54    ("thirdparty/mupdf/build/release/libmupdf.so", "libmupdf.so"),
55];
56
57const CROSS_ENV: &[(&str, &str)] = &[
58    ("PKG_CONFIG_ALLOW_CROSS", "1"),
59    (
60        "CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER",
61        "arm-linux-gnueabihf-gcc",
62    ),
63    ("CC_arm_unknown_linux_gnueabihf", "arm-linux-gnueabihf-gcc"),
64    ("AR_arm_unknown_linux_gnueabihf", "arm-linux-gnueabihf-ar"),
65];
66
67/// Arguments for `cargo xtask build-kobo`.
68#[derive(Debug, Args)]
69pub struct BuildKoboArgs {
70    /// Build all thirdparty libraries from source instead of downloading
71    /// pre-built binaries.
72    ///
73    /// Required for CI where pre-built binaries are not available.
74    #[arg(long)]
75    pub slow: bool,
76
77    /// Skip the library download/build step entirely.
78    ///
79    /// Use this when `libs/` already contains the required `.so` files.
80    #[arg(long)]
81    pub skip: bool,
82
83    /// Download thirdparty sources without building or cross-compiling.
84    ///
85    /// Useful for pre-populating the source cache in CI setup steps.
86    #[arg(long)]
87    pub download_only: bool,
88
89    /// Cargo feature flags to pass to the Cadmus build (e.g. `test`).
90    #[arg(long)]
91    pub features: Option<String>,
92}
93
94/// Cross-compiles Cadmus for Kobo ARM devices.
95///
96/// # Errors
97///
98/// Returns an error if:
99/// - The platform is not Linux.
100/// - The Linaro toolchain is not on `PATH`.
101/// - Any build step fails.
102pub fn run(args: BuildKoboArgs) -> Result<()> {
103    if !cfg!(target_os = "linux") {
104        bail!(
105            "Kobo cross-compilation is only available on Linux.\n\
106             The Linaro ARM toolchain consists of x86_64 Linux ELF binaries \
107             that cannot run on macOS. Use Docker or a Linux VM instead."
108        );
109    }
110
111    let root = workspace::root()?;
112
113    ensure_linaro_toolchain()?;
114
115    match (args.slow, args.skip, args.download_only) {
116        (_, true, _) => {
117            println!("Skipping library download (--skip).");
118        }
119        (true, false, true) => {
120            println!("Downloading thirdparty sources (--slow --download-only)…");
121            thirdparty::download_libraries(&root.join("thirdparty"), &[])?;
122            return Ok(());
123        }
124        (true, false, false) => {
125            println!("Building thirdparty libraries from source (--slow)…");
126            build_thirdparty_slow(&root)?;
127        }
128        (false, false, true) => {
129            println!("Downloading MuPDF sources (--download-only)…");
130            thirdparty::download_libraries(&root.join("thirdparty"), &["mupdf"])?;
131            return Ok(());
132        }
133        (false, false, false) => {
134            println!("Downloading pre-built libraries (fast mode)…");
135            build_thirdparty_fast(&root)?;
136        }
137    }
138
139    build_mupdf_wrapper_kobo(&root)?;
140    cargo_build_kobo(&root, args.features.as_deref())?;
141
142    Ok(())
143}
144
145/// Verifies that the Linaro ARM cross-compiler is available on `PATH`.
146fn ensure_linaro_toolchain() -> Result<()> {
147    cmd::run(
148        "arm-linux-gnueabihf-gcc",
149        &["--version"],
150        std::path::Path::new("."),
151        &[],
152    )
153    .map_err(|_| {
154        anyhow::anyhow!(
155            "arm-linux-gnueabihf-gcc not found on PATH.\n\
156             Install the Linaro toolchain or run inside the devenv shell."
157        )
158    })
159}
160
161/// Downloads pre-built `.so` files and MuPDF sources (fast mode).
162fn build_thirdparty_fast(root: &std::path::Path) -> Result<()> {
163    download_release_libs(root)?;
164
165    let libs_dir = root.join("libs");
166    create_symlinks(&libs_dir)?;
167
168    thirdparty::download_libraries(&root.join("thirdparty"), &["mupdf"])
169}
170
171/// Builds all thirdparty libraries from source (slow mode).
172fn build_thirdparty_slow(root: &std::path::Path) -> Result<()> {
173    let thirdparty_dir = root.join("thirdparty");
174
175    thirdparty::download_libraries(&thirdparty_dir, &[])?;
176    thirdparty::build_libraries(&thirdparty_dir, &[])?;
177
178    let libs_dir = root.join("libs");
179    std::fs::create_dir_all(&libs_dir)?;
180
181    copy_built_libs(root, &libs_dir)?;
182    create_symlinks(&libs_dir)
183}
184
185/// Creates the `.so` version symlinks expected by the Cadmus runtime.
186fn create_symlinks(libs_dir: &std::path::Path) -> Result<()> {
187    for &lib in thirdparty::SONAMES {
188        let target = thirdparty::soname(libs_dir, lib)?;
189        let link_path = libs_dir.join(lib);
190        if !link_path.exists() {
191            #[cfg(unix)]
192            std::os::unix::fs::symlink(&target, &link_path)?;
193        }
194    }
195
196    Ok(())
197}
198
199/// Copies the `.so` files produced by the slow build into `libs/`.
200fn copy_built_libs(root: &std::path::Path, libs_dir: &std::path::Path) -> Result<()> {
201    for (src_rel, dest_name) in BUILT_LIBRARY_COPIES {
202        let src = root.join(src_rel);
203        let dest = libs_dir.join(dest_name);
204        std::fs::copy(&src, &dest).map_err(|e| {
205            anyhow::anyhow!("failed to copy {} → {}: {e}", src.display(), dest.display())
206        })?;
207    }
208
209    Ok(())
210}
211
212/// Downloads pre-built `.so` release assets from the cadmus GitHub release
213/// with checksum verification.
214fn download_release_libs(root: &std::path::Path) -> Result<()> {
215    let version = workspace::current_version()?;
216    let tag = format!("v{version}");
217    let archive_name = "cadmus-kobo.tar.gz";
218
219    let libs_dir = root.join("libs");
220    if libs_dir.exists() {
221        println!("libs/ directory already exists; skipping download of pre-built libraries.");
222        return Ok(());
223    }
224
225    std::fs::create_dir_all(&libs_dir)?;
226
227    let asset = github::fetch_release_asset("ogkevin/cadmus", &tag, archive_name)?;
228    let archive = root.join(archive_name);
229
230    match asset.sha256() {
231        Some(expected) => {
232            http::download_verified(&asset.browser_download_url, &archive, expected)?;
233        }
234        None => {
235            http::download(&asset.browser_download_url, &archive)
236                .with_context(|| format!("failed to download {archive_name}"))?;
237        }
238    }
239
240    fs::extract_tarball_paths(&archive, root, &["libs"])?;
241    std::fs::remove_file(&archive).ok();
242
243    Ok(())
244}
245
246/// Builds the `mupdf_wrapper` C library for the Kobo ARM target.
247fn build_mupdf_wrapper_kobo(root: &std::path::Path) -> Result<()> {
248    println!("Building mupdf_wrapper for Kobo…");
249    mupdf_wrapper::build_kobo(root)
250}
251
252/// Runs `cargo build --release` for the ARM Kobo target.
253fn cargo_build_kobo(root: &std::path::Path, features: Option<&str>) -> Result<()> {
254    let mut cargo_args = vec![
255        "build",
256        "--release",
257        "--target",
258        "arm-unknown-linux-gnueabihf",
259        "-p",
260        "cadmus",
261    ];
262
263    if let Some(f) = features {
264        cargo_args.push("--features");
265        cargo_args.push(f);
266    }
267
268    cmd::run("cargo", &cargo_args, root, CROSS_ENV)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn symlink_list_has_no_duplicates() {
277        let mut link_names: Vec<&str> = thirdparty::SONAMES.to_vec();
278        link_names.sort_unstable();
279        let original_len = link_names.len();
280        link_names.dedup();
281        assert_eq!(link_names.len(), original_len, "duplicate link names found");
282    }
283}