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 (
43 "thirdparty/freetype2/objs/.libs/libfreetype.so",
44 "libfreetype.so",
45 ),
46 (
47 "thirdparty/harfbuzz/build/src/libharfbuzz.so",
48 "libharfbuzz.so",
49 ),
50 ("thirdparty/gumbo/.libs/libgumbo.so", "libgumbo.so"),
51 (
52 "thirdparty/djvulibre/libdjvu/.libs/libdjvulibre.so",
53 "libdjvulibre.so",
54 ),
55 ("thirdparty/mupdf/build/release/libmupdf.so", "libmupdf.so"),
56];
57
58const CROSS_ENV: &[(&str, &str)] = &[
59 ("PKG_CONFIG_ALLOW_CROSS", "1"),
60 (
61 "CARGO_TARGET_ARM_UNKNOWN_LINUX_GNUEABIHF_LINKER",
62 "arm-linux-gnueabihf-gcc",
63 ),
64 ("CC_arm_unknown_linux_gnueabihf", "arm-linux-gnueabihf-gcc"),
65 ("AR_arm_unknown_linux_gnueabihf", "arm-linux-gnueabihf-ar"),
66];
67
68#[derive(Debug, Args)]
70pub struct BuildKoboArgs {
71 #[arg(long)]
76 pub slow: bool,
77
78 #[arg(long)]
82 pub skip: bool,
83
84 #[arg(long)]
88 pub download_only: bool,
89
90 #[arg(long)]
92 pub features: Option<String>,
93}
94
95pub fn run(args: BuildKoboArgs) -> Result<()> {
104 if !cfg!(any(target_os = "linux", target_os = "macos")) {
105 bail!(
106 "Kobo cross-compilation is only available on Linux and macOS.\n\
107 On other platforms, please 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}