1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12use std::process::Command;
13
14fn pkg_config_name_for_package(pkg_src_dir: &Path) -> Option<&'static str> {
19 let pkg_dir = pkg_src_dir.parent()?;
22 let pkg_name = pkg_dir.file_name()?.to_str()?;
23 match pkg_name {
24 "openssl" => Some("openssl"),
25 "xml2" => Some("libxml-2.0"),
26 "stringi" => Some("icu-i18n"),
27 "curl" => Some("libcurl"),
28 "sodium" => Some("libsodium"),
29 "fs" => Some("libuv"),
30 "cairo" => Some("cairo"),
31 "RPostgres" | "RPostgreSQL" => Some("libpq"),
32 "magick" => Some("Magick++"),
33 "poppler" => Some("poppler-cpp"),
34 "protolite" => Some("protobuf"),
35 "pdftools" => Some("poppler-glib"),
36 "rsvg" => Some("librsvg-2.0"),
37 "gifski" => Some("gifski"),
38 _ => None,
39 }
40}
41
42fn extract_pkg_config_name_from_configure(pkg_src_dir: &Path) -> Option<String> {
45 let configure = pkg_src_dir.parent()?.join("configure");
46 let content = std::fs::read_to_string(configure).ok()?;
47 for line in content.lines() {
48 let trimmed = line.trim();
49 if let Some(rest) = trimmed.strip_prefix("PKG_CONFIG_NAME=") {
50 let name = rest.trim_matches('"').trim_matches('\'');
51 if !name.is_empty() {
52 return Some(name.to_string());
53 }
54 }
55 }
56 None
57}
58
59fn resolve_anticonf(pkg_src_dir: &Path, makevars_in_content: &str) -> String {
62 let lib_name = pkg_config_name_for_package(pkg_src_dir)
64 .map(String::from)
65 .or_else(|| extract_pkg_config_name_from_configure(pkg_src_dir));
66
67 let (cflags, libs) = if let Some(ref name) = lib_name {
68 match pkg_config::Config::new()
70 .cargo_metadata(false)
71 .env_metadata(false)
72 .probe(name)
73 {
74 Ok(lib) => {
75 let cflags: String = lib
76 .include_paths
77 .iter()
78 .map(|p| format!("-I{}", p.display()))
79 .collect::<Vec<_>>()
80 .join(" ");
81 let libs: String = {
82 let mut parts: Vec<String> = lib
83 .link_paths
84 .iter()
85 .map(|p| format!("-L{}", p.display()))
86 .collect();
87 parts.extend(lib.libs.iter().map(|l| format!("-l{l}")));
88 parts.join(" ")
89 };
90 tracing::debug!(
91 pkg = name,
92 cflags = cflags.as_str(),
93 libs = libs.as_str(),
94 "pkg-config resolved"
95 );
96 (cflags, libs)
97 }
98 Err(e) => {
99 tracing::debug!(pkg = name, error = %e, "pkg-config failed");
100 (String::new(), String::new())
101 }
102 }
103 } else {
104 (String::new(), String::new())
105 };
106
107 makevars_in_content
109 .replace("@cflags@", &cflags)
110 .replace("@libs@", &libs)
111 .replace("@CFLAGS@", &cflags)
113 .replace("@LIBS@", &libs)
114 .replace("@STRINGI_CPPFLAGS@", &cflags)
116 .replace("@STRINGI_LIBS@", &libs)
117 .replace("@STRINGI_LDFLAGS@", "")
118 .replace("@STRINGI_CXXSTD@", "-std=c++17")
119 .split('\n')
121 .map(|line| {
122 if line.contains('@') {
123 let mut result = line.to_string();
125 while let Some(start) = result.find('@') {
126 if let Some(end) = result[start + 1..].find('@') {
127 result.replace_range(start..=start + 1 + end, "");
128 } else {
129 break;
130 }
131 }
132 result
133 } else {
134 line.to_string()
135 }
136 })
137 .collect::<Vec<_>>()
138 .join("\n")
139}
140
141fn emulate_configure(pkg_src_dir: &Path) {
144 let pkg_dir = match pkg_src_dir.parent() {
145 Some(d) => d,
146 None => return,
147 };
148 let pkg_name = match pkg_dir.file_name().and_then(|n| n.to_str()) {
149 Some(n) => n,
150 None => return,
151 };
152
153 if pkg_name == "ps" {
154 emulate_configure_ps(pkg_src_dir);
155 } else if pkg_name == "fs" {
156 emulate_configure_fs(pkg_src_dir);
157 } else if pkg_name == "sass" {
158 emulate_configure_system_lib(pkg_src_dir, "libsass", "-I./libsass/include");
159 }
160}
161
162fn emulate_configure_ps(pkg_src_dir: &Path) {
164 let config_h = pkg_src_dir.join("config.h");
166 if config_h.exists() {
167 return;
168 }
169
170 let is_macos = cfg!(target_os = "macos");
172 let is_linux = cfg!(target_os = "linux");
173
174 let mut macros = vec![("PS__VERSION", "546")];
175 let mut objects = vec![
176 "init.o",
177 "api-common.o",
178 "common.o",
179 "extra.o",
180 "dummy.o",
181 "error-codes.o",
182 "cleancall.o",
183 ];
184
185 if is_macos || is_linux {
186 macros.push(("PS__POSIX", "1"));
187 objects.extend(&["posix.o", "api-posix.o"]);
188 }
189
190 if is_macos {
191 macros.push(("PS__MACOS", "1"));
192 for obj in &[
194 "macos.o",
195 "api-macos.o",
196 "arch/macos/process_info.o",
197 "arch/macos/disk.o",
198 "arch/macos/apps.o",
199 ] {
200 let stem = obj.strip_suffix(".o").unwrap();
201 if pkg_src_dir.join(format!("{stem}.c")).is_file() {
202 objects.push(obj);
203 }
204 }
205 } else if is_linux {
206 macros.push(("PS__LINUX", "1"));
207 objects.extend(&["linux.o", "api-linux.o"]);
208 }
209
210 let mut config = String::from("/* Generated by miniR configure emulation */\n");
212 for (name, value) in ¯os {
213 config.push_str(&format!("#define {name} {value}\n"));
214 }
215 if let Err(e) = std::fs::write(&config_h, &config) {
216 tracing::warn!(error = %e, "failed to write config.h for ps");
217 return;
218 }
219
220 let makevars_path = pkg_src_dir.join("Makevars");
222 if !makevars_path.exists() {
223 let objects_str = objects.join(" ");
224 let makevars = format!("OBJECTS = {objects_str}\nPKG_LIBS =\n");
225 if let Err(e) = std::fs::write(&makevars_path, &makevars) {
226 tracing::warn!(error = %e, "failed to write Makevars for ps");
227 }
228 }
229
230 tracing::debug!(
231 pkg = "ps",
232 "configure emulated: config.h + Makevars generated"
233 );
234}
235
236fn emulate_configure_fs(pkg_src_dir: &Path) {
241 let makevars_path = pkg_src_dir.join("Makevars");
242
243 if makevars_path.exists() {
245 if let Ok(content) = std::fs::read_to_string(&makevars_path) {
246 if content.contains("# Generated by miniR") {
247 return;
248 }
249 }
250 }
251
252 let lib = match pkg_config::Config::new()
254 .cargo_metadata(false)
255 .env_metadata(false)
256 .probe("libuv")
257 {
258 Ok(lib) => lib,
259 Err(_) => return, };
261
262 let cflags: String = lib
263 .include_paths
264 .iter()
265 .map(|p| format!("-I{}", p.display()))
266 .collect::<Vec<_>>()
267 .join(" ");
268 let libs: String = {
269 let mut parts: Vec<String> = lib
270 .link_paths
271 .iter()
272 .map(|p| format!("-L{}", p.display()))
273 .collect();
274 parts.extend(lib.libs.iter().map(|l| format!("-l{l}")));
275 parts.push("-lpthread".to_string());
276 parts.join(" ")
277 };
278
279 let makevars = format!(
281 "# Generated by miniR (system libuv)\n\
282 PKG_CPPFLAGS = {cflags} -I. -pthread\n\
283 PKG_LIBS = {libs}\n\
284 PKG_CFLAGS = -fvisibility=hidden\n"
285 );
286
287 if let Err(e) = std::fs::write(&makevars_path, &makevars) {
288 tracing::warn!(error = %e, "failed to write Makevars for fs");
289 return;
290 }
291 tracing::debug!(
292 cflags = cflags.as_str(),
293 libs = libs.as_str(),
294 "fs: using system libuv"
295 );
296}
297
298fn emulate_configure_system_lib(
302 pkg_src_dir: &Path,
303 pkg_config_name: &str,
304 fallback_cppflags: &str,
305) {
306 let makevars_path = pkg_src_dir.join("Makevars");
307 if makevars_path.exists() {
308 let content = std::fs::read_to_string(&makevars_path).unwrap_or_default();
309 if content.contains("# Generated by miniR") {
311 return;
312 }
313 if !content.contains(".a") {
315 return;
316 }
317 }
318
319 let lib = match pkg_config::Config::new()
320 .cargo_metadata(false)
321 .env_metadata(false)
322 .probe(pkg_config_name)
323 {
324 Ok(lib) => lib,
325 Err(_) => return, };
327
328 let cflags: String = lib
329 .include_paths
330 .iter()
331 .map(|p| format!("-I{}", p.display()))
332 .collect::<Vec<_>>()
333 .join(" ");
334 let libs: String = {
335 let mut parts: Vec<String> = lib
336 .link_paths
337 .iter()
338 .map(|p| format!("-L{}", p.display()))
339 .collect();
340 parts.extend(lib.libs.iter().map(|l| format!("-l{l}")));
341 parts.join(" ")
342 };
343
344 let makevars = format!(
345 "# Generated by miniR (pkg-config {pkg_config_name})\nPKG_CPPFLAGS = {cflags} {fallback_cppflags}\nPKG_LIBS = {libs}\n"
346 );
347
348 if let Err(e) = std::fs::write(&makevars_path, &makevars) {
349 tracing::warn!(error = %e, pkg = pkg_config_name, "failed to write Makevars");
350 return;
351 }
352 tracing::debug!(pkg = pkg_config_name, "using system library via pkg-config");
353}
354
355#[derive(Debug, Default)]
361pub struct Makevars {
362 pub vars: HashMap<String, String>,
364}
365
366impl Makevars {
367 pub fn parse(path: &Path) -> Self {
371 if let Ok(content) = std::fs::read_to_string(path) {
373 return Self::parse_str(&content);
374 }
375
376 let makevars_in = path.with_extension("in");
378 if let Ok(content) = std::fs::read_to_string(&makevars_in) {
379 let resolved = resolve_anticonf(path.parent().unwrap_or(Path::new(".")), &content);
380 return Self::parse_str(&resolved);
381 }
382
383 Makevars::default()
384 }
385
386 pub fn parse_str(content: &str) -> Self {
391 let mut vars = HashMap::new();
392 let mut continued_key: Option<String> = None;
393 let mut continued_val = String::new();
394 let mut in_conditional = 0i32; for line in content.lines() {
397 let line = line.trim();
398
399 if line.starts_with('#') || line.is_empty() {
401 if continued_key.is_some() {
402 if let Some(key) = continued_key.take() {
403 vars.insert(key, continued_val.trim().to_string());
404 continued_val.clear();
405 }
406 }
407 continue;
408 }
409
410 if line.starts_with("ifeq")
412 || line.starts_with("ifdef")
413 || line.starts_with("ifneq")
414 || line.starts_with("ifndef")
415 {
416 in_conditional += 1;
417 continue;
418 }
419 if line.starts_with("endif") {
420 in_conditional = (in_conditional - 1).max(0);
421 continue;
422 }
423 if line == "else" || line.starts_with("else ") {
424 continue;
425 }
426 if in_conditional > 0 {
427 continue;
428 }
429
430 if let Some(colon_pos) = line.find(':') {
432 if let Some(eq_pos) = line.find('=') {
433 if colon_pos + 1 != eq_pos
435 && colon_pos < eq_pos
436 && !line[..colon_pos].contains('$')
437 {
438 continue; }
440 } else {
441 continue; }
443 }
444
445 if let Some(ref key) = continued_key {
447 let (val, has_cont) = strip_continuation(line);
448 continued_val.push(' ');
449 continued_val.push_str(val.trim());
450 if !has_cont {
451 vars.insert(key.clone(), continued_val.trim().to_string());
452 continued_key = None;
453 continued_val.clear();
454 }
455 continue;
456 }
457
458 if let Some((key, op, val_part)) = parse_assignment(line) {
460 let (val, has_continuation) = strip_continuation(val_part);
461 let val = val.trim();
462
463 match op {
464 AssignOp::Set => {
465 if has_continuation {
466 continued_key = Some(key.to_string());
467 continued_val = val.trim().to_string();
468 } else {
469 vars.insert(key.to_string(), val.trim().to_string());
470 }
471 }
472 AssignOp::Append => {
473 let existing = vars.get(key).cloned().unwrap_or_default();
474 let val = val.trim();
475 let new_val = if existing.is_empty() {
476 val.to_string()
477 } else {
478 format!("{existing} {val}")
479 };
480 if has_continuation {
481 continued_key = Some(key.to_string());
482 continued_val = new_val;
483 } else {
484 vars.insert(key.to_string(), new_val);
485 }
486 }
487 }
488 }
489 }
490
491 if let Some(key) = continued_key {
493 vars.insert(key, continued_val.trim().to_string());
494 }
495
496 let mut expanded = vars;
500 for _ in 0..5 {
501 let snapshot = expanded.clone();
502 let mut changed = false;
503 for value in expanded.values_mut() {
504 let mut result = String::with_capacity(value.len());
505 let mut rest = value.as_str();
506 while let Some(start) = rest.find("$(") {
507 result.push_str(&rest[..start]);
508 let after = &rest[start + 2..];
509 if let Some(end) = after.find(')') {
510 let var = &after[..end];
511 if let Some(replacement) = snapshot.get(var) {
512 result.push_str(replacement);
513 changed = true;
514 }
515 rest = &after[end + 1..];
517 } else {
518 result.push_str(&rest[start..]);
519 rest = "";
520 }
521 }
522 result.push_str(rest);
523 *value = result;
524 }
525 if !changed {
526 break;
527 }
528 }
529
530 let final_vars: HashMap<String, String> = expanded
532 .into_iter()
533 .map(|(k, v)| (k, expand_make_vars(&v)))
534 .collect();
535
536 Makevars { vars: final_vars }
537 }
538
539 pub fn pkg_cflags(&self) -> &str {
541 self.vars.get("PKG_CFLAGS").map_or("", |s| s.as_str())
542 }
543
544 pub fn pkg_cppflags(&self) -> &str {
546 self.vars.get("PKG_CPPFLAGS").map_or("", |s| s.as_str())
547 }
548
549 pub fn pkg_libs(&self) -> &str {
551 self.vars.get("PKG_LIBS").map_or("", |s| s.as_str())
552 }
553
554 pub fn pkg_cxxflags(&self) -> &str {
556 self.vars.get("PKG_CXXFLAGS").map_or("", |s| s.as_str())
557 }
558
559 pub fn objects(&self) -> Option<&str> {
562 self.vars.get("OBJECTS").map(|s| s.as_str())
563 }
564}
565
566#[derive(Debug, PartialEq)]
567enum AssignOp {
568 Set,
569 Append,
570}
571
572fn parse_assignment(line: &str) -> Option<(&str, AssignOp, &str)> {
575 if let Some(pos) = line.find("+=") {
577 let key = line[..pos].trim();
578 let val = &line[pos + 2..];
579 if !key.is_empty() && is_valid_makevars_key(key) {
580 return Some((key, AssignOp::Append, val));
581 }
582 }
583
584 if let Some(pos) = line.find('=') {
586 let (key, val) = if pos > 0 && line.as_bytes()[pos - 1] == b':' {
588 (line[..pos - 1].trim(), &line[pos + 1..])
589 } else {
590 (line[..pos].trim(), &line[pos + 1..])
591 };
592 if !key.is_empty() && is_valid_makevars_key(key) {
593 return Some((key, AssignOp::Set, val));
594 }
595 }
596
597 None
598}
599
600fn is_valid_makevars_key(s: &str) -> bool {
602 s.chars()
603 .all(|c| c.is_alphanumeric() || c == '_' || c == '.')
604}
605
606fn expand_make_vars(s: &str) -> String {
609 let mut result = String::with_capacity(s.len());
610 let mut chars = s.chars().peekable();
611
612 while let Some(ch) = chars.next() {
613 if ch == '$' && chars.peek() == Some(&'(') {
614 chars.next(); let mut var_name = String::new();
617 let mut depth = 1;
618 for c in chars.by_ref() {
619 if c == '(' {
620 depth += 1;
621 } else if c == ')' {
622 depth -= 1;
623 if depth == 0 {
624 break;
625 }
626 }
627 var_name.push(c);
628 }
629 match var_name.as_str() {
631 "C_VISIBILITY" | "CXX_VISIBILITY" => {
632 result.push_str("-fvisibility=hidden");
633 }
634 "F_VISIBILITY" | "FPICFLAGS" | "CPICFLAGS" => {
635 }
637 "SHLIB_OPENMP_CFLAGS" | "SHLIB_OPENMP_CXXFLAGS" => {
638 }
640 "BLAS_LIBS" | "LAPACK_LIBS" | "FLIBS" => {
641 }
643 "CC" | "CXX" | "AR" | "RANLIB" | "MAKE" | "RM" | "SHLIB" | "STATLIB"
645 | "OBJECTS" | "LIBR" | "SHLIB_EXT" | "SHLIB_LINK" | "SHLIB_LIBADD"
646 | "SHLIB_CXXLD" | "SHLIB_CXXLDFLAGS" | "SHLIB_FFLAGS" | "CFLAGS" | "CPPFLAGS"
647 | "LDFLAGS" | "SAFE_FFLAGS" | "R_ARCH" | "R_ARCH_BIN" | "R_HOME" | "R_CC"
648 | "R_CXX" | "R_CONFIGURE_FLAGS" | "CONFIGURE_ARGS" | "ALL_CFLAGS"
649 | "ALL_CPPFLAGS" | "UNAME" | "OS" | "WIN" | "CYGWIN" | "CC_TARGET"
650 | "CLANG_CHECK" => {}
651 _ => {
652 tracing::debug!("Makevars: stripping unknown variable $({})", var_name);
654 }
655 }
656 } else {
657 result.push(ch);
658 }
659 }
660
661 let mut clean = result.replace(" ", " ");
663 while clean.contains(" ") {
664 clean = clean.replace(" ", " ");
665 }
666 clean.trim().to_string()
667}
668
669fn strip_continuation(s: &str) -> (&str, bool) {
671 let trimmed = s.trim_end();
672 match trimmed.strip_suffix('\\') {
673 Some(without) => (without, true),
674 None => (trimmed, false),
675 }
676}
677
678fn current_target_triple() -> String {
684 if let Ok(target) = std::env::var("TARGET") {
686 return target;
687 }
688 let arch = std::env::consts::ARCH;
690 let os = std::env::consts::OS;
691 let vendor_os = match os {
693 "macos" => "apple-darwin",
694 "linux" => "unknown-linux-gnu",
695 "windows" => "pc-windows-msvc",
696 "freebsd" => "unknown-freebsd",
697 other => other,
698 };
699 format!("{arch}-{vendor_os}")
700}
701
702fn dylib_ext() -> &'static str {
704 if cfg!(target_os = "macos") {
705 "dylib"
706 } else {
707 "so"
708 }
709}
710
711pub fn compile_package(
726 pkg_src_dir: &Path,
727 pkg_name: &str,
728 output_dir: &Path,
729 include_dir: &Path,
730) -> Result<PathBuf, String> {
731 compile_package_with_deps(pkg_src_dir, pkg_name, output_dir, include_dir, &[])
732}
733
734pub fn compile_package_with_deps(
736 pkg_src_dir: &Path,
737 pkg_name: &str,
738 output_dir: &Path,
739 include_dir: &Path,
740 linking_to_includes: &[PathBuf],
741) -> Result<PathBuf, String> {
742 emulate_configure(pkg_src_dir);
744
745 let makevars = Makevars::parse(&pkg_src_dir.join("Makevars"));
747
748 let src_files = find_sources(pkg_src_dir, &makevars)?;
750 if src_files.is_empty() {
751 return Err(format!(
752 "no C/C++ source files found in {}",
753 pkg_src_dir.display()
754 ));
755 }
756
757 let target = current_target_triple();
767
768 let mut c_files = Vec::new();
770 let mut cpp_files = Vec::new();
771 let mut fortran_files = Vec::new();
772 for f in &src_files {
773 match f.extension().and_then(|e| e.to_str()) {
774 Some("cpp" | "cc" | "cxx" | "C") => cpp_files.push(f.clone()),
775 Some("f" | "f90" | "f95" | "F" | "F90" | "F95") => fortran_files.push(f.clone()),
776 _ => c_files.push(f.clone()),
777 }
778 }
779 let has_cpp = !cpp_files.is_empty();
780 let has_fortran = !fortran_files.is_empty();
781
782 let configure_build = |build: &mut cc::Build| {
784 build
785 .pic(true)
786 .warnings(false)
787 .debug(true)
788 .flag("-fno-omit-frame-pointer")
789 .cargo_metadata(false) .flag_if_supported("-Wno-incompatible-function-pointer-types")
792 .flag_if_supported("-Wno-int-conversion")
793 .flag_if_supported("-Wno-error")
794 .flag_if_supported("-Wno-return-type")
796 .target(&target)
797 .host(&target)
798 .opt_level(2)
799 .define("HAVE_UNISTD_H", None)
801 .define("HAVE_GETTIMEOFDAY", None)
802 .define("HAVE_NANOSLEEP", None);
803 if cfg!(target_os = "macos") {
804 build.define("MB_HAVE_MACH_TIME", None);
805 } else {
806 build
807 .define("MB_HAVE_CLOCK_GETTIME", None)
808 .define("MB_CLOCKID_T", Some("CLOCK_REALTIME"));
809 }
810 build
811 .out_dir(output_dir)
812 .include(include_dir)
813 .include(include_dir.join("miniR"))
814 .include(pkg_src_dir);
815
816 for inc in linking_to_includes {
818 build.include(inc);
819 }
820
821 let cppflags = makevars.pkg_cppflags();
823 if !cppflags.is_empty() {
824 for flag in shell_split(cppflags) {
825 if let Some(rel_path) = flag.strip_prefix("-I") {
826 let rel_path = rel_path.trim_matches('"').trim_matches('\'');
827 let path = Path::new(rel_path);
828 if path.is_relative() {
829 build.include(pkg_src_dir.join(path));
830 } else {
831 build.include(path);
832 }
833 } else {
834 build.flag(&flag);
835 }
836 }
837 }
838 };
839
840 let mut object_files = Vec::new();
841
842 if !c_files.is_empty() {
844 let mut c_build = cc::Build::new();
845 configure_build(&mut c_build);
846 let cflags = makevars.pkg_cflags();
847 if !cflags.is_empty() {
848 for flag in shell_split(cflags) {
849 c_build.flag(&flag);
850 }
851 }
852 for src in &c_files {
853 c_build.file(src);
854 }
855 let c_objs = c_build
856 .try_compile_intermediates()
857 .map_err(|e| format!("C compilation failed: {e}"))?;
858 object_files.extend(c_objs);
859 }
860
861 if has_cpp {
863 let mut cxx_build = cc::Build::new();
864 configure_build(&mut cxx_build);
865 cxx_build.cpp(true).std("c++17");
866 let cxxflags = makevars.pkg_cxxflags();
867 if !cxxflags.is_empty() {
868 for flag in shell_split(cxxflags) {
869 if let Some(rel_path) = flag.strip_prefix("-I") {
871 let rel_path = rel_path.trim_matches('"').trim_matches('\'');
872 let path = Path::new(rel_path);
873 if path.is_relative() {
874 cxx_build.include(pkg_src_dir.join(path));
875 } else {
876 cxx_build.include(path);
877 }
878 } else {
879 cxx_build.flag(&flag);
880 }
881 }
882 }
883 for src in &cpp_files {
884 cxx_build.file(src);
885 }
886 let cxx_objs = cxx_build
887 .try_compile_intermediates()
888 .map_err(|e| format!("C++ compilation failed: {e}"))?;
889 object_files.extend(cxx_objs);
890 }
891
892 if has_fortran {
894 let gfortran = std::env::var("FC")
895 .or_else(|_| std::env::var("F77"))
896 .unwrap_or_else(|_| "gfortran".to_string());
897
898 let fflags = makevars.vars.get("PKG_FFLAGS").cloned().unwrap_or_default();
900
901 for src in &fortran_files {
902 let stem = src.file_stem().and_then(|s| s.to_str()).unwrap_or("f");
903 let obj_path = output_dir.join(format!("{stem}.o"));
904 let mut cmd = Command::new(&gfortran);
905 cmd.arg("-c")
906 .arg("-fPIC")
907 .arg("-O2")
908 .arg(format!("-J{}", output_dir.display()))
910 .arg("-o")
911 .arg(&obj_path);
912 for flag in shell_split(&fflags) {
914 cmd.arg(&flag);
915 }
916 cmd.arg(src);
917 let output = cmd
918 .output()
919 .map_err(|e| format!("failed to run gfortran: {e}"))?;
920 if !output.status.success() {
921 let stderr = String::from_utf8_lossy(&output.stderr);
922 return Err(format!(
923 "Fortran compilation failed for {}:\n{stderr}",
924 src.display()
925 ));
926 }
927 object_files.push(obj_path);
928 }
929 }
930
931 let linker_build = if has_cpp {
934 let mut b = cc::Build::new();
935 b.cpp(true)
936 .cargo_metadata(false)
937 .target(&target)
938 .host(&target)
939 .opt_level(2);
940 b
941 } else {
942 let mut b = cc::Build::new();
943 b.cargo_metadata(false)
944 .target(&target)
945 .host(&target)
946 .opt_level(2);
947 b
948 };
949 let linker = linker_build
950 .try_get_compiler()
951 .map_err(|e| format!("cannot find compiler: {e}"))?;
952
953 let lib_name = format!("{pkg_name}.{}", dylib_ext());
954 let lib_path = output_dir.join(&lib_name);
955
956 let mut cmd = Command::new(linker.path());
957 cmd.arg("-shared").arg("-o").arg(&lib_path);
958
959 for obj in &object_files {
960 cmd.arg(obj);
961 }
962
963 if cfg!(target_os = "macos") {
965 cmd.arg("-undefined").arg("dynamic_lookup");
966 cmd.arg("-framework").arg("Accelerate");
968 }
969
970 if has_cpp {
972 if cfg!(target_os = "macos") {
973 cmd.arg("-lc++");
974 } else {
975 cmd.arg("-lstdc++");
976 }
977 }
978
979 if has_fortran {
981 let gfortran = std::env::var("FC")
982 .or_else(|_| std::env::var("F77"))
983 .unwrap_or_else(|_| "gfortran".to_string());
984 if let Ok(output) = Command::new(&gfortran)
986 .arg("-print-file-name=libgfortran.dylib")
987 .output()
988 {
989 if output.status.success() {
990 let lib_path = String::from_utf8_lossy(&output.stdout).trim().to_string();
991 if let Some(dir) = Path::new(&lib_path).parent() {
992 cmd.arg(format!("-L{}", dir.display()));
993 }
994 }
995 }
996 cmd.arg("-lgfortran");
997 }
998
999 let libs = makevars.pkg_libs();
1004 if !libs.is_empty() {
1005 let flags = shell_split(libs);
1006 let has_local_lib = flags
1007 .iter()
1008 .any(|f| f.starts_with("-L") && !f.starts_with("-L/"));
1009 for flag in &flags {
1010 if flag.starts_with("-L") && !flag.starts_with("-L/") {
1011 continue; }
1013 if has_local_lib && flag.starts_with("-l") {
1014 continue; }
1016 if flag.ends_with(".o") || flag.ends_with(".a") {
1019 let obj_path = if Path::new(flag).is_relative() {
1020 pkg_src_dir.join(flag)
1021 } else {
1022 PathBuf::from(flag)
1023 };
1024 if !obj_path.exists() && flag.ends_with(".o") {
1025 let c_src = obj_path.with_extension("c");
1027 if c_src.is_file() {
1028 let mut obj_build = cc::Build::new();
1029 configure_build(&mut obj_build);
1030 obj_build.file(&c_src);
1031 if let Ok(objs) = obj_build.try_compile_intermediates() {
1032 object_files.extend(objs);
1033 continue; }
1035 }
1036 }
1037 if obj_path.exists() {
1038 cmd.arg(&obj_path);
1039 }
1040 continue;
1041 }
1042 cmd.arg(flag);
1043 }
1044 }
1045
1046 let output = cmd
1047 .output()
1048 .map_err(|e| format!("failed to run linker: {e}"))?;
1049
1050 if !output.status.success() {
1051 let stderr = String::from_utf8_lossy(&output.stderr);
1052 return Err(format!("linking {lib_name} failed:\n{stderr}"));
1053 }
1054
1055 Ok(lib_path)
1056}
1057
1058fn find_sources(src_dir: &Path, makevars: &Makevars) -> Result<Vec<PathBuf>, String> {
1070 let mut all_objects: Vec<String> = Vec::new();
1072
1073 for (key, value) in &makevars.vars {
1074 if matches!(
1076 key.as_str(),
1077 "PKG_CFLAGS" | "PKG_CPPFLAGS" | "PKG_CXXFLAGS" | "PKG_LIBS" | "CXX_STD"
1078 ) {
1079 continue;
1080 }
1081 for token in shell_split(value) {
1083 let token = token.trim().to_string();
1084 if token.ends_with(".o") {
1085 all_objects.push(token);
1086 }
1087 }
1088 }
1089
1090 if !all_objects.is_empty() {
1091 let mut sources = Vec::new();
1093 let mut seen = std::collections::HashSet::new();
1094 for obj in &all_objects {
1095 let stem = if let Some(s) = obj.strip_suffix(".o") {
1096 s
1097 } else {
1098 continue;
1099 };
1100 for ext in &["c", "cpp", "cc", "cxx", "f", "f90", "f95"] {
1101 let path = src_dir.join(format!("{stem}.{ext}"));
1102 if path.is_file() && seen.insert(path.clone()) {
1103 sources.push(path);
1104 break;
1105 }
1106 }
1107 }
1108
1109 let has_explicit_objects = makevars.vars.contains_key("OBJECTS");
1113 if !has_explicit_objects {
1114 if let Ok(entries) = std::fs::read_dir(src_dir) {
1117 for entry in entries.flatten() {
1118 let path = entry.path();
1119 if path.is_file() {
1120 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
1121 if matches!(ext, "c" | "cpp" | "cc" | "cxx" | "f" | "f90" | "f95")
1122 && seen.insert(path.clone())
1123 {
1124 sources.push(path);
1125 }
1126 }
1127 }
1128 }
1129 }
1130 }
1131
1132 sources.sort();
1133 Ok(sources)
1134 } else {
1135 let mut sources = Vec::new();
1137 let entries = std::fs::read_dir(src_dir)
1138 .map_err(|e| format!("cannot read {}: {e}", src_dir.display()))?;
1139 for entry in entries {
1140 let entry = entry.map_err(|e| format!("readdir error: {e}"))?;
1141 let path = entry.path();
1142 if path.is_file() {
1143 if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
1144 if matches!(ext, "c" | "cpp" | "cc" | "cxx" | "f" | "f90" | "f95") {
1145 sources.push(path);
1146 }
1147 }
1148 }
1149 }
1150 sources.sort();
1151 Ok(sources)
1152 }
1153}
1154
1155fn shell_split(s: &str) -> Vec<String> {
1158 let mut result = Vec::new();
1159 let mut current = String::new();
1160 let mut in_quotes = false;
1161
1162 for ch in s.chars() {
1163 match ch {
1164 '"' => in_quotes = !in_quotes,
1165 ' ' | '\t' if !in_quotes => {
1166 if !current.is_empty() {
1167 result.push(std::mem::take(&mut current));
1168 }
1169 }
1170 _ => current.push(ch),
1171 }
1172 }
1173 if !current.is_empty() {
1174 result.push(current);
1175 }
1176 result
1177}
1178
1179#[cfg(test)]
1182mod tests {
1183 use super::*;
1184
1185 #[test]
1186 fn parse_simple_makevars() {
1187 let content = r#"
1188# Package flags
1189PKG_CFLAGS = -Wall -O2
1190PKG_LIBS = -lz
1191OBJECTS = foo.o bar.o
1192"#;
1193 let mv = Makevars::parse_str(content);
1194 assert_eq!(mv.pkg_cflags(), "-Wall -O2");
1195 assert_eq!(mv.pkg_libs(), "-lz");
1196 assert_eq!(mv.objects(), Some("foo.o bar.o"));
1197 }
1198
1199 #[test]
1200 fn parse_continuation_lines() {
1201 let content = "PKG_CFLAGS = -Wall \\\n -O2 \\\n -Wextra\n";
1202 let mv = Makevars::parse_str(content);
1203 assert_eq!(mv.pkg_cflags(), "-Wall -O2 -Wextra");
1204 }
1205
1206 #[test]
1207 fn parse_append_operator() {
1208 let content = "PKG_CFLAGS = -Wall\nPKG_CFLAGS += -O2\n";
1209 let mv = Makevars::parse_str(content);
1210 assert_eq!(mv.pkg_cflags(), "-Wall -O2");
1211 }
1212
1213 #[test]
1214 fn parse_colon_equals() {
1215 let content = "PKG_CFLAGS := -Wall\n";
1216 let mv = Makevars::parse_str(content);
1217 assert_eq!(mv.pkg_cflags(), "-Wall");
1218 }
1219
1220 #[test]
1221 fn empty_makevars() {
1222 let mv = Makevars::parse_str("");
1223 assert_eq!(mv.pkg_cflags(), "");
1224 assert_eq!(mv.pkg_libs(), "");
1225 assert!(mv.objects().is_none());
1226 }
1227
1228 #[test]
1229 fn shell_split_basic() {
1230 assert_eq!(shell_split("-Wall -O2"), vec!["-Wall", "-O2"]);
1231 assert_eq!(shell_split(" -I/usr/include "), vec!["-I/usr/include"]);
1232 assert_eq!(shell_split(r#"-DFOO="bar baz""#), vec!["-DFOO=bar baz"]);
1233 }
1234}