xtask_lib/tasks/
setup_native.rs1use std::path::Path;
15
16use anyhow::{Context, Result, bail};
17use clap::Args;
18
19use super::util::{cmd, mupdf_wrapper, thirdparty, workspace};
20
21pub const REQUIRED_MUPDF_VERSION: &str = "1.27.0";
23
24const NATIVE_BUILT_MARKER: &str = ".built-native";
26
27#[derive(Debug, Args)]
29pub struct SetupNativeArgs {
30 #[arg(long)]
33 pub force: bool,
34}
35
36pub fn run(args: SetupNativeArgs) -> Result<()> {
43 let root = workspace::root()?;
44
45 ensure_mupdf_sources(&root, args.force)?;
46 build_mupdf_wrapper_native_if_needed(&root)?;
47
48 if native_mupdf_ready(&root) {
49 println!("Native MuPDF build already present.");
50 } else {
51 build_mupdf_native(&root)?;
52 write_native_build_marker(&root)?;
53 }
54
55 link_mupdf_artifacts(&root)?;
56
57 println!("\nNative setup complete!");
58 println!("You can now run:");
59 println!(" cargo test - Run tests");
60 println!(" cargo xtask build-kobo - Build for Kobo (Linux & macOS)");
61
62 Ok(())
63}
64
65pub fn ensure_mupdf_sources(root: &Path, force: bool) -> Result<()> {
71 let version_header = root.join("thirdparty/mupdf/include/mupdf/fitz/version.h");
72
73 let current_version = read_mupdf_version(&version_header);
74
75 if !force && current_version.as_deref() == Some(REQUIRED_MUPDF_VERSION) {
76 println!("MuPDF {REQUIRED_MUPDF_VERSION} already present.");
77 return Ok(());
78 }
79
80 if let Some(v) = ¤t_version {
81 println!("MuPDF version mismatch: have '{v}', need '{REQUIRED_MUPDF_VERSION}'");
82 }
83
84 println!("Downloading MuPDF {REQUIRED_MUPDF_VERSION} sources…");
85
86 let mupdf_dir = root.join("thirdparty/mupdf");
87 if mupdf_dir.exists() {
88 thirdparty::clean_untracked(&mupdf_dir)
89 .context("failed to clean untracked files from thirdparty/mupdf")?;
90 }
91
92 thirdparty::download_libraries(&root.join("thirdparty"), &["mupdf"])
93}
94
95fn read_mupdf_version(header: &Path) -> Option<String> {
99 let content = std::fs::read_to_string(header).ok()?;
100
101 for line in content.lines() {
103 if line.contains("FZ_VERSION") && line.contains('"') {
104 let start = line.find('"')? + 1;
105 let end = line.rfind('"')?;
106 if start < end {
107 return Some(line[start..end].to_owned());
108 }
109 }
110 }
111
112 None
113}
114
115pub fn native_setup_done(root: &Path) -> bool {
121 let platform_dir = if cfg!(target_os = "macos") {
122 "Darwin"
123 } else {
124 "Linux"
125 };
126
127 let wrapper_dir = root.join(format!("target/mupdf_wrapper/{platform_dir}"));
128
129 native_mupdf_ready(root)
130 && wrapper_dir.join("libmupdf.a").exists()
131 && wrapper_dir.join("libmupdf_wrapper.a").exists()
132}
133
134fn native_mupdf_ready(root: &Path) -> bool {
136 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
137 if !marker.exists() {
138 return false;
139 }
140
141 let libmupdf = root.join("thirdparty/mupdf/build/release/libmupdf.a");
142
143 libmupdf.exists()
144}
145
146fn write_native_build_marker(root: &Path) -> Result<()> {
148 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
149 std::fs::write(&marker, "").with_context(|| {
150 format!(
151 "failed to write native build marker at {}",
152 marker.display()
153 )
154 })
155}
156
157fn build_mupdf_wrapper_native_if_needed(root: &Path) -> Result<()> {
159 println!("Ensuring mupdf_wrapper is available…");
160 mupdf_wrapper::build_native_if_needed(root)
161}
162
163fn build_mupdf_native(root: &Path) -> Result<()> {
165 println!("Building MuPDF for native development…");
166
167 let mupdf_dir = root.join("thirdparty/mupdf");
168
169 for entry in ["gitattributes", ".gitattributes"] {
171 let path = mupdf_dir.join(entry);
172 if path.exists() {
173 std::fs::remove_file(&path).ok();
174 }
175 }
176
177 cmd::run("make", &["clean"], &mupdf_dir, &[]).ok();
178 cmd::run("make", &["verbose=yes", "generate"], &mupdf_dir, &[])?;
179
180 let sys_cflags = collect_system_cflags()?;
181 let xcflags = format!(
182 "-DFZ_ENABLE_ICC=0 -DFZ_ENABLE_SPOT_RENDERING=0 \
183 -DFZ_ENABLE_ODT_OUTPUT=0 -DFZ_ENABLE_OCR_OUTPUT=0 {sys_cflags}"
184 );
185
186 cmd::run(
187 "make",
188 &[
189 "verbose=yes",
190 "mujs=no",
191 "tesseract=no",
192 "extract=no",
193 "archive=no",
194 "brotli=no",
195 "barcode=no",
196 "commercial=no",
197 "USE_SYSTEM_LIBS=yes",
198 &format!("XCFLAGS={xcflags}"),
199 "libs",
200 ],
201 &mupdf_dir,
202 &[],
203 )
204}
205
206fn collect_system_cflags() -> Result<String> {
211 if !cfg!(target_os = "macos") {
212 return Ok(String::new());
213 }
214
215 let libs = [
216 "freetype2",
217 "harfbuzz",
218 "libopenjp2",
219 "libjpeg",
220 "zlib",
221 "jbig2dec",
222 "gumbo",
223 ];
224
225 let mut flags = String::new();
226 for lib in libs {
227 if let Ok(f) = cmd::output("pkg-config", &["--cflags", lib], Path::new("."), &[]) {
228 if !f.is_empty() {
229 flags.push(' ');
230 flags.push_str(&f);
231 }
232 }
233 }
234
235 Ok(flags.trim().to_owned())
236}
237
238fn link_mupdf_artifacts(root: &Path) -> Result<()> {
241 let platform_dir = if cfg!(target_os = "macos") {
242 "Darwin"
243 } else {
244 "Linux"
245 };
246
247 let target_dir = root.join(format!("target/mupdf_wrapper/{platform_dir}"));
248 std::fs::create_dir_all(&target_dir)
249 .context("failed to create target/mupdf_wrapper directory")?;
250
251 let release_dir = root.join("thirdparty/mupdf/build/release");
252
253 let libmupdf = release_dir.join("libmupdf.a");
254 if !libmupdf.exists() {
255 bail!("libmupdf.a not found after build — check MuPDF build output");
256 }
257
258 symlink_force(&libmupdf, &target_dir.join("libmupdf.a"))?;
259 println!("✓ Created libmupdf.a in target/mupdf_wrapper/{platform_dir}");
260
261 let libmupdf_third = release_dir.join("libmupdf-third.a");
262 if !libmupdf_third.exists() {
263 println!("Creating empty libmupdf-third.a (system libs used instead)…");
264 cmd::run("ar", &["cr", "libmupdf-third.a"], &release_dir, &[])?;
265 }
266
267 symlink_force(&libmupdf_third, &target_dir.join("libmupdf-third.a"))?;
268 println!("✓ Created libmupdf-third.a");
269
270 Ok(())
271}
272
273fn symlink_force(target: &Path, link: &Path) -> Result<()> {
276 if link.exists() || link.symlink_metadata().is_ok() {
277 std::fs::remove_file(link)
278 .with_context(|| format!("failed to remove existing {}", link.display()))?;
279 }
280
281 #[cfg(unix)]
282 std::os::unix::fs::symlink(target, link)
283 .with_context(|| format!("failed to create symlink {}", link.display()))?;
284
285 #[cfg(not(unix))]
286 std::fs::copy(target, link)
287 .with_context(|| format!("failed to copy {} to {}", target.display(), link.display()))?;
288
289 Ok(())
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use std::fs;
296
297 #[test]
298 fn read_mupdf_version_parses_define() {
299 let tmp = tempfile::tempdir().unwrap();
300 let header = tmp.path().join("version.h");
301 fs::write(
302 &header,
303 r#"/* MuPDF version */
304#define FZ_VERSION "1.27.0"
305#define FZ_VERSION_MAJOR 1
306"#,
307 )
308 .unwrap();
309
310 let version = read_mupdf_version(&header);
311 assert_eq!(version.as_deref(), Some("1.27.0"));
312 }
313
314 #[test]
315 fn read_mupdf_version_returns_none_for_malformed_header() {
316 let tmp = tempfile::tempdir().unwrap();
317 let header = tmp.path().join("version.h");
318 fs::write(&header, "/* no version define here */\n").unwrap();
319
320 let version = read_mupdf_version(&header);
321 assert!(version.is_none());
322 }
323}