1use anyhow::{Context, Result, bail};
24use clap::Args;
25
26use super::util::{cmd, fs, github, http, mupdf_wrapper, thirdparty, workspace};
27
28const 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#[derive(Debug, Args)]
69pub struct BuildKoboArgs {
70 #[arg(long)]
75 pub slow: bool,
76
77 #[arg(long)]
81 pub skip: bool,
82
83 #[arg(long)]
87 pub download_only: bool,
88
89 #[arg(long)]
91 pub features: Option<String>,
92}
93
94pub 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
145fn 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
161fn 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
171fn 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
185fn 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
199fn 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
212fn 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
246fn build_mupdf_wrapper_kobo(root: &std::path::Path) -> Result<()> {
248 println!("Building mupdf_wrapper for Kobo…");
249 mupdf_wrapper::build_kobo(root)
250}
251
252fn 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}