1use std::path::Path;
15
16use anyhow::{Context, Result};
17use clap::Args;
18
19use super::util::thirdparty::MUPDF_VERSION;
20use super::util::{cmd, mupdf_wrapper, thirdparty, workspace};
21
22const NATIVE_BUILT_MARKER: &str = ".built-native";
24
25#[derive(Debug, Args)]
27pub struct SetupNativeArgs {
28 #[arg(long)]
31 pub force: bool,
32}
33
34pub fn run(args: SetupNativeArgs) -> Result<()> {
41 let root = workspace::root()?;
42
43 ensure_native_artifacts(&root, args.force)?;
44
45 println!("\nNative setup complete!");
46 println!("You can now run:");
47 println!(" cargo test - Run tests");
48 println!(" cargo xtask build-kobo - Build for Kobo (Linux & macOS)");
49
50 Ok(())
51}
52
53pub fn ensure_native_artifacts(root: &Path, force: bool) -> Result<()> {
54 thirdparty::download_libraries(&root.join("thirdparty"), &["libwebp"])?;
55 build_libwebp_native(root)?;
56
57 let mupdf_patched = ensure_mupdf_sources_with_webp_patches(root, force)?;
58 if mupdf_patched {
59 remove_native_wrapper_artifact(root)?;
60 }
61
62 let rebuild_mupdf = mupdf_patched || !native_mupdf_ready(root);
63 if rebuild_mupdf {
64 build_mupdf_native(root)?;
65 write_native_build_marker(root)?;
66 } else {
67 println!("Native MuPDF build already present.");
68 }
69
70 build_mupdf_wrapper_native_if_needed(root)?;
71
72 link_mupdf_artifacts(root)?;
73
74 Ok(())
75}
76
77pub fn build_libwebp_native(root: &Path) -> Result<()> {
88 let libwebp_dir = root.join("thirdparty/libwebp");
89 if libwebp_dir.join("src/.libs/libwebp.a").exists() {
90 println!("libwebp already built for native.");
91 return Ok(());
92 }
93
94 println!("Building libwebp for native development…");
95
96 if !libwebp_dir.join("configure").exists() {
97 cmd::run("sh", &["autogen.sh"], &libwebp_dir, &[("NOCONFIGURE", "1")])
98 .context("failed to run autogen.sh for libwebp")?;
99 }
100
101 cmd::run(
102 "./configure",
103 &[
104 "--disable-shared",
105 "--enable-static",
106 "--disable-libwebpmux",
107 "--enable-libwebpdecoder",
108 "--enable-libwebpdemux",
109 "--disable-webp-tools",
110 "--with-pic",
111 ],
112 &libwebp_dir,
113 &[],
114 )
115 .context("failed to configure libwebp")?;
116
117 cmd::run("make", &["-j4"], &libwebp_dir, &[]).context("failed to build libwebp")?;
118
119 combine_libwebp_static_archives(&libwebp_dir)?;
120
121 println!("✓ libwebp built successfully");
122 Ok(())
123}
124
125fn combine_libwebp_static_archives(libwebp_dir: &Path) -> Result<()> {
130 let libs_dir = libwebp_dir.join("src/.libs");
131 std::fs::create_dir_all(&libs_dir).context("failed to create src/.libs for libwebp")?;
132
133 let sublibs = [
134 "dec/.libs/libwebpdecode.a",
135 "dsp/.libs/libwebpdsp.a",
136 "enc/.libs/libwebpencode.a",
137 "utils/.libs/libwebputils.a",
138 ];
139
140 let mut objects = vec![];
141 for sublib in &sublibs {
142 let path = libwebp_dir.join("src").join(sublib);
143 let extract_dir = libs_dir.join(format!("extract_{}", sublib.replace('/', "_")));
144 std::fs::create_dir_all(&extract_dir)?;
145 cmd::run("ar", &["x", path.to_str().unwrap()], &extract_dir, &[])
146 .with_context(|| format!("failed to extract objects from {}", path.display()))?;
147 for entry in std::fs::read_dir(&extract_dir)? {
148 objects.push(entry?.path());
149 }
150 }
151
152 let libwebp_a = libs_dir.join("libwebp.a");
153 let mut ar_args: Vec<&str> = vec!["rcs", libwebp_a.to_str().unwrap()];
154 for obj in &objects {
155 ar_args.push(obj.to_str().unwrap());
156 }
157 cmd::run("ar", &ar_args, &libwebp_dir, &[]).context("failed to create combined libwebp.a")?;
158
159 Ok(())
160}
161
162pub fn ensure_mupdf_sources(root: &Path, force: bool) -> Result<()> {
168 ensure_mupdf_sources_with_webp_patches(root, force).map(|_| ())
169}
170
171fn ensure_mupdf_sources_with_webp_patches(root: &Path, force: bool) -> Result<bool> {
172 let mupdf_dir = root.join("thirdparty/mupdf");
173 let version_header = mupdf_dir.join("include/mupdf/fitz/version.h");
174 let current_version = read_mupdf_version(&version_header);
175
176 if force || current_version.as_deref() != Some(MUPDF_VERSION) {
177 if let Some(v) = ¤t_version {
178 println!("MuPDF version mismatch: have '{v}', need '{MUPDF_VERSION}'");
179 }
180
181 println!("Downloading MuPDF {MUPDF_VERSION} sources…");
182
183 if mupdf_dir.exists() {
184 thirdparty::clean_untracked(&mupdf_dir)
185 .context("failed to clean untracked files from thirdparty/mupdf")?;
186 }
187
188 thirdparty::download_libraries(&root.join("thirdparty"), &["mupdf"])?;
189 } else {
190 println!("MuPDF {MUPDF_VERSION} already present.");
191 }
192
193 apply_mupdf_webp_patches_if_needed(&mupdf_dir)
194}
195
196fn apply_mupdf_webp_patches_if_needed(mupdf_dir: &Path) -> Result<bool> {
197 let patched = thirdparty::apply_mupdf_webp_patches_if_needed(mupdf_dir)?;
198 if patched {
199 remove_native_build_marker(mupdf_dir);
200 }
201
202 Ok(patched)
203}
204
205fn remove_native_build_marker(mupdf_dir: &Path) {
206 let marker = mupdf_dir.join(NATIVE_BUILT_MARKER);
207 if marker.exists() {
208 std::fs::remove_file(&marker).ok();
209 }
210}
211
212fn read_mupdf_version(header: &Path) -> Option<String> {
216 let content = std::fs::read_to_string(header).ok()?;
217
218 for line in content.lines() {
220 if line.contains("FZ_VERSION") && line.contains('"') {
221 let start = line.find('"')? + 1;
222 let end = line.rfind('"')?;
223 if start < end {
224 return Some(line[start..end].to_owned());
225 }
226 }
227 }
228
229 None
230}
231
232pub fn native_setup_done(root: &Path) -> bool {
238 let platform_dir = if cfg!(target_os = "macos") {
239 "Darwin"
240 } else {
241 "Linux"
242 };
243
244 let wrapper_dir = root.join(format!("target/mupdf_wrapper/{platform_dir}"));
245
246 native_mupdf_ready(root)
247 && wrapper_dir.join("libmupdf.a").exists()
248 && wrapper_dir.join("libmupdf_wrapper.a").exists()
249}
250
251fn native_mupdf_ready(root: &Path) -> bool {
253 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
254 if !marker.exists() {
255 return false;
256 }
257
258 let libmupdf = root.join("thirdparty/mupdf/build/release/libmupdf.a");
259
260 libmupdf.exists()
261}
262
263fn write_native_build_marker(root: &Path) -> Result<()> {
265 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
266 std::fs::write(&marker, "").with_context(|| {
267 format!(
268 "failed to write native build marker at {}",
269 marker.display()
270 )
271 })
272}
273
274fn build_mupdf_wrapper_native_if_needed(root: &Path) -> Result<()> {
276 println!("Ensuring mupdf_wrapper is available…");
277 mupdf_wrapper::build_native_if_needed(root)
278}
279
280fn remove_native_wrapper_artifact(root: &Path) -> Result<()> {
281 let platform_dir = if cfg!(target_os = "macos") {
282 "Darwin"
283 } else {
284 "Linux"
285 };
286 let lib = root.join(format!(
287 "target/mupdf_wrapper/{platform_dir}/libmupdf_wrapper.a"
288 ));
289
290 if lib.exists() {
291 std::fs::remove_file(&lib)
292 .with_context(|| format!("failed to remove stale {}", lib.display()))?;
293 }
294
295 Ok(())
296}
297
298fn build_mupdf_native(root: &Path) -> Result<()> {
300 println!("Building MuPDF for native development…");
301
302 let mupdf_dir = root.join("thirdparty/mupdf");
303
304 for entry in ["gitattributes", ".gitattributes"] {
306 let path = mupdf_dir.join(entry);
307 if path.exists() {
308 std::fs::remove_file(&path).ok();
309 }
310 }
311
312 cmd::run("make", &["clean"], &mupdf_dir, &[]).ok();
313 cmd::run("make", &["verbose=yes", "generate"], &mupdf_dir, &[])?;
314
315 let sys_cflags = collect_system_cflags()?;
316 let xcflags = format!(
317 "-DFZ_ENABLE_ICC=0 -DFZ_ENABLE_SPOT_RENDERING=0 \
318 -DFZ_ENABLE_ODT_OUTPUT=0 -DFZ_ENABLE_OCR_OUTPUT=0 \
319 -DHAVE_WEBP=1 -I{root}/thirdparty/libwebp/src {sys_cflags}",
320 root = root.display()
321 );
322
323 let xlibs = format!(
325 "-L{root}/thirdparty/libwebp/src/.libs -lwebp \
326 -L{root}/thirdparty/libwebp/src/demux/.libs -lwebpdemux",
327 root = root.display()
328 );
329
330 cmd::run(
331 "make",
332 &[
333 "verbose=yes",
334 "mujs=no",
335 "tesseract=no",
336 "extract=no",
337 "archive=no",
338 "brotli=no",
339 "barcode=no",
340 "commercial=no",
341 "USE_SYSTEM_LIBS=yes",
342 &format!("XCFLAGS={xcflags}"),
343 &format!("XLIBS={xlibs}"),
344 "libs",
345 ],
346 &mupdf_dir,
347 &[],
348 )
349}
350
351fn collect_system_cflags() -> Result<String> {
356 if !cfg!(target_os = "macos") {
357 return Ok(String::new());
358 }
359
360 let libs = [
361 "freetype2",
362 "harfbuzz",
363 "libopenjp2",
364 "libjpeg",
365 "libwebp",
366 "zlib",
367 "jbig2dec",
368 "gumbo",
369 ];
370
371 let mut flags = String::new();
372 for lib in libs {
373 if let Ok(f) = cmd::output("pkg-config", &["--cflags", lib], Path::new("."), &[]) {
374 if !f.is_empty() {
375 flags.push(' ');
376 flags.push_str(&f);
377 }
378 }
379 }
380
381 Ok(flags.trim().to_owned())
382}
383
384fn link_mupdf_artifacts(root: &Path) -> Result<()> {
387 let platform_dir = if cfg!(target_os = "macos") {
388 "Darwin"
389 } else {
390 "Linux"
391 };
392
393 let target_dir = root.join(format!("target/mupdf_wrapper/{platform_dir}"));
394 std::fs::create_dir_all(&target_dir)
395 .context("failed to create target/mupdf_wrapper directory")?;
396
397 let release_dir = root.join("thirdparty/mupdf/build/release");
398
399 let libmupdf = release_dir.join("libmupdf.a");
400 if !libmupdf.exists() {
401 anyhow::bail!("libmupdf.a not found after build -- check MuPDF build output");
402 }
403
404 symlink_force(&libmupdf, &target_dir.join("libmupdf.a"))?;
405 println!("✓ Created libmupdf.a in target/mupdf_wrapper/{platform_dir}");
406
407 let libmupdf_third = release_dir.join("libmupdf-third.a");
408 if !libmupdf_third.exists() {
409 println!("Creating empty libmupdf-third.a (system libs used instead)…");
410 cmd::run("ar", &["cr", "libmupdf-third.a"], &release_dir, &[])?;
411 }
412
413 symlink_force(&libmupdf_third, &target_dir.join("libmupdf-third.a"))?;
414 println!("✓ Created libmupdf-third.a");
415
416 Ok(())
417}
418
419fn symlink_force(target: &Path, link: &Path) -> Result<()> {
422 if link.exists() || link.symlink_metadata().is_ok() {
423 std::fs::remove_file(link)
424 .with_context(|| format!("failed to remove existing {}", link.display()))?;
425 }
426
427 #[cfg(unix)]
428 std::os::unix::fs::symlink(target, link)
429 .with_context(|| format!("failed to create symlink {}", link.display()))?;
430
431 #[cfg(not(unix))]
432 std::fs::copy(target, link)
433 .with_context(|| format!("failed to copy {} to {}", target.display(), link.display()))?;
434
435 Ok(())
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441 use std::fs;
442
443 #[test]
444 fn read_mupdf_version_parses_define() {
445 let tmp = tempfile::tempdir().unwrap();
446 let header = tmp.path().join("version.h");
447 fs::write(
448 &header,
449 r#"/* MuPDF version */
450#define FZ_VERSION "1.27.0"
451#define FZ_VERSION_MAJOR 1
452"#,
453 )
454 .unwrap();
455
456 let version = read_mupdf_version(&header);
457 assert_eq!(version.as_deref(), Some("1.27.0"));
458 }
459
460 #[test]
461 fn read_mupdf_version_returns_none_for_malformed_header() {
462 let tmp = tempfile::tempdir().unwrap();
463 let header = tmp.path().join("version.h");
464 fs::write(&header, "/* no version define here */\n").unwrap();
465
466 let version = read_mupdf_version(&header);
467 assert!(version.is_none());
468 }
469}