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 only)");
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 std::fs::remove_dir_all(&mupdf_dir).context("failed to remove stale thirdparty/mupdf")?;
89 }
90
91 thirdparty::download_libraries(&root.join("thirdparty"), &["mupdf"])
92}
93
94fn read_mupdf_version(header: &Path) -> Option<String> {
98 let content = std::fs::read_to_string(header).ok()?;
99
100 for line in content.lines() {
102 if line.contains("FZ_VERSION") && line.contains('"') {
103 let start = line.find('"')? + 1;
104 let end = line.rfind('"')?;
105 if start < end {
106 return Some(line[start..end].to_owned());
107 }
108 }
109 }
110
111 None
112}
113
114fn native_mupdf_ready(root: &Path) -> bool {
116 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
117 if !marker.exists() {
118 return false;
119 }
120
121 let libmupdf = root.join("thirdparty/mupdf/build/release/libmupdf.a");
122
123 libmupdf.exists()
124}
125
126fn write_native_build_marker(root: &Path) -> Result<()> {
128 let marker = root.join("thirdparty/mupdf").join(NATIVE_BUILT_MARKER);
129 std::fs::write(&marker, "").with_context(|| {
130 format!(
131 "failed to write native build marker at {}",
132 marker.display()
133 )
134 })
135}
136
137fn build_mupdf_wrapper_native_if_needed(root: &Path) -> Result<()> {
139 println!("Ensuring mupdf_wrapper is available…");
140 mupdf_wrapper::build_native_if_needed(root)
141}
142
143fn build_mupdf_native(root: &Path) -> Result<()> {
145 println!("Building MuPDF for native development…");
146
147 let mupdf_dir = root.join("thirdparty/mupdf");
148
149 for entry in ["gitattributes", ".gitattributes"] {
151 let path = mupdf_dir.join(entry);
152 if path.exists() {
153 std::fs::remove_file(&path).ok();
154 }
155 }
156
157 cmd::run("make", &["clean"], &mupdf_dir, &[]).ok();
158 cmd::run("make", &["verbose=yes", "generate"], &mupdf_dir, &[])?;
159
160 let sys_cflags = collect_system_cflags()?;
161 let xcflags = format!(
162 "-DFZ_ENABLE_ICC=0 -DFZ_ENABLE_SPOT_RENDERING=0 \
163 -DFZ_ENABLE_ODT_OUTPUT=0 -DFZ_ENABLE_OCR_OUTPUT=0 {sys_cflags}"
164 );
165
166 cmd::run(
167 "make",
168 &[
169 "verbose=yes",
170 "mujs=no",
171 "tesseract=no",
172 "extract=no",
173 "archive=no",
174 "brotli=no",
175 "barcode=no",
176 "commercial=no",
177 "USE_SYSTEM_LIBS=yes",
178 &format!("XCFLAGS={xcflags}"),
179 "libs",
180 ],
181 &mupdf_dir,
182 &[],
183 )
184}
185
186fn collect_system_cflags() -> Result<String> {
191 if !cfg!(target_os = "macos") {
192 return Ok(String::new());
193 }
194
195 let libs = [
196 "freetype2",
197 "harfbuzz",
198 "libopenjp2",
199 "libjpeg",
200 "zlib",
201 "jbig2dec",
202 "gumbo",
203 ];
204
205 let mut flags = String::new();
206 for lib in libs {
207 if let Ok(f) = cmd::output("pkg-config", &["--cflags", lib], Path::new("."), &[]) {
208 if !f.is_empty() {
209 flags.push(' ');
210 flags.push_str(&f);
211 }
212 }
213 }
214
215 Ok(flags.trim().to_owned())
216}
217
218fn link_mupdf_artifacts(root: &Path) -> Result<()> {
221 let platform_dir = if cfg!(target_os = "macos") {
222 "Darwin"
223 } else {
224 "Linux"
225 };
226
227 let target_dir = root.join(format!("target/mupdf_wrapper/{platform_dir}"));
228 std::fs::create_dir_all(&target_dir)
229 .context("failed to create target/mupdf_wrapper directory")?;
230
231 let release_dir = root.join("thirdparty/mupdf/build/release");
232
233 let libmupdf = release_dir.join("libmupdf.a");
234 if !libmupdf.exists() {
235 bail!("libmupdf.a not found after build — check MuPDF build output");
236 }
237
238 symlink_force(&libmupdf, &target_dir.join("libmupdf.a"))?;
239 println!("✓ Created libmupdf.a in target/mupdf_wrapper/{platform_dir}");
240
241 let libmupdf_third = release_dir.join("libmupdf-third.a");
242 if !libmupdf_third.exists() {
243 println!("Creating empty libmupdf-third.a (system libs used instead)…");
244 cmd::run("ar", &["cr", "libmupdf-third.a"], &release_dir, &[])?;
245 }
246
247 symlink_force(&libmupdf_third, &target_dir.join("libmupdf-third.a"))?;
248 println!("✓ Created libmupdf-third.a");
249
250 Ok(())
251}
252
253fn symlink_force(target: &Path, link: &Path) -> Result<()> {
256 if link.exists() || link.symlink_metadata().is_ok() {
257 std::fs::remove_file(link)
258 .with_context(|| format!("failed to remove existing {}", link.display()))?;
259 }
260
261 #[cfg(unix)]
262 std::os::unix::fs::symlink(target, link)
263 .with_context(|| format!("failed to create symlink {}", link.display()))?;
264
265 #[cfg(not(unix))]
266 std::fs::copy(target, link)
267 .with_context(|| format!("failed to copy {} to {}", target.display(), link.display()))?;
268
269 Ok(())
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use std::fs;
276
277 #[test]
278 fn read_mupdf_version_parses_define() {
279 let tmp = tempfile::tempdir().unwrap();
280 let header = tmp.path().join("version.h");
281 fs::write(
282 &header,
283 r#"/* MuPDF version */
284#define FZ_VERSION "1.27.0"
285#define FZ_VERSION_MAJOR 1
286"#,
287 )
288 .unwrap();
289
290 let version = read_mupdf_version(&header);
291 assert_eq!(version.as_deref(), Some("1.27.0"));
292 }
293
294 #[test]
295 fn read_mupdf_version_returns_none_for_malformed_header() {
296 let tmp = tempfile::tempdir().unwrap();
297 let header = tmp.path().join("version.h");
298 fs::write(&header, "/* no version define here */\n").unwrap();
299
300 let version = read_mupdf_version(&header);
301 assert!(version.is_none());
302 }
303}