1use anyhow::{Context, Result, bail};
25use clap::Args;
26
27use super::util::{cmd, fs, github, http, mupdf_wrapper, thirdparty, workspace};
28
29const 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#[derive(Debug, Args)]
75pub struct BuildKoboArgs {
76 #[arg(long)]
81 pub slow: bool,
82
83 #[arg(long)]
87 pub skip: bool,
88
89 #[arg(long)]
93 pub download_only: bool,
94
95 #[arg(long)]
97 pub features: Option<String>,
98}
99
100pub 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
150fn 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
166fn 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
176fn 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
190fn 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
204fn 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
217fn 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
251fn build_mupdf_wrapper_kobo(root: &std::path::Path) -> Result<()> {
253 println!("Building mupdf_wrapper for Kobo…");
254 mupdf_wrapper::build_kobo(root)
255}
256
257fn 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}