1use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25
26use serde::{Deserialize, Serialize};
27use tracing::debug;
28
29use crate::error::{Result, ToolchainError};
30use zlayer_types::package_index::FormulaData;
31
32const RAW_BASE: &str = "https://raw.githubusercontent.com/Homebrew/homebrew-core/HEAD";
34const CDN_BASE: &str = "https://cdn.jsdelivr.net/gh/Homebrew/homebrew-core@HEAD";
36
37pub async fn fetch_recipe(formula: &FormulaData) -> Result<String> {
52 let Some(path) = formula
53 .ruby_source_path
54 .as_deref()
55 .filter(|p| !p.is_empty())
56 else {
57 return Err(ToolchainError::RegistryError {
58 message: "formula JSON has no ruby_source_path; cannot fetch the Homebrew recipe \
59 (package index entry predates ruby_source_path support?)"
60 .to_string(),
61 });
62 };
63 let client = reqwest::Client::builder()
64 .user_agent("zlayer-toolchain")
65 .build()
66 .unwrap_or_default();
67 let primary = format!("{RAW_BASE}/{path}");
68 match try_get_text(&client, &primary).await {
69 Ok(text) => Ok(text),
70 Err(primary_err) => {
71 debug!(path, error = %primary_err, "raw.githubusercontent fetch failed; trying jsDelivr fallback");
72 let fallback = format!("{CDN_BASE}/{path}");
73 try_get_text(&client, &fallback)
74 .await
75 .map_err(|fallback_err| ToolchainError::RegistryError {
76 message: format!(
77 "failed to fetch recipe {path}: {primary_err}; fallback: {fallback_err}"
78 ),
79 })
80 }
81 }
82}
83
84pub async fn fetch_recipe_cached(formula: &FormulaData, cache_path: &Path) -> Result<String> {
94 if let Ok(cached) = tokio::fs::read_to_string(cache_path).await {
95 if !cached.is_empty() {
96 return Ok(cached);
97 }
98 }
99 let text = fetch_recipe(formula).await?;
100 if let Some(parent) = cache_path.parent() {
101 tokio::fs::create_dir_all(parent).await?;
102 }
103 tokio::fs::write(cache_path, &text).await?;
104 Ok(text)
105}
106
107async fn try_get_text(client: &reqwest::Client, url: &str) -> Result<String> {
109 let resp = client
110 .get(url)
111 .send()
112 .await
113 .map_err(|e| ToolchainError::RegistryError {
114 message: format!("failed to GET {url}: {e}"),
115 })?;
116 if !resp.status().is_success() {
117 return Err(ToolchainError::RegistryError {
118 message: format!("GET {url} returned status {}", resp.status()),
119 });
120 }
121 resp.text()
122 .await
123 .map_err(|e| ToolchainError::RegistryError {
124 message: format!("failed to read body from {url}: {e}"),
125 })
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136pub struct RecipeCtx {
137 pub tool: String,
139 pub version: String,
141 pub prefix: PathBuf,
143 pub dep_prefixes: HashMap<String, PathBuf>,
146 pub target_macos: bool,
148 pub target_arm: bool,
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct ResourceSpec {
157 pub name: String,
159 pub url: String,
161 pub sha256: String,
163 pub stage_to: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171pub struct PatchSpec {
172 pub url: String,
174 pub sha256: String,
176 pub strip: u32,
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub enum InstallStep {
184 System {
187 argv: Vec<String>,
189 },
190 Configure {
192 args: Vec<String>,
194 },
195 Make {
197 args: Vec<String>,
199 },
200 CMake {
202 args: Vec<String>,
204 },
205 Bootstrap {
207 args: Vec<String>,
209 },
210 Cargo {
212 args: Vec<String>,
214 },
215 Go {
217 args: Vec<String>,
219 },
220 Meson {
224 args: Vec<String>,
226 },
227 BinInstall {
229 from: String,
231 to: Option<String>,
233 },
234 LibInstall {
236 from: String,
238 to: Option<String>,
240 },
241 IncludeInstall {
243 from: String,
245 to: Option<String>,
247 },
248 PrefixInstall {
254 from: String,
256 to: Option<String>,
258 },
259 EnvSet {
262 key: String,
264 value: String,
266 },
267 Inreplace {
269 file: String,
271 from: String,
273 to: String,
275 },
276 StageResource {
279 name: String,
281 dir: Option<String>,
283 },
284 ApplyPatch {
286 index: usize,
288 },
289 Unsupported {
293 construct: String,
295 },
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
300pub struct InstallPlan {
301 pub steps: Vec<InstallStep>,
304 pub resources: Vec<ResourceSpec>,
306 pub patches: Vec<PatchSpec>,
308 pub env: HashMap<String, String>,
311 pub deparallelize: bool,
313}
314
315impl InstallPlan {
316 #[must_use]
319 pub fn is_fully_supported(&self) -> bool {
320 !self
321 .steps
322 .iter()
323 .any(|s| matches!(s, InstallStep::Unsupported { .. }))
324 }
325
326 #[must_use]
328 pub fn unsupported_constructs(&self) -> Vec<&str> {
329 self.steps
330 .iter()
331 .filter_map(|s| match s {
332 InstallStep::Unsupported { construct } => Some(construct.as_str()),
333 _ => None,
334 })
335 .collect()
336 }
337}
338
339pub fn parse_install_plan(rb: &str, ctx: &RecipeCtx) -> Result<InstallPlan> {
354 let mut parser = Parser {
355 ctx,
356 lines: logical_lines(rb),
357 pos: 0,
358 steps: Vec::new(),
359 resources: Vec::new(),
360 patches: Vec::new(),
361 env: HashMap::new(),
362 deparallelize: false,
363 saw_install: false,
364 };
365 parser.parse_top_level();
366 if !parser.saw_install {
367 return Err(ToolchainError::RegistryError {
368 message: format!(
369 "no `def install` block found in formula source for {}",
370 ctx.tool
371 ),
372 });
373 }
374 Ok(parser.finish())
375}
376
377fn logical_lines(rb: &str) -> Vec<String> {
386 let phys: Vec<&str> = rb.lines().collect();
387 let mut out = Vec::new();
388 let mut i = 0;
389 while i < phys.len() {
390 let mut line = strip_comment(phys[i]).trim().to_string();
391 i += 1;
392 if line.is_empty() {
393 continue;
394 }
395 loop {
396 if let Some(id) = heredoc_id(&line) {
397 while i < phys.len() {
400 let body = phys[i].trim();
401 i += 1;
402 if body == id {
403 break;
404 }
405 }
406 }
407 if has_unclosed_word_array(&line) && i < phys.len() {
408 let next = strip_comment(phys[i]).trim().to_string();
409 i += 1;
410 line.push(' ');
411 line.push_str(&next);
412 continue;
413 }
414 if (line.ends_with(',') || line.ends_with('(')) && i < phys.len() {
415 let next = strip_comment(phys[i]).trim().to_string();
416 i += 1;
417 if !next.is_empty() {
418 line.push(' ');
419 line.push_str(&next);
420 }
421 continue;
422 }
423 if line.ends_with('\\') && i < phys.len() {
424 line.pop();
425 let next = strip_comment(phys[i]).trim().to_string();
426 i += 1;
427 if !next.is_empty() {
428 line.push(' ');
429 line.push_str(&next);
430 }
431 continue;
432 }
433 break;
434 }
435 out.push(line);
436 }
437 out
438}
439
440fn strip_comment(raw: &str) -> String {
444 let mut out = String::new();
445 let mut in_double = false;
446 let mut in_single = false;
447 let mut escaped = false;
448 let mut chars = raw.chars().peekable();
449 while let Some(c) = chars.next() {
450 if escaped {
451 out.push(c);
452 escaped = false;
453 continue;
454 }
455 match c {
456 '\\' if in_double || in_single => {
457 out.push(c);
458 escaped = true;
459 }
460 '"' if !in_single => {
461 in_double = !in_double;
462 out.push(c);
463 }
464 '\'' if !in_double => {
465 in_single = !in_single;
466 out.push(c);
467 }
468 '#' if !in_double && !in_single => {
469 if chars.peek() == Some(&'{') {
470 out.push(c);
471 } else {
472 break;
473 }
474 }
475 _ => out.push(c),
476 }
477 }
478 out
479}
480
481fn heredoc_id(line: &str) -> Option<String> {
484 let mut in_double = false;
485 let mut in_single = false;
486 let mut escaped = false;
487 let bytes = line.as_bytes();
488 for (i, c) in line.char_indices() {
489 if escaped {
490 escaped = false;
491 continue;
492 }
493 match c {
494 '\\' if in_double || in_single => escaped = true,
495 '"' if !in_single => in_double = !in_double,
496 '\'' if !in_double => in_single = !in_single,
497 '<' if !in_double && !in_single && bytes.get(i + 1) == Some(&b'<') => {
498 let mut j = i + 2;
499 while matches!(bytes.get(j), Some(b'~' | b'-')) {
500 j += 1;
501 }
502 let start = j;
503 while matches!(bytes.get(j), Some(b'A'..=b'Z' | b'0'..=b'9' | b'_')) {
504 j += 1;
505 }
506 if j > start && bytes[start].is_ascii_uppercase() {
507 return Some(line[start..j].to_string());
508 }
509 }
510 _ => {}
511 }
512 }
513 None
514}
515
516fn has_unclosed_word_array(line: &str) -> bool {
519 let mut in_double = false;
520 let mut in_single = false;
521 let mut escaped = false;
522 let bytes = line.as_bytes();
523 for (i, c) in line.char_indices() {
524 if escaped {
525 escaped = false;
526 continue;
527 }
528 match c {
529 '\\' if in_double || in_single => escaped = true,
530 '"' if !in_single => in_double = !in_double,
531 '\'' if !in_double => in_single = !in_single,
532 '%' if !in_double
533 && !in_single
534 && matches!(bytes.get(i + 1), Some(b'w' | b'W'))
535 && bytes.get(i + 2) == Some(&b'[') =>
536 {
537 return !line[i + 3..].contains(']');
538 }
539 _ => {}
540 }
541 }
542 false
543}
544
545fn starts_with_word(line: &str, w: &str) -> bool {
551 line == w || (line.starts_with(w) && line[w.len()..].starts_with(' '))
552}
553
554fn closes_block(line: &str) -> bool {
556 line == "end"
557 || line.starts_with("end ")
558 || line.starts_with("end.")
559 || line.starts_with("end,")
560}
561
562fn ends_with_do(line: &str) -> bool {
565 let t = line.trim_end();
566 let t = if let Some(without_bar) = t.strip_suffix('|') {
567 match without_bar.rfind('|') {
568 Some(p) => t[..p].trim_end(),
569 None => return false,
570 }
571 } else {
572 t
573 };
574 t == "do" || t.ends_with(" do")
575}
576
577fn find_trailing_conditional(line: &str) -> Option<(usize, bool)> {
581 let mut best = None;
582 let mut in_double = false;
583 let mut in_single = false;
584 let mut escaped = false;
585 let mut depth = 0i32;
586 for (i, c) in line.char_indices() {
587 if escaped {
588 escaped = false;
589 continue;
590 }
591 match c {
592 '\\' if in_double || in_single => escaped = true,
593 '"' if !in_single => in_double = !in_double,
594 '\'' if !in_double => in_single = !in_single,
595 '(' | '[' | '{' if !in_double && !in_single => depth += 1,
596 ')' | ']' | '}' if !in_double && !in_single => depth -= 1,
597 ' ' if !in_double && !in_single && depth == 0 => {
598 let rest = &line[i..];
599 if rest.starts_with(" if ") {
600 best = Some((i, false));
601 } else if rest.starts_with(" unless ") {
602 best = Some((i, true));
603 }
604 }
605 _ => {}
606 }
607 }
608 best
609}
610
611fn is_expression_if(line: &str) -> bool {
615 if let Some((pos, _)) = find_trailing_conditional(line) {
616 matches!(
617 line[..pos].trim_end().chars().last(),
618 Some('=' | '(' | ',' | '{')
619 )
620 } else {
621 false
622 }
623}
624
625struct Modifier<'a> {
627 head: &'a str,
629 cond: &'a str,
631 negated: bool,
633}
634
635fn analyze_modifier(line: &str) -> Option<Modifier<'_>> {
638 if starts_with_word(line, "if") || starts_with_word(line, "unless") {
639 return None;
640 }
641 let (pos, negated) = find_trailing_conditional(line)?;
642 let head = line[..pos].trim_end();
643 if matches!(head.chars().last(), Some('=' | '(' | ',' | '{')) {
644 return None; }
646 let kw_len = if negated {
647 " unless ".len()
648 } else {
649 " if ".len()
650 };
651 Some(Modifier {
652 head,
653 cond: line[pos + kw_len..].trim(),
654 negated,
655 })
656}
657
658fn opens_block(line: &str) -> bool {
660 const OPENER_WORDS: [&str; 9] = [
661 "if", "unless", "def", "class", "module", "case", "while", "until", "begin",
662 ];
663 if OPENER_WORDS.iter().any(|w| starts_with_word(line, w)) {
664 return true;
665 }
666 ends_with_do(line) || is_expression_if(line)
667}
668
669fn split_args(s: &str) -> Vec<String> {
676 let mut out = Vec::new();
677 let mut cur = String::new();
678 let mut in_double = false;
679 let mut in_single = false;
680 let mut escaped = false;
681 let mut depth = 0i32;
682 for c in s.chars() {
683 if escaped {
684 cur.push(c);
685 escaped = false;
686 continue;
687 }
688 match c {
689 '\\' if in_double || in_single => {
690 cur.push(c);
691 escaped = true;
692 }
693 '"' if !in_single => {
694 in_double = !in_double;
695 cur.push(c);
696 }
697 '\'' if !in_double => {
698 in_single = !in_single;
699 cur.push(c);
700 }
701 '(' | '[' | '{' if !in_double && !in_single => {
702 depth += 1;
703 cur.push(c);
704 }
705 ')' | ']' | '}' if !in_double && !in_single => {
706 depth -= 1;
707 cur.push(c);
708 }
709 ',' if !in_double && !in_single && depth == 0 => {
710 out.push(cur.trim().to_string());
711 cur.clear();
712 }
713 _ => cur.push(c),
714 }
715 }
716 if !cur.trim().is_empty() {
717 out.push(cur.trim().to_string());
718 }
719 out
720}
721
722fn split_rename(token: &str) -> (String, Option<String>) {
724 let mut in_double = false;
725 let mut in_single = false;
726 let mut escaped = false;
727 for (i, c) in token.char_indices() {
728 if escaped {
729 escaped = false;
730 continue;
731 }
732 match c {
733 '\\' if in_double || in_single => escaped = true,
734 '"' if !in_single => in_double = !in_double,
735 '\'' if !in_double => in_single = !in_single,
736 ' ' if !in_double && !in_single && token[i..].starts_with(" => ") => {
737 return (
738 token[..i].trim().to_string(),
739 Some(token[i + 4..].trim().to_string()),
740 );
741 }
742 _ => {}
743 }
744 }
745 (token.to_string(), None)
746}
747
748fn quoted_literal(s: &str) -> Option<String> {
751 let s = s.trim();
752 for q in ['"', '\''] {
753 if s.len() >= 2 && s.starts_with(q) && s.ends_with(q) && !s[1..s.len() - 1].contains(q) {
754 return Some(s[1..s.len() - 1].to_string());
755 }
756 }
757 None
758}
759
760fn string_call_arg(line: &str, kw: &str) -> Option<String> {
762 let rest = line.strip_prefix(kw)?;
763 let rest = rest.strip_prefix(' ').or_else(|| rest.strip_prefix('('))?;
764 let toks = split_args(rest.trim_end_matches(')'));
765 quoted_literal(toks.first()?)
766}
767
768struct Parser<'a> {
774 ctx: &'a RecipeCtx,
775 lines: Vec<String>,
776 pos: usize,
777 steps: Vec<InstallStep>,
778 resources: Vec<ResourceSpec>,
779 patches: Vec<PatchSpec>,
780 env: HashMap<String, String>,
781 deparallelize: bool,
782 saw_install: bool,
783}
784
785enum BranchEnd {
787 End,
789 Else,
791 Elsif(String),
793}
794
795impl Parser<'_> {
796 fn take(&mut self) -> String {
798 let line = self.lines[self.pos].clone();
799 self.pos += 1;
800 line
801 }
802
803 fn unsupported(&mut self, construct: &str) {
805 self.steps.push(InstallStep::Unsupported {
806 construct: construct.to_string(),
807 });
808 }
809
810 fn unsupported_line(&mut self, line: &str) {
813 self.unsupported(line);
814 if opens_block(line) {
815 self.skip_block();
816 }
817 }
818
819 fn skip_block(&mut self) {
821 let mut depth = 1usize;
822 while self.pos < self.lines.len() {
823 let line = self.take();
824 if closes_block(&line) {
825 depth -= 1;
826 if depth == 0 {
827 return;
828 }
829 } else if opens_block(&line) {
830 depth += 1;
831 }
832 }
833 }
834
835 fn skip_block_counting(&mut self) -> usize {
838 let mut depth = 1usize;
839 let mut count = 0usize;
840 while self.pos < self.lines.len() {
841 let line = self.take();
842 if closes_block(&line) {
843 depth -= 1;
844 if depth == 0 {
845 return count;
846 }
847 } else if opens_block(&line) {
848 depth += 1;
849 }
850 count += 1;
851 }
852 count
853 }
854
855 fn parse_top_level(&mut self) {
859 while self.pos < self.lines.len() {
860 let line = self.take();
861 if starts_with_word(&line, "class") && line.contains("< Formula") {
862 self.class_region();
863 return;
864 }
865 }
866 }
867
868 fn class_region(&mut self) {
870 while self.pos < self.lines.len() {
871 let line = self.take();
872 if closes_block(&line) {
873 return;
874 }
875 self.class_line(&line);
876 }
877 }
878
879 fn class_line(&mut self, line: &str) {
881 if let Some(name) = parse_resource_open(line) {
882 self.resource_block(name, line);
883 return;
884 }
885 if starts_with_word(line, "patch") {
886 self.patch_line(line);
887 return;
888 }
889 if let Some(keep) = self.on_block_keep(line) {
890 if keep {
891 self.class_region();
892 } else {
893 self.skip_block();
894 }
895 return;
896 }
897 if line == "def install" {
898 self.saw_install = true;
899 self.install_region();
900 return;
901 }
902 if opens_block(line) {
903 self.skip_block();
906 }
907 }
910
911 fn patch_line(&mut self, line: &str) {
913 if line == "patch :DATA" {
914 self.unsupported("patch :DATA");
915 return;
916 }
917 let strip = match line {
918 "patch do" => Some(1),
919 _ => line
920 .strip_prefix("patch :p")
921 .and_then(|r| r.strip_suffix(" do"))
922 .and_then(|n| n.parse::<u32>().ok()),
923 };
924 match strip {
925 Some(strip) => {
926 let (url, sha256) = self.url_sha_block();
927 match (url, sha256) {
928 (Some(url), Some(sha256)) => {
929 self.patches.push(PatchSpec { url, sha256, strip });
930 }
931 _ => self.unsupported(line),
932 }
933 }
934 None => self.unsupported_line(line),
935 }
936 }
937
938 fn resource_block(&mut self, name: String, open_line: &str) {
940 let (url, sha256) = self.url_sha_block();
941 match (url, sha256) {
942 (Some(url), Some(sha256)) => self.resources.push(ResourceSpec {
943 name,
944 url,
945 sha256,
946 stage_to: None,
947 }),
948 _ => self.unsupported(open_line),
949 }
950 }
951
952 fn url_sha_block(&mut self) -> (Option<String>, Option<String>) {
955 let mut depth = 1usize;
956 let mut url = None;
957 let mut sha = None;
958 while self.pos < self.lines.len() {
959 let line = self.take();
960 if closes_block(&line) {
961 depth -= 1;
962 if depth == 0 {
963 break;
964 }
965 continue;
966 }
967 if opens_block(&line) {
968 depth += 1;
969 continue;
970 }
971 if depth == 1 {
972 if url.is_none() {
973 if let Some(u) = string_call_arg(&line, "url") {
974 url = Some(u);
975 continue;
976 }
977 }
978 if sha.is_none() {
979 if let Some(s) = string_call_arg(&line, "sha256") {
980 sha = Some(s);
981 }
982 }
983 }
984 }
985 (url, sha)
986 }
987
988 fn on_block_keep(&self, line: &str) -> Option<bool> {
991 match line {
992 "on_macos do" => Some(self.ctx.target_macos),
993 "on_linux do" => Some(!self.ctx.target_macos),
994 "on_arm do" => Some(self.ctx.target_arm),
995 "on_intel do" => Some(!self.ctx.target_arm),
996 _ => None,
997 }
998 }
999
1000 fn fold_condition(&self, cond: &str) -> Option<bool> {
1002 let cond = cond.trim();
1003 let (cond, negated) = cond
1004 .strip_prefix('!')
1005 .map_or((cond, false), |rest| (rest.trim_start(), true));
1006 let value = match cond {
1007 "OS.mac?" => self.ctx.target_macos,
1008 "OS.linux?" => !self.ctx.target_macos,
1009 "Hardware::CPU.arm?" => self.ctx.target_arm,
1010 "Hardware::CPU.intel?" => !self.ctx.target_arm,
1011 "build.head?" => false,
1012 "build.stable?" => true,
1013 _ => return None,
1014 };
1015 Some(value != negated)
1016 }
1017
1018 fn install_region(&mut self) {
1022 while self.pos < self.lines.len() {
1023 let line = self.take();
1024 if closes_block(&line) {
1025 return;
1026 }
1027 self.install_line(&line);
1028 }
1029 }
1030
1031 fn install_line(&mut self, line: &str) {
1033 if let Some(cond) = line.strip_prefix("if ") {
1035 self.if_construct(line, cond, false);
1036 return;
1037 }
1038 if let Some(cond) = line.strip_prefix("unless ") {
1039 self.if_construct(line, cond, true);
1040 return;
1041 }
1042 if let Some(keep) = self.on_block_keep(line) {
1043 if keep {
1044 self.install_region();
1045 } else {
1046 self.skip_block();
1047 }
1048 return;
1049 }
1050 if let Some(m) = analyze_modifier(line) {
1052 match self.fold_condition(m.cond).map(|v| v != m.negated) {
1053 Some(true) => {
1054 let head = m.head.to_string();
1055 self.install_line(&head);
1056 }
1057 Some(false) => {}
1058 None => self.unsupported_line(line),
1059 }
1060 return;
1061 }
1062 if self.env_line(line)
1063 || self.system_line(line)
1064 || self.inreplace_line(line)
1065 || self.receiver_install_line(line)
1066 || self.resource_stage_line(line)
1067 {
1068 return;
1069 }
1070 self.unsupported_line(line);
1071 }
1072
1073 fn if_construct(&mut self, line: &str, cond: &str, negated: bool) {
1075 if let Some(keep_then) = self.fold_condition(cond).map(|v| v != negated) {
1076 self.if_branches(keep_then);
1077 } else {
1078 self.unsupported(line);
1079 self.skip_block();
1080 }
1081 }
1082
1083 fn if_branches(&mut self, keep_then: bool) {
1085 match self.branch_region(keep_then) {
1086 BranchEnd::End => {}
1087 BranchEnd::Else => {
1088 let _ = self.branch_region(!keep_then);
1089 }
1090 BranchEnd::Elsif(elsif_line) => {
1091 if keep_then {
1092 self.skip_block();
1094 } else {
1095 let cond = elsif_line.trim_start_matches("elsif").trim().to_string();
1096 if let Some(keep) = self.fold_condition(&cond) {
1097 self.if_branches(keep);
1098 } else {
1099 self.unsupported(&elsif_line);
1100 self.skip_block();
1101 }
1102 }
1103 }
1104 }
1105 }
1106
1107 fn branch_region(&mut self, execute: bool) -> BranchEnd {
1110 while self.pos < self.lines.len() {
1111 let line = self.take();
1112 if closes_block(&line) {
1113 return BranchEnd::End;
1114 }
1115 if line == "else" {
1116 return BranchEnd::Else;
1117 }
1118 if starts_with_word(&line, "elsif") {
1119 return BranchEnd::Elsif(line);
1120 }
1121 if execute {
1122 self.install_line(&line);
1123 } else if opens_block(&line) {
1124 self.skip_block();
1125 }
1126 }
1127 BranchEnd::End
1128 }
1129
1130 fn env_line(&mut self, line: &str) -> bool {
1134 if line == "ENV.deparallelize" {
1135 self.deparallelize = true;
1136 return true;
1137 }
1138 if let Some(rest) = line.strip_prefix("ENV[") {
1139 let Some((key_part, after)) = rest.split_once(']') else {
1140 self.unsupported_line(line);
1141 return true;
1142 };
1143 let (Some(key), Some(expr)) = (
1144 quoted_literal(key_part),
1145 after.trim_start().strip_prefix('=').map(str::trim),
1146 ) else {
1147 self.unsupported_line(line);
1148 return true;
1149 };
1150 if expr.starts_with('=') {
1151 self.unsupported_line(line);
1153 return true;
1154 }
1155 match self.resolve_value(expr) {
1156 Some(value) => self.set_env(key, value),
1157 None => self.unsupported_line(line),
1158 }
1159 return true;
1160 }
1161 for (kw, prepend) in [("ENV.append ", false), ("ENV.prepend ", true)] {
1162 if let Some(rest) = line.strip_prefix(kw) {
1163 self.env_merge(line, rest, prepend);
1164 return true;
1165 }
1166 }
1167 false
1168 }
1169
1170 fn env_merge(&mut self, line: &str, rest: &str, prepend: bool) {
1172 let toks = split_args(rest);
1173 let (Some(key), Some(value)) = (
1174 toks.first().and_then(|t| quoted_literal(t)),
1175 toks.get(1).and_then(|t| self.resolve_value(t)),
1176 ) else {
1177 self.unsupported_line(line);
1178 return;
1179 };
1180 if toks.len() != 2 {
1181 self.unsupported_line(line);
1182 return;
1183 }
1184 let merged = match self.env.get(&key) {
1185 Some(existing) if prepend => format!("{value} {existing}"),
1186 Some(existing) => format!("{existing} {value}"),
1187 None => value,
1188 };
1189 self.set_env(key, merged);
1190 }
1191
1192 fn set_env(&mut self, key: String, value: String) {
1194 self.steps.push(InstallStep::EnvSet {
1195 key: key.clone(),
1196 value: value.clone(),
1197 });
1198 self.env.insert(key, value);
1199 }
1200
1201 fn system_line(&mut self, line: &str) -> bool {
1204 let rest = if let Some(r) = line.strip_prefix("system ") {
1205 r
1206 } else if let Some(r) = line
1207 .strip_prefix("system(")
1208 .and_then(|r| r.strip_suffix(')'))
1209 {
1210 r
1211 } else {
1212 return false;
1213 };
1214 let mut argv = Vec::new();
1215 for token in split_args(rest) {
1216 if let Some(mut vals) = self.resolve_arg(&token) {
1217 argv.append(&mut vals);
1218 } else {
1219 self.unsupported_line(line);
1220 return true;
1221 }
1222 }
1223 if argv.is_empty() {
1224 self.unsupported_line(line);
1225 return true;
1226 }
1227 self.steps.push(classify_system(argv));
1228 true
1229 }
1230
1231 fn resolve_arg(&self, token: &str) -> Option<Vec<String>> {
1233 if let Some(splat) = token.strip_prefix('*') {
1234 return self.resolve_splat(splat);
1235 }
1236 self.resolve_value(token).map(|v| vec![v])
1237 }
1238
1239 fn resolve_splat(&self, splat: &str) -> Option<Vec<String>> {
1241 let prefix = self.ctx.prefix.display().to_string();
1242 match splat {
1243 "std_configure_args" => Some(vec![
1244 format!("--prefix={prefix}"),
1245 format!("--libdir={prefix}/lib"),
1246 "--disable-debug".to_string(),
1247 "--disable-dependency-tracking".to_string(),
1248 "--disable-silent-rules".to_string(),
1249 ]),
1250 "std_cmake_args" => Some(vec![
1251 format!("-DCMAKE_INSTALL_PREFIX={prefix}"),
1252 "-DCMAKE_BUILD_TYPE=Release".to_string(),
1253 "-DCMAKE_FIND_FRAMEWORK=LAST".to_string(),
1254 "-DBUILD_TESTING=OFF".to_string(),
1255 ]),
1256 "std_cargo_args" => Some(vec![
1257 "--locked".to_string(),
1258 "--root".to_string(),
1259 prefix,
1260 "--path".to_string(),
1261 ".".to_string(),
1262 ]),
1263 "std_go_args" => self.std_go_args(None),
1264 _ => splat
1265 .strip_prefix("std_go_args(")
1266 .and_then(|r| r.strip_suffix(')'))
1267 .and_then(|kwargs| self.std_go_args(Some(kwargs))),
1268 }
1269 }
1270
1271 fn std_go_args(&self, kwargs: Option<&str>) -> Option<Vec<String>> {
1274 let mut output = format!("{}/bin/{}", self.ctx.prefix.display(), self.ctx.tool);
1275 let mut ldflags = None;
1276 let mut gcflags = None;
1277 let mut tags = None;
1278 if let Some(kwargs) = kwargs {
1279 for part in split_args(kwargs) {
1280 let (key, value) = part.split_once(':')?;
1281 let value = self.resolve_value(value.trim())?;
1282 match key.trim() {
1283 "output" => output = value,
1284 "ldflags" => ldflags = Some(value),
1285 "gcflags" => gcflags = Some(value),
1286 "tags" => tags = Some(value),
1287 _ => return None,
1288 }
1289 }
1290 }
1291 let mut args = vec!["-trimpath".to_string(), format!("-o={output}")];
1292 if let Some(tags) = tags {
1293 args.push(format!("-tags={tags}"));
1294 }
1295 if let Some(ldflags) = ldflags {
1296 args.push(format!("-ldflags={ldflags}"));
1297 }
1298 if let Some(gcflags) = gcflags {
1299 args.push(format!("-gcflags={gcflags}"));
1300 }
1301 Some(args)
1302 }
1303
1304 fn inreplace_line(&mut self, line: &str) -> bool {
1307 let Some(rest) = line.strip_prefix("inreplace ") else {
1308 return false;
1309 };
1310 if ends_with_do(line) {
1311 self.unsupported(line);
1312 self.skip_block();
1313 return true;
1314 }
1315 let toks = split_args(rest);
1316 if toks.len() == 3
1317 && toks
1318 .iter()
1319 .all(|t| t.starts_with('"') || t.starts_with('\''))
1320 {
1321 if let (Some(file), Some(from), Some(to)) = (
1322 self.resolve_value(&toks[0]),
1323 self.resolve_value(&toks[1]),
1324 self.resolve_value(&toks[2]),
1325 ) {
1326 self.steps.push(InstallStep::Inreplace { file, from, to });
1327 return true;
1328 }
1329 }
1330 self.unsupported(line);
1331 true
1332 }
1333
1334 fn receiver_install_line(&mut self, line: &str) -> bool {
1338 const RECEIVERS: [&str; 8] = [
1339 "bin", "lib", "include", "libexec", "share", "man1", "pkgshare", "prefix",
1340 ];
1341 let Some((recv, rest)) = RECEIVERS.iter().find_map(|r| {
1342 line.strip_prefix(&format!("{r}.install "))
1343 .map(|rest| (*r, rest))
1344 }) else {
1345 return false;
1346 };
1347 let mut staged = Vec::new();
1348 for token in split_args(rest) {
1349 let (from_expr, rename_expr) = split_rename(&token);
1350 let Some(from) = self.resolve_value(&from_expr) else {
1351 self.unsupported(line);
1352 return true;
1353 };
1354 let rename = match rename_expr {
1355 None => None,
1356 Some(expr) => {
1357 if let Some(v) = self.resolve_value(&expr) {
1358 Some(v)
1359 } else {
1360 self.unsupported(line);
1361 return true;
1362 }
1363 }
1364 };
1365 staged.push(make_install_step(recv, &self.ctx.tool, from, rename));
1366 }
1367 if staged.is_empty() {
1368 self.unsupported(line);
1369 } else {
1370 self.steps.extend(staged);
1371 }
1372 true
1373 }
1374
1375 fn resource_stage_line(&mut self, line: &str) -> bool {
1378 let Some(rest) = line.strip_prefix("resource(") else {
1379 return false;
1380 };
1381 let Some((name_part, tail)) = rest.split_once(')') else {
1382 self.unsupported_line(line);
1383 return true;
1384 };
1385 let Some(name) = quoted_literal(name_part) else {
1386 self.unsupported_line(line);
1387 return true;
1388 };
1389 if tail == ".stage" {
1390 self.steps
1391 .push(InstallStep::StageResource { name, dir: None });
1392 } else if tail == ".stage do" || (tail.starts_with(".stage do |") && tail.ends_with('|')) {
1393 if self.skip_block_counting() == 0 {
1396 self.steps
1397 .push(InstallStep::StageResource { name, dir: None });
1398 } else {
1399 self.unsupported(line);
1400 }
1401 } else if let Some(dir_expr) = tail
1402 .strip_prefix(".stage(")
1403 .and_then(|t| t.strip_suffix(')'))
1404 {
1405 match self.resolve_value(dir_expr) {
1406 Some(dir) => self.steps.push(InstallStep::StageResource {
1407 name,
1408 dir: normalize_stage_dir(&dir),
1409 }),
1410 None => self.unsupported(line),
1411 }
1412 } else {
1413 self.unsupported_line(line);
1414 }
1415 true
1416 }
1417
1418 fn resolve_value(&self, expr: &str) -> Option<String> {
1423 let expr = expr.trim();
1424 if let Some(body) = full_quoted_body(expr, '"') {
1425 return self.resolve_dquote(body);
1426 }
1427 if let Some(body) = full_quoted_body(expr, '\'') {
1428 return Some(unescape_single(body));
1429 }
1430 self.resolve_expr(expr)
1431 }
1432
1433 fn resolve_dquote(&self, body: &str) -> Option<String> {
1435 let mut out = String::new();
1436 let bytes = body.as_bytes();
1437 let mut i = 0;
1438 while i < body.len() {
1439 let c = body[i..].chars().next()?;
1440 if c == '\\' {
1441 let next = body[i + 1..].chars().next()?;
1442 out.push(match next {
1443 'n' => '\n',
1444 't' => '\t',
1445 's' => ' ',
1446 '0' => '\0',
1447 'e' => '\u{1b}',
1448 other => other,
1449 });
1450 i += 1 + next.len_utf8();
1451 continue;
1452 }
1453 if c == '#' && bytes.get(i + 1) == Some(&b'{') {
1454 let close = body[i + 2..].find('}')? + i + 2;
1455 out.push_str(&self.resolve_interp(&body[i + 2..close])?);
1456 i = close + 1;
1457 continue;
1458 }
1459 out.push(c);
1460 i += c.len_utf8();
1461 }
1462 Some(out)
1463 }
1464
1465 fn resolve_interp(&self, inner: &str) -> Option<String> {
1467 self.resolve_value(inner)
1468 }
1469
1470 fn resolve_expr(&self, expr: &str) -> Option<String> {
1473 if let Some(v) = self.resolve_atom(expr) {
1474 return Some(v);
1475 }
1476 let segs = split_top_level_slash(expr);
1477 if segs.len() >= 2 {
1478 let mut path = self.resolve_atom(segs[0].trim())?;
1479 for seg in &segs[1..] {
1480 let part = self.resolve_value(seg.trim())?;
1481 let base = path.trim_end_matches('/').to_string();
1482 path = format!("{base}/{part}");
1483 }
1484 return Some(path);
1485 }
1486 None
1487 }
1488
1489 fn resolve_atom(&self, expr: &str) -> Option<String> {
1491 let prefix = self.ctx.prefix.display().to_string();
1492 let subdir = |d: &str| Some(format!("{prefix}/{d}"));
1493 match expr {
1494 "prefix" | "opt_prefix" => Some(prefix),
1495 "bin" | "opt_bin" => subdir("bin"),
1496 "lib" | "opt_lib" => subdir("lib"),
1497 "include" | "opt_include" => subdir("include"),
1498 "libexec" | "opt_libexec" => subdir("libexec"),
1499 "share" | "opt_share" => subdir("share"),
1500 "etc" => subdir("etc"),
1501 "var" => subdir("var"),
1502 "pkgshare" => subdir(&format!("share/{}", self.ctx.tool)),
1503 "man" => subdir("share/man"),
1504 "buildpath" => Some(".".to_string()),
1505 "version" => Some(self.ctx.version.clone()),
1506 "name" => Some(self.ctx.tool.clone()),
1507 "ENV.cc" => Some("cc".to_string()),
1508 "ENV.cxx" => Some("c++".to_string()),
1509 _ => {
1510 if let Some(rest) = expr.strip_prefix("man") {
1511 if rest.len() == 1 && rest.chars().all(|c| c.is_ascii_digit()) {
1512 return subdir(&format!("share/man/man{rest}"));
1513 }
1514 }
1515 self.resolve_formula_accessor(expr)
1516 }
1517 }
1518 }
1519
1520 fn resolve_formula_accessor(&self, expr: &str) -> Option<String> {
1523 if let Some(rest) = expr.strip_prefix("Formula[") {
1524 let (name_part, attr_part) = rest.split_once(']')?;
1525 let dep = quoted_literal(name_part)?;
1526 let attr = attr_part.strip_prefix('.')?;
1527 let base = self.ctx.dep_prefixes.get(&dep)?.display().to_string();
1528 return match attr {
1529 "opt_prefix" | "prefix" => Some(base),
1530 "opt_bin" | "bin" => Some(format!("{base}/bin")),
1531 "opt_lib" | "lib" => Some(format!("{base}/lib")),
1532 "opt_include" | "include" => Some(format!("{base}/include")),
1533 "opt_libexec" | "libexec" => Some(format!("{base}/libexec")),
1534 "opt_share" | "share" => Some(format!("{base}/share")),
1535 _ => None,
1536 };
1537 }
1538 let dep = expr
1539 .strip_prefix("formula_opt_prefix(")
1540 .and_then(|r| r.strip_suffix(')'))
1541 .and_then(quoted_literal)?;
1542 self.ctx
1543 .dep_prefixes
1544 .get(&dep)
1545 .map(|p| p.display().to_string())
1546 }
1547
1548 fn finish(mut self) -> InstallPlan {
1552 let stages: Vec<(String, Option<String>)> = self
1553 .steps
1554 .iter()
1555 .filter_map(|s| match s {
1556 InstallStep::StageResource { name, dir } => Some((name.clone(), dir.clone())),
1557 _ => None,
1558 })
1559 .collect();
1560 for (name, dir) in stages {
1561 if let Some(resource) = self.resources.iter_mut().find(|r| r.name == name) {
1562 resource.stage_to = dir;
1563 }
1564 }
1565 let mut steps: Vec<InstallStep> = (0..self.patches.len())
1566 .map(|index| InstallStep::ApplyPatch { index })
1567 .collect();
1568 steps.append(&mut self.steps);
1569 InstallPlan {
1570 steps,
1571 resources: self.resources,
1572 patches: self.patches,
1573 env: self.env,
1574 deparallelize: self.deparallelize,
1575 }
1576 }
1577}
1578
1579fn parse_resource_open(line: &str) -> Option<String> {
1581 let rest = line.strip_prefix("resource ")?;
1582 let body = rest.strip_suffix(" do")?;
1583 quoted_literal(body)
1584}
1585
1586fn classify_system(argv: Vec<String>) -> InstallStep {
1588 let rest = || argv[1..].to_vec();
1589 match argv[0].as_str() {
1590 "./configure" => InstallStep::Configure { args: rest() },
1591 "make" => InstallStep::Make { args: rest() },
1592 "cmake" => InstallStep::CMake { args: rest() },
1593 "./bootstrap" => InstallStep::Bootstrap { args: rest() },
1594 "cargo" => InstallStep::Cargo { args: rest() },
1595 "go" => InstallStep::Go { args: rest() },
1596 "meson" | "ninja" => InstallStep::Meson { args: argv },
1597 _ => InstallStep::System { argv },
1598 }
1599}
1600
1601fn make_install_step(recv: &str, tool: &str, from: String, rename: Option<String>) -> InstallStep {
1603 let under = |dir: &str, rename: Option<String>| {
1604 Some(rename.map_or_else(|| dir.to_string(), |r| format!("{dir}/{r}")))
1605 };
1606 match recv {
1607 "bin" => InstallStep::BinInstall { from, to: rename },
1608 "lib" => InstallStep::LibInstall { from, to: rename },
1609 "include" => InstallStep::IncludeInstall { from, to: rename },
1610 "libexec" => InstallStep::PrefixInstall {
1611 from,
1612 to: under("libexec", rename),
1613 },
1614 "share" => InstallStep::PrefixInstall {
1615 from,
1616 to: under("share", rename),
1617 },
1618 "man1" => InstallStep::PrefixInstall {
1619 from,
1620 to: under("share/man/man1", rename),
1621 },
1622 "pkgshare" => InstallStep::PrefixInstall {
1623 from,
1624 to: under(&format!("share/{tool}"), rename),
1625 },
1626 _ => InstallStep::PrefixInstall { from, to: rename },
1627 }
1628}
1629
1630fn normalize_stage_dir(dir: &str) -> Option<String> {
1633 let trimmed = dir.strip_prefix("./").unwrap_or(dir);
1634 if trimmed.is_empty() || trimmed == "." {
1635 None
1636 } else {
1637 Some(trimmed.to_string())
1638 }
1639}
1640
1641fn full_quoted_body(expr: &str, quote: char) -> Option<&str> {
1646 if expr.len() < 2 || !expr.starts_with(quote) {
1647 return None;
1648 }
1649 let body = &expr[1..];
1650 let bytes = body.as_bytes();
1651 let mut escaped = false;
1652 let mut in_interp = false;
1653 for (i, c) in body.char_indices() {
1654 if escaped {
1655 escaped = false;
1656 continue;
1657 }
1658 if in_interp {
1659 if c == '}' {
1660 in_interp = false;
1661 }
1662 continue;
1663 }
1664 if c == '\\' {
1665 escaped = true;
1666 } else if quote == '"' && c == '#' && bytes.get(i + 1) == Some(&b'{') {
1667 in_interp = true;
1668 } else if c == quote {
1669 return (i == body.len() - 1).then(|| &body[..i]);
1670 }
1671 }
1672 None
1673}
1674
1675fn unescape_single(body: &str) -> String {
1677 let mut out = String::new();
1678 let mut chars = body.chars();
1679 while let Some(c) = chars.next() {
1680 if c == '\\' {
1681 match chars.next() {
1682 Some(next @ ('\'' | '\\')) => out.push(next),
1683 Some(next) => {
1684 out.push('\\');
1685 out.push(next);
1686 }
1687 None => out.push('\\'),
1688 }
1689 } else {
1690 out.push(c);
1691 }
1692 }
1693 out
1694}
1695
1696fn split_top_level_slash(expr: &str) -> Vec<&str> {
1699 let mut out = Vec::new();
1700 let mut in_double = false;
1701 let mut in_single = false;
1702 let mut escaped = false;
1703 let mut depth = 0i32;
1704 let mut start = 0;
1705 for (i, c) in expr.char_indices() {
1706 if escaped {
1707 escaped = false;
1708 continue;
1709 }
1710 match c {
1711 '\\' if in_double || in_single => escaped = true,
1712 '"' if !in_single => in_double = !in_double,
1713 '\'' if !in_double => in_single = !in_single,
1714 '(' | '[' | '{' if !in_double && !in_single => depth += 1,
1715 ')' | ']' | '}' if !in_double && !in_single => depth -= 1,
1716 '/' if !in_double && !in_single && depth == 0 => {
1717 out.push(&expr[start..i]);
1718 start = i + 1;
1719 }
1720 _ => {}
1721 }
1722 }
1723 out.push(&expr[start..]);
1724 out
1725}
1726
1727#[cfg(test)]
1728mod tests {
1729 use super::*;
1730
1731 fn ctx(tool: &str, mac: bool, arm: bool) -> RecipeCtx {
1733 RecipeCtx {
1734 tool: tool.to_string(),
1735 version: "1.0.0".to_string(),
1736 prefix: PathBuf::from(format!("/tc/{tool}")),
1737 dep_prefixes: HashMap::new(),
1738 target_macos: mac,
1739 target_arm: arm,
1740 }
1741 }
1742
1743 fn plan(rb: &str, ctx: &RecipeCtx) -> InstallPlan {
1744 parse_install_plan(rb, ctx).expect("plan should parse")
1745 }
1746
1747 #[test]
1749 fn autotools_std_configure_args_exact_plan() {
1750 let rb = r#"
1751class Foo < Formula
1752 url "https://example.com/foo-1.0.0.tar.gz"
1753 sha256 "0000000000000000000000000000000000000000000000000000000000000000"
1754
1755 def install
1756 system "./configure", *std_configure_args
1757 system "make"
1758 system "make", "install"
1759 end
1760end
1761"#;
1762 let p = plan(rb, &ctx("foo", true, true));
1763 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
1764 assert_eq!(
1765 p.steps,
1766 vec![
1767 InstallStep::Configure {
1768 args: vec![
1769 "--prefix=/tc/foo".into(),
1770 "--libdir=/tc/foo/lib".into(),
1771 "--disable-debug".into(),
1772 "--disable-dependency-tracking".into(),
1773 "--disable-silent-rules".into(),
1774 ]
1775 },
1776 InstallStep::Make { args: vec![] },
1777 InstallStep::Make {
1778 args: vec!["install".into()]
1779 },
1780 ]
1781 );
1782 assert!(p.resources.is_empty() && p.patches.is_empty() && p.env.is_empty());
1783 assert!(!p.deparallelize);
1784 }
1785
1786 #[test]
1788 fn make_install_big_args_resolves_prefix() {
1789 let rb = r#"
1790class Foo < Formula
1791 def install
1792 system "make", "install", "prefix=#{prefix}", "sysconfdir=#{etc}", "CC=#{ENV.cc}", "CXX=#{ENV.cxx}"
1793 end
1794end
1795"#;
1796 let p = plan(rb, &ctx("foo", true, true));
1797 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
1798 assert_eq!(
1799 p.steps,
1800 vec![InstallStep::Make {
1801 args: vec![
1802 "install".into(),
1803 "prefix=/tc/foo".into(),
1804 "sysconfdir=/tc/foo/etc".into(),
1805 "CC=cc".into(),
1806 "CXX=c++".into(),
1807 ]
1808 }]
1809 );
1810 }
1811
1812 #[test]
1814 fn cargo_std_args_expand() {
1815 let rb = r#"
1816class Foo < Formula
1817 def install
1818 system "cargo", "install", *std_cargo_args
1819 end
1820end
1821"#;
1822 let p = plan(rb, &ctx("foo", true, true));
1823 assert_eq!(
1824 p.steps,
1825 vec![InstallStep::Cargo {
1826 args: vec![
1827 "install".into(),
1828 "--locked".into(),
1829 "--root".into(),
1830 "/tc/foo".into(),
1831 "--path".into(),
1832 ".".into(),
1833 ]
1834 }]
1835 );
1836 }
1837
1838 #[test]
1840 fn cmake_std_args_expand_per_invocation() {
1841 let rb = r#"
1842class Foo < Formula
1843 def install
1844 system "cmake", "-S", ".", "-B", "build", *std_cmake_args
1845 system "cmake", "--build", "build"
1846 system "cmake", "--install", "build"
1847 end
1848end
1849"#;
1850 let p = plan(rb, &ctx("foo", true, true));
1851 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
1852 assert_eq!(
1853 p.steps,
1854 vec![
1855 InstallStep::CMake {
1856 args: vec![
1857 "-S".into(),
1858 ".".into(),
1859 "-B".into(),
1860 "build".into(),
1861 "-DCMAKE_INSTALL_PREFIX=/tc/foo".into(),
1862 "-DCMAKE_BUILD_TYPE=Release".into(),
1863 "-DCMAKE_FIND_FRAMEWORK=LAST".into(),
1864 "-DBUILD_TESTING=OFF".into(),
1865 ]
1866 },
1867 InstallStep::CMake {
1868 args: vec!["--build".into(), "build".into()]
1869 },
1870 InstallStep::CMake {
1871 args: vec!["--install".into(), "build".into()]
1872 },
1873 ]
1874 );
1875 }
1876
1877 #[test]
1879 fn on_macos_on_linux_blocks_fold() {
1880 let rb = r#"
1881class Foo < Formula
1882 def install
1883 on_macos do
1884 ENV["MAC"] = "1"
1885 end
1886 on_linux do
1887 ENV["LINUX"] = "1"
1888 end
1889 system "make"
1890 end
1891end
1892"#;
1893 let mac = plan(rb, &ctx("foo", true, true));
1894 assert!(mac.is_fully_supported());
1895 assert_eq!(mac.env.get("MAC"), Some(&"1".to_string()));
1896 assert!(!mac.env.contains_key("LINUX"));
1897
1898 let linux = plan(rb, &ctx("foo", false, false));
1899 assert_eq!(linux.env.get("LINUX"), Some(&"1".to_string()));
1900 assert!(!linux.env.contains_key("MAC"));
1901 }
1902
1903 #[test]
1905 fn hardware_cpu_arm_if_else_folds_both_ways() {
1906 let rb = r#"
1907class Foo < Formula
1908 def install
1909 if Hardware::CPU.arm?
1910 ENV["ARCH"] = "arm64"
1911 else
1912 ENV["ARCH"] = "x86_64"
1913 end
1914 system "make"
1915 end
1916end
1917"#;
1918 let arm = plan(rb, &ctx("foo", true, true));
1919 assert!(arm.is_fully_supported());
1920 assert_eq!(arm.env.get("ARCH"), Some(&"arm64".to_string()));
1921
1922 let intel = plan(rb, &ctx("foo", true, false));
1923 assert!(intel.is_fully_supported());
1924 assert_eq!(intel.env.get("ARCH"), Some(&"x86_64".to_string()));
1925 }
1926
1927 #[test]
1930 fn resource_block_and_stage_dir() {
1931 let rb = r#"
1932class Foo < Formula
1933 resource "vendor" do
1934 url "https://example.com/vendor-2.0.tar.gz"
1935 sha256 "1111111111111111111111111111111111111111111111111111111111111111"
1936 end
1937
1938 def install
1939 resource("vendor").stage(buildpath/"vendor")
1940 system "make"
1941 end
1942end
1943"#;
1944 let p = plan(rb, &ctx("foo", true, true));
1945 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
1946 assert_eq!(
1947 p.resources,
1948 vec![ResourceSpec {
1949 name: "vendor".into(),
1950 url: "https://example.com/vendor-2.0.tar.gz".into(),
1951 sha256: "1111111111111111111111111111111111111111111111111111111111111111".into(),
1952 stage_to: Some("vendor".into()),
1953 }]
1954 );
1955 assert_eq!(
1956 p.steps[0],
1957 InstallStep::StageResource {
1958 name: "vendor".into(),
1959 dir: Some("vendor".into())
1960 }
1961 );
1962 }
1963
1964 #[test]
1967 fn patch_blocks_and_patch_data() {
1968 let rb = r#"
1969class Foo < Formula
1970 patch do
1971 url "https://example.com/fix.patch"
1972 sha256 "2222222222222222222222222222222222222222222222222222222222222222"
1973 end
1974
1975 patch :p0 do
1976 url "https://example.com/fix0.patch"
1977 sha256 "3333333333333333333333333333333333333333333333333333333333333333"
1978 end
1979
1980 patch :DATA
1981
1982 def install
1983 system "make"
1984 end
1985end
1986"#;
1987 let p = plan(rb, &ctx("foo", true, true));
1988 assert_eq!(
1989 p.patches,
1990 vec![
1991 PatchSpec {
1992 url: "https://example.com/fix.patch".into(),
1993 sha256: "2222222222222222222222222222222222222222222222222222222222222222"
1994 .into(),
1995 strip: 1
1996 },
1997 PatchSpec {
1998 url: "https://example.com/fix0.patch".into(),
1999 sha256: "3333333333333333333333333333333333333333333333333333333333333333"
2000 .into(),
2001 strip: 0
2002 },
2003 ]
2004 );
2005 assert_eq!(p.steps[0], InstallStep::ApplyPatch { index: 0 });
2006 assert_eq!(p.steps[1], InstallStep::ApplyPatch { index: 1 });
2007 assert!(!p.is_fully_supported());
2008 assert_eq!(p.unsupported_constructs(), vec!["patch :DATA"]);
2009 }
2010
2011 #[test]
2014 fn unsupported_constructs_surface_with_text() {
2015 let rb = r##"
2016class Foo < Formula
2017 def install
2018 %w[a b].each do |f|
2019 bin.install f
2020 end
2021 system "make", *args
2022 ENV["X"] = which("python3")
2023 system "install", "-m", "0755", "#{testpath}/x", bin
2024 cd "sub" do
2025 system "make"
2026 end
2027 end
2028end
2029"##;
2030 let p = plan(rb, &ctx("foo", true, true));
2031 assert!(!p.is_fully_supported());
2032 let constructs = p.unsupported_constructs();
2033 assert_eq!(constructs.len(), 5, "{constructs:?}");
2034 assert!(constructs[0].starts_with("%w[a b].each do"));
2035 assert_eq!(constructs[1], "system \"make\", *args");
2036 assert_eq!(constructs[2], "ENV[\"X\"] = which(\"python3\")");
2037 assert!(constructs[3].starts_with("system \"install\""));
2038 assert!(constructs[4].starts_with("cd \"sub\" do"));
2039 assert!(!p
2041 .steps
2042 .iter()
2043 .any(|s| matches!(s, InstallStep::BinInstall { .. })));
2044 }
2045
2046 #[test]
2048 fn install_receivers_rename_and_subdirs() {
2049 let rb = r#"
2050class Foo < Formula
2051 def install
2052 bin.install "foo.sh" => "foo"
2053 bin.install "a", "b"
2054 lib.install "libfoo.a"
2055 include.install "foo.h" => "foo/foo.h"
2056 libexec.install "helper" => "h2"
2057 share.install "data.dat"
2058 man1.install "foo.1"
2059 pkgshare.install "examples"
2060 prefix.install "README"
2061 end
2062end
2063"#;
2064 let p = plan(rb, &ctx("foo", true, true));
2065 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
2066 assert_eq!(
2067 p.steps,
2068 vec![
2069 InstallStep::BinInstall {
2070 from: "foo.sh".into(),
2071 to: Some("foo".into())
2072 },
2073 InstallStep::BinInstall {
2074 from: "a".into(),
2075 to: None
2076 },
2077 InstallStep::BinInstall {
2078 from: "b".into(),
2079 to: None
2080 },
2081 InstallStep::LibInstall {
2082 from: "libfoo.a".into(),
2083 to: None
2084 },
2085 InstallStep::IncludeInstall {
2086 from: "foo.h".into(),
2087 to: Some("foo/foo.h".into())
2088 },
2089 InstallStep::PrefixInstall {
2090 from: "helper".into(),
2091 to: Some("libexec/h2".into())
2092 },
2093 InstallStep::PrefixInstall {
2094 from: "data.dat".into(),
2095 to: Some("share".into())
2096 },
2097 InstallStep::PrefixInstall {
2098 from: "foo.1".into(),
2099 to: Some("share/man/man1".into())
2100 },
2101 InstallStep::PrefixInstall {
2102 from: "examples".into(),
2103 to: Some("share/foo".into())
2104 },
2105 InstallStep::PrefixInstall {
2106 from: "README".into(),
2107 to: None
2108 },
2109 ]
2110 );
2111 }
2112
2113 #[test]
2115 fn env_set_append_prepend_deparallelize() {
2116 let rb = r#"
2117class Foo < Formula
2118 def install
2119 ENV["CFLAGS"] = "-O2"
2120 ENV.append "CFLAGS", "-fPIC"
2121 ENV.prepend "LDFLAGS", "-L#{lib}"
2122 ENV.deparallelize
2123 system "make"
2124 end
2125end
2126"#;
2127 let p = plan(rb, &ctx("foo", true, true));
2128 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
2129 assert!(p.deparallelize);
2130 assert_eq!(
2131 p.steps[..3],
2132 [
2133 InstallStep::EnvSet {
2134 key: "CFLAGS".into(),
2135 value: "-O2".into()
2136 },
2137 InstallStep::EnvSet {
2138 key: "CFLAGS".into(),
2139 value: "-O2 -fPIC".into()
2140 },
2141 InstallStep::EnvSet {
2142 key: "LDFLAGS".into(),
2143 value: "-L/tc/foo/lib".into()
2144 },
2145 ]
2146 );
2147 assert_eq!(p.env.get("CFLAGS"), Some(&"-O2 -fPIC".to_string()));
2148 assert_eq!(p.env.get("LDFLAGS"), Some(&"-L/tc/foo/lib".to_string()));
2149 }
2150
2151 #[test]
2153 fn inreplace_literal_and_block_forms() {
2154 let rb = r##"
2155class Foo < Formula
2156 def install
2157 inreplace "Makefile", "@PREFIX@", "#{prefix}"
2158 inreplace "config" do |s|
2159 s.gsub! "a", "b"
2160 end
2161 end
2162end
2163"##;
2164 let p = plan(rb, &ctx("foo", true, true));
2165 assert_eq!(
2166 p.steps[0],
2167 InstallStep::Inreplace {
2168 file: "Makefile".into(),
2169 from: "@PREFIX@".into(),
2170 to: "/tc/foo".into()
2171 }
2172 );
2173 assert_eq!(p.unsupported_constructs().len(), 1);
2174 assert!(p.unsupported_constructs()[0].starts_with("inreplace \"config\" do"));
2175 }
2176
2177 #[test]
2180 fn formula_dep_accessors_resolve_or_unsupported() {
2181 let rb = r#"
2182class Foo < Formula
2183 def install
2184 system "./configure", "--with-ssl=#{Formula["openssl@3"].opt_prefix}", "--with-zlib=#{Formula["missing"].opt_lib}"
2185 end
2186end
2187"#;
2188 let mut c = ctx("foo", true, true);
2189 c.dep_prefixes
2190 .insert("openssl@3".into(), PathBuf::from("/tc/openssl@3"));
2191 let p = plan(rb, &c);
2192 assert!(!p.is_fully_supported());
2194
2195 let rb_ok = r#"
2196class Foo < Formula
2197 def install
2198 system "./configure", "--with-ssl=#{Formula["openssl@3"].opt_prefix}", "--sbin=#{Formula["openssl@3"].opt_bin}"
2199 end
2200end
2201"#;
2202 let p = plan(rb_ok, &c);
2203 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
2204 assert_eq!(
2205 p.steps,
2206 vec![InstallStep::Configure {
2207 args: vec![
2208 "--with-ssl=/tc/openssl@3".into(),
2209 "--sbin=/tc/openssl@3/bin".into(),
2210 ]
2211 }]
2212 );
2213 }
2214
2215 #[test]
2217 fn go_std_args_kwargs() {
2218 let rb = r#"
2219class Foo < Formula
2220 def install
2221 system "go", "build", *std_go_args(ldflags: "-s -w")
2222 system "go", "build", *std_go_args(output: bin/"gadget")
2223 system "go", "build", *std_go_args(wat: "x")
2224 end
2225end
2226"#;
2227 let p = plan(rb, &ctx("foo", true, true));
2228 assert_eq!(
2229 p.steps[0],
2230 InstallStep::Go {
2231 args: vec![
2232 "build".into(),
2233 "-trimpath".into(),
2234 "-o=/tc/foo/bin/foo".into(),
2235 "-ldflags=-s -w".into(),
2236 ]
2237 }
2238 );
2239 assert_eq!(
2240 p.steps[1],
2241 InstallStep::Go {
2242 args: vec![
2243 "build".into(),
2244 "-trimpath".into(),
2245 "-o=/tc/foo/bin/gadget".into(),
2246 ]
2247 }
2248 );
2249 assert_eq!(p.unsupported_constructs().len(), 1);
2250 }
2251
2252 #[test]
2255 fn meson_ninja_and_generic_system() {
2256 let rb = r##"
2257class Foo < Formula
2258 def install
2259 system "meson", "setup", "build"
2260 system "ninja", "-C", "build", "install"
2261 system "install_name_tool", "-id", "#{lib}/libfoo.dylib", "libfoo.dylib"
2262 end
2263end
2264"##;
2265 let p = plan(rb, &ctx("foo", true, true));
2266 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
2267 assert_eq!(
2268 p.steps,
2269 vec![
2270 InstallStep::Meson {
2271 args: vec!["meson".into(), "setup".into(), "build".into()]
2272 },
2273 InstallStep::Meson {
2274 args: vec![
2275 "ninja".into(),
2276 "-C".into(),
2277 "build".into(),
2278 "install".into()
2279 ]
2280 },
2281 InstallStep::System {
2282 argv: vec![
2283 "install_name_tool".into(),
2284 "-id".into(),
2285 "/tc/foo/lib/libfoo.dylib".into(),
2286 "libfoo.dylib".into(),
2287 ]
2288 },
2289 ]
2290 );
2291 }
2292
2293 #[tokio::test]
2295 async fn fetch_recipe_without_source_path_errors() {
2296 let formula = FormulaData::default();
2297 let err = fetch_recipe(&formula).await.unwrap_err();
2298 assert!(matches!(err, ToolchainError::RegistryError { .. }));
2299 assert!(err.to_string().contains("ruby_source_path"));
2300 }
2301
2302 #[tokio::test]
2304 async fn fetch_recipe_cached_reads_cache_file() {
2305 let tmp = tempfile::tempdir().unwrap();
2306 let cache = tmp.path().join(".build/recipe.rb");
2307 tokio::fs::create_dir_all(cache.parent().unwrap())
2308 .await
2309 .unwrap();
2310 tokio::fs::write(&cache, "class Foo < Formula\nend\n")
2311 .await
2312 .unwrap();
2313 let text = fetch_recipe_cached(&FormulaData::default(), &cache)
2316 .await
2317 .unwrap();
2318 assert_eq!(text, "class Foo < Formula\nend\n");
2319 }
2320
2321 #[test]
2323 fn missing_install_block_errors() {
2324 let rb = "class Foo < Formula\n url \"x\"\nend\n";
2325 let err = parse_install_plan(rb, &ctx("foo", true, true)).unwrap_err();
2326 assert!(err.to_string().contains("def install"));
2327 }
2328
2329 #[test]
2336 fn real_jq_formula_is_fully_supported() {
2337 let p = plan(JQ_RB, &ctx("jq", true, true));
2338 assert!(p.is_fully_supported(), "{:?}", p.unsupported_constructs());
2339 assert_eq!(
2340 p.steps,
2341 vec![
2342 InstallStep::Configure {
2343 args: vec![
2344 "--prefix=/tc/jq".into(),
2345 "--libdir=/tc/jq/lib".into(),
2346 "--disable-debug".into(),
2347 "--disable-dependency-tracking".into(),
2348 "--disable-silent-rules".into(),
2349 "--disable-silent-rules".into(),
2350 "--disable-maintainer-mode".into(),
2351 ]
2352 },
2353 InstallStep::Make {
2354 args: vec!["install".into()]
2355 },
2356 ]
2357 );
2358 assert!(p.resources.is_empty() && p.patches.is_empty());
2359 }
2360
2361 #[test]
2366 fn real_git_formula_locks_unsupported_set() {
2367 let mut c = ctx("git", true, true);
2368 c.dep_prefixes
2369 .insert("pcre2".into(), PathBuf::from("/tc/pcre2"));
2370 let p = plan(GIT_RB, &c);
2371 assert!(!p.is_fully_supported());
2372
2373 let names: Vec<&str> = p.resources.iter().map(|r| r.name.as_str()).collect();
2375 assert_eq!(names, vec!["Authen::SASL", "html", "man", "Net::SMTP::SSL"]);
2376
2377 for (key, value) in [
2379 ("NO_FINK", "1"),
2380 ("NO_DARWIN_PORTS", "1"),
2381 ("USE_LIBPCRE2", "1"),
2382 ("INSTALL_SYMLINKS", "1"),
2383 ("LIBPCREDIR", "/tc/pcre2"),
2384 ("V", "1"),
2385 ] {
2386 assert_eq!(p.env.get(key), Some(&value.to_string()), "env {key}");
2387 }
2388 assert_eq!(p.env.len(), 6);
2389
2390 assert!(p.steps.iter().all(|s| matches!(
2392 s,
2393 InstallStep::EnvSet { .. } | InstallStep::Unsupported { .. }
2394 )));
2395
2396 let constructs = p.unsupported_constructs();
2399 let expected_markers = [
2400 "odie ",
2401 "ENV[\"PYTHON_PATH\"] = which(\"python3\")",
2402 "ENV[\"PERL_PATH\"] = which(\"perl\")",
2403 "perl_version = ",
2404 "ENV[\"PERLLIB_EXTRA\"] = %W[",
2405 "args = %W[",
2406 "args += if OS.mac?",
2407 "inreplace \"Makefile\"",
2408 "system \"make\", \"install\", *args",
2409 "git_core = ",
2410 "rm ",
2411 "cd \"contrib/",
2412 "bash_completion.install ",
2413 "zsh_completion.install ",
2414 "cp \"#{bash_completion}/",
2415 "(share/\"git-core\").install ",
2416 "man.install ",
2417 "(share/\"doc/git-doc\").install ",
2418 "chmod ",
2419 "resource(\"Net::SMTP::SSL\").stage do",
2420 "resource(\"Authen::SASL\").stage do",
2421 "perl_dir = ",
2422 "rm_r ",
2423 "(buildpath/\"gitconfig\").write ",
2424 "etc.install ",
2425 ];
2426 for construct in &constructs {
2427 assert!(
2428 expected_markers.iter().any(|m| construct.starts_with(m)),
2429 "unexpected unsupported construct: {construct}"
2430 );
2431 }
2432 assert_eq!(constructs.len(), 33, "{constructs:#?}");
2433 }
2434
2435 const JQ_RB: &str = r##"class Jq < Formula
2437 desc "Lightweight and flexible command-line JSON processor"
2438 homepage "https://jqlang.github.io/jq/"
2439 url "https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-1.8.2.tar.gz"
2440 sha256 "71b8d6e8f5fe81f6c6d0d110e3892251f6ce76ed095abd315e26e6e1193af3af"
2441 license "MIT"
2442 compatibility_version 1
2443
2444 livecheck do
2445 url :stable
2446 regex(/^(?:jq[._-])?v?(\d+(?:\.\d+)+)$/i)
2447 end
2448
2449 bottle do
2450 sha256 cellar: :any, arm64_tahoe: "f6ec821177b4c55fefb4c58f8e6f5d8386a5fce0a3c5d8811c4d6a0cc818dead"
2451 sha256 cellar: :any, arm64_sequoia: "29c079abc7e165583b7fc4d3feba7a21a12160a414a7d4202731f7ffa2e349de"
2452 sha256 cellar: :any, arm64_sonoma: "c5710de7fe77793fda8d4c550e5450b9fd77f9caf4a90c01be3a0969a8c078e8"
2453 sha256 cellar: :any, sonoma: "0a471afde49b03dd7a8a1100c2581dc35ae49b3877a9c37fa219aeccdb998154"
2454 sha256 cellar: :any, arm64_linux: "30137e8a68ffb92bc8e16494f1509f0c08b404cae8a0842e15b63f46e4baf028"
2455 sha256 cellar: :any, x86_64_linux: "c3e26f0bff91db3e6606c3fc23ebe5dc2b1e9854252e6e4a3e03b17f7a3cde4e"
2456 end
2457
2458 head do
2459 url "https://github.com/jqlang/jq.git", branch: "master"
2460
2461 depends_on "autoconf" => :build
2462 depends_on "automake" => :build
2463 depends_on "libtool" => :build
2464 end
2465
2466 depends_on "oniguruma"
2467
2468 def install
2469 system "autoreconf", "--force", "--install", "--verbose" if build.head?
2470 system "./configure", *std_configure_args,
2471 "--disable-silent-rules",
2472 "--disable-maintainer-mode"
2473 system "make", "install"
2474 end
2475
2476 test do
2477 assert_equal "2\n", pipe_output("#{bin}/jq .bar", '{"foo":1, "bar":2}')
2478 end
2479end
2480"##;
2481
2482 const GIT_RB: &str = r##"class Git < Formula
2483 desc "Distributed revision control system"
2484 homepage "https://git-scm.com"
2485 url "https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.55.0.tar.xz"
2486 sha256 "457fdb04dc8728e007d4688695e6912e6f680727920f2a40bf11eacc17505357"
2487 license all_of: [
2488 "GPL-2.0-only",
2489 "GPL-2.0-or-later", # imap-send.c; trace.c; ...
2490 "LGPL-2.1-or-later", # xdiff/
2491 "BSD-3-Clause", # xdiff/xhistogram.c; reftable/
2492 "MIT", # khash.h; sha1dc/
2493 ]
2494 compatibility_version 1
2495 head "https://github.com/git/git.git", branch: "master"
2496
2497 livecheck do
2498 url "https://mirrors.edge.kernel.org/pub/software/scm/git/"
2499 regex(/href=.*?git[._-]v?(\d+(?:\.\d+)+)\.t/i)
2500 end
2501
2502 bottle do
2503 sha256 arm64_tahoe: "8cae58826d317457128023d07c07f00ac00aaf3019356ff72a837e804f5d0306"
2504 sha256 arm64_sequoia: "5df08160c5e705ffcce25edfe1bb189d50fd3366a0e451fc254a8d70904adc5f"
2505 sha256 arm64_sonoma: "3d7a66500cc7bf1530d7a5e862f4b4c2e317ed53c95bf76c012ce8ff7ec92ae7"
2506 sha256 sonoma: "f7477c65f53669d1f9fd90ad2d633e6c009d5f6b0e10e039c15ebc022ac18bde"
2507 sha256 arm64_linux: "ea73cb7a909a7c80c2c5474b15b53659dd04b1222efd678e9a693bb06a7ed314"
2508 sha256 x86_64_linux: "01365c33768a2cb170a34f8f201ea3f3a7bb8c0d2140640ff3fbc58584e9758e"
2509 end
2510
2511 depends_on "gettext" => :build
2512 depends_on "pkgconf" => :build
2513 depends_on "pcre2"
2514
2515 uses_from_macos "curl"
2516 uses_from_macos "expat"
2517
2518 # Don't try to add a libiconv dependency without reading this PR first:
2519 # https://github.com/Homebrew/homebrew-core/pull/258461
2520
2521 on_macos do
2522 depends_on "gettext"
2523 end
2524
2525 on_linux do
2526 depends_on "openssl@3" # for git-imap-send (GPL-2.0-or-later), uses CommonCrypto on macOS
2527 depends_on "zlib-ng-compat"
2528 end
2529
2530 resource "Authen::SASL" do
2531 url "https://cpan.metacpan.org/authors/id/E/EH/EHUELS/Authen-SASL-2.2000.tar.gz"
2532 sha256 "8cdf5a7f185448b614471675dae5b26f8c6e330b62264c3ff5d91172d6889b99"
2533 end
2534
2535 resource "html" do
2536 url "https://mirrors.edge.kernel.org/pub/software/scm/git/git-htmldocs-2.55.0.tar.xz"
2537 sha256 "d1142c4e28b469d297d6df6519653e92a76c952f55202fde17a72a3b03d49437"
2538
2539 livecheck do
2540 formula :parent
2541 end
2542 end
2543
2544 resource "man" do
2545 url "https://mirrors.edge.kernel.org/pub/software/scm/git/git-manpages-2.55.0.tar.xz"
2546 sha256 "a32d432f80df46a14a05d1104c72d5a13fe27e9feba9aa0f017e54131db6b982"
2547
2548 livecheck do
2549 formula :parent
2550 end
2551 end
2552
2553 resource "Net::SMTP::SSL" do
2554 url "https://cpan.metacpan.org/authors/id/R/RJ/RJBS/Net-SMTP-SSL-1.04.tar.gz"
2555 sha256 "7b29c45add19d3d5084b751f7ba89a8e40479a446ce21cfd9cc741e558332a00"
2556 end
2557
2558 deny_network_access! [:build, :postinstall]
2559
2560 def install
2561 odie "html resource needs to be updated" if build.stable? && version != resource("html").version
2562 odie "man resource needs to be updated" if build.stable? && version != resource("man").version
2563
2564 # If these things are installed, tell Git build system not to use them
2565 ENV["NO_FINK"] = "1"
2566 ENV["NO_DARWIN_PORTS"] = "1"
2567 ENV["PYTHON_PATH"] = which("python3")
2568 ENV["PERL_PATH"] = which("perl")
2569 ENV["USE_LIBPCRE2"] = "1"
2570 ENV["INSTALL_SYMLINKS"] = "1"
2571 ENV["LIBPCREDIR"] = formula_opt_prefix("pcre2")
2572 ENV["V"] = "1" # build verbosely
2573
2574 perl_version = Utils.safe_popen_read("perl", "--version")[/v(\d+\.\d+)(?:\.\d+)?/, 1]
2575
2576 if OS.mac?
2577 ENV["PERLLIB_EXTRA"] = %W[
2578 #{MacOS.active_developer_dir}
2579 /Library/Developer/CommandLineTools
2580 /Applications/Xcode.app/Contents/Developer
2581 ].uniq.map do |p|
2582 "#{p}/Library/Perl/#{perl_version}/darwin-thread-multi-2level"
2583 end.join(":")
2584 end
2585
2586 # The git-gui and gitk tools are installed by a separate formula (git-gui)
2587 # to avoid a dependency on tcl-tk and to avoid using the broken system
2588 # tcl-tk (see https://github.com/Homebrew/homebrew-core/issues/36390)
2589 # This is done by setting the NO_TCLTK make variable.
2590 args = %W[
2591 prefix=#{prefix}
2592 sysconfdir=#{etc}
2593 CC=#{ENV.cc}
2594 CFLAGS=#{ENV.cflags}
2595 LDFLAGS=#{ENV.ldflags}
2596 NO_TCLTK=1
2597 NO_RUST=1
2598 ]
2599
2600 args += if OS.mac?
2601 %w[NO_OPENSSL=1 APPLE_COMMON_CRYPTO=1]
2602 else
2603 openssl_prefix = formula_opt_prefix("openssl@3")
2604
2605 %W[NO_APPLE_COMMON_CRYPTO=1 OPENSSLDIR=#{openssl_prefix}]
2606 end
2607
2608 # Make sure `git` looks in `opt_prefix` instead of the Cellar.
2609 # Otherwise, Cellar references propagate to generated plists from `git maintenance`.
2610 inreplace "Makefile", /(-DFALLBACK_RUNTIME_PREFIX=")[^"]+/, "\\1#{opt_prefix}"
2611
2612 system "make", "install", *args
2613
2614 git_core = libexec/"git-core"
2615 rm git_core/"git-svn"
2616
2617 # Install the macOS keychain credential helper
2618 if OS.mac?
2619 cd "contrib/credential/osxkeychain" do
2620 system "make", *args
2621 git_core.install "git-credential-osxkeychain"
2622 system "make", "clean"
2623 end
2624 end
2625
2626 # Generate diff-highlight perl script executable
2627 cd "contrib/diff-highlight" do
2628 system "make"
2629 end
2630
2631 # Install the netrc credential helper
2632 cd "contrib/credential/netrc" do
2633 system "make", "test"
2634 git_core.install "git-credential-netrc"
2635 end
2636
2637 # Install git-subtree
2638 cd "contrib/subtree" do
2639 system "make", "CC=#{ENV.cc}",
2640 "CFLAGS=#{ENV.cflags}",
2641 "LDFLAGS=#{ENV.ldflags}"
2642 git_core.install "git-subtree"
2643 end
2644
2645 # Install git-jump
2646 cd "contrib/git-jump" do
2647 git_core.install "git-jump"
2648 end
2649
2650 # install the completion script first because it is inside "contrib"
2651 bash_completion.install "contrib/completion/git-completion.bash"
2652 bash_completion.install "contrib/completion/git-prompt.sh"
2653 zsh_completion.install "contrib/completion/git-completion.zsh" => "_git"
2654 cp "#{bash_completion}/git-completion.bash", zsh_completion
2655 cp "#{bash_completion}/git-prompt.sh", zsh_completion
2656
2657 (share/"git-core").install "contrib"
2658
2659 # We could build the manpages ourselves, but the build process depends
2660 # on many other packages, and is somewhat crazy, this way is easier.
2661 man.install resource("man")
2662 (share/"doc/git-doc").install resource("html")
2663
2664 # Make html docs world-readable
2665 chmod 0644, Dir["#{share}/doc/git-doc/**/*.{html,txt}"]
2666 chmod 0755, Dir["#{share}/doc/git-doc/{RelNotes,howto,technical}"]
2667
2668 # git-send-email needs Net::SMTP::SSL or Net::SMTP >= 2.34 and Authen::SASL
2669 resource("Net::SMTP::SSL").stage do
2670 (share/"perl5").install "lib/Net"
2671 end
2672
2673 resource("Authen::SASL").stage do
2674 (share/"perl5").install "lib/Authen"
2675 end
2676
2677 # This is only created when building against system Perl, but it isn't
2678 # purged by Homebrew's post-install cleaner because that doesn't check
2679 # "Library" directories. It is however pointless to keep around as it
2680 # only contains the perllocal.pod installation file.
2681 perl_dir = prefix/"Library/Perl"
2682 rm_r perl_dir if perl_dir.exist?
2683
2684 # Set the macOS keychain credential helper by default
2685 # (as Apple's CLT's git also does this).
2686 if OS.mac?
2687 (buildpath/"gitconfig").write <<~EOS
2688 [credential]
2689 \thelper = osxkeychain
2690 EOS
2691 etc.install "gitconfig"
2692 end
2693 end
2694
2695 def caveats
2696 <<~EOS
2697 The Tcl/Tk GUIs (e.g. gitk, git-gui) are now in the `git-gui` formula.
2698 Subversion interoperability (git-svn) is now in the `git-svn` formula.
2699 EOS
2700 end
2701
2702 test do
2703 system bin/"git", "init"
2704 %w[haunted house].each { |f| touch testpath/f }
2705 system bin/"git", "add", "haunted", "house"
2706 system bin/"git", "config", "user.name", "'A U Thor'"
2707 system bin/"git", "config", "user.email", "author@example.com"
2708 system bin/"git", "commit", "-a", "-m", "Initial Commit"
2709 assert_equal "haunted\nhouse", shell_output("#{bin}/git ls-files").strip
2710
2711 # Check that our `inreplace` for the `Makefile` does not break.
2712 # If this assertion fails, please fix the `inreplace` instead of removing this test.
2713 # The failure of this test means that `git` will generate broken launchctl plist files.
2714 refute_match HOMEBREW_CELLAR.to_s, shell_output("#{bin}/git --exec-path")
2715
2716 return unless OS.mac?
2717
2718 # Check Net::SMTP or Net::SMTP::SSL works for git-send-email
2719 %w[foo bar].each { |f| touch testpath/f }
2720 system bin/"git", "add", "foo", "bar"
2721 system bin/"git", "commit", "-a", "-m", "Second Commit"
2722 assert_match "Authentication Required", pipe_output(
2723 "#{bin}/git send-email --from=test@example.com --to=dev@null.com " \
2724 "--smtp-server=smtp.gmail.com --smtp-server-port=587 " \
2725 "--smtp-encryption=tls --confirm=never HEAD^ 2>&1",
2726 )
2727 end
2728end
2729"##;
2730}