Skip to main content

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