1use std::borrow::Borrow;
14use std::collections::{BTreeMap, BTreeSet};
15use std::str::FromStr;
16use std::sync::LazyLock;
17
18use regex::Regex;
19
20use crate::errors::NameError;
21use crate::model::app_manifest::{AppManifest, ManifestDeps, ManifestSource, ManifestUnreadable};
22use crate::model::names::{ApplicationName, DependencyName, DependencyVersion, MakeVariableName};
23pub use crate::model::snapshot::{VendoredDep as DepPin, VendoredDepSource as DepSource};
24
25const ASSIGNMENT_OPERATORS: [&str; 5] = ["::=", ":=", "?=", "+=", "="];
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum VariableValue {
29 Words(Vec<String>),
30 Unreadable(ManifestUnreadable),
31}
32
33#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct MakeVariables {
35 values: BTreeMap<MakeVariableName, VariableValue>,
36}
37
38impl MakeVariables {
39 #[must_use]
40 pub fn parse(text: &str) -> Self {
41 let mut variables = Self::default();
42 let mut in_define = false;
43 for line in logical_lines(text) {
44 let trimmed = line.trim();
45 if in_define {
46 in_define = trimmed != "endef";
47 continue;
48 }
49 if trimmed.starts_with("define ") {
50 in_define = true;
51 continue;
52 }
53 if line.starts_with('\t') {
54 continue;
55 }
56 if let Some(assignment) = Assignment::parse(trimmed) {
57 variables.apply(assignment);
58 }
59 }
60 variables
61 }
62
63 #[must_use]
64 pub fn get<Q>(&self, name: &Q) -> Option<&VariableValue>
65 where
66 MakeVariableName: Borrow<Q>,
67 Q: Ord + ?Sized,
68 {
69 self.values.get(name)
70 }
71
72 pub fn words<Q>(&self, name: &Q) -> Result<&[String], ManifestUnreadable>
73 where
74 MakeVariableName: Borrow<Q>,
75 Q: Ord + ?Sized,
76 {
77 match self.values.get(name) {
78 Some(VariableValue::Words(words)) => Ok(words),
79 Some(VariableValue::Unreadable(reason)) => Err(reason.clone()),
80 None => Err(ManifestUnreadable::Missing),
81 }
82 }
83
84 fn apply(&mut self, assignment: Assignment) {
85 let Assignment {
86 name,
87 operator,
88 value,
89 } = assignment;
90 let previous = self.values.get_mut(&name);
91 match (operator, previous) {
92 (Operator::IfUnset, Some(_)) => {}
93 (Operator::Append, Some(VariableValue::Words(words))) => match value {
94 VariableValue::Words(more) => words.extend(more),
95 unreadable @ VariableValue::Unreadable(_) => {
96 self.values.insert(name, unreadable);
97 }
98 },
99 (Operator::Append, Some(VariableValue::Unreadable(_))) => {}
100 _ => {
101 self.values.insert(name, value);
102 }
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108enum Operator {
109 Replace,
110 IfUnset,
111 Append,
112}
113
114struct Assignment {
115 name: MakeVariableName,
116 operator: Operator,
117 value: VariableValue,
118}
119
120impl Assignment {
121 fn parse(line: &str) -> Option<Self> {
122 let name_end = line
123 .find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-')))
124 .unwrap_or(line.len());
125 let name = MakeVariableName::from_str(&line[..name_end]).ok()?;
126 let after = line[name_end..].trim_start();
127 let operator_text = ASSIGNMENT_OPERATORS
128 .iter()
129 .find(|op| after.starts_with(**op))?;
130 let operator = match *operator_text {
131 "?=" => Operator::IfUnset,
132 "+=" => Operator::Append,
133 _ => Operator::Replace,
134 };
135 let value = read_value(&after[operator_text.len()..]);
136 Some(Self {
137 name,
138 operator,
139 value,
140 })
141 }
142}
143
144fn read_value(text: &str) -> VariableValue {
145 let words: Vec<String> = text.split_whitespace().map(str::to_owned).collect();
146 match words.iter().find_map(|w| first_reference(w)) {
147 Some(Ok(name)) => VariableValue::Unreadable(ManifestUnreadable::UnexpandedVariable(name)),
148 Some(Err(_)) => VariableValue::Unreadable(ManifestUnreadable::ParseFailed),
149 None => VariableValue::Words(words),
150 }
151}
152
153fn first_reference(word: &str) -> Option<Result<MakeVariableName, NameError>> {
154 let start = word.find("$(").or_else(|| word.find("${"))? + 2;
155 let rest = &word[start..];
156 let end = rest
157 .find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-')))
158 .unwrap_or(rest.len());
159 Some(MakeVariableName::from_str(&rest[..end]))
160}
161
162fn logical_lines(text: &str) -> Vec<String> {
163 let mut out = Vec::new();
164 let mut current = String::new();
165 for raw in text.lines() {
166 let raw = raw.trim_end_matches('\r');
167 let code = raw.split_once('#').map_or(raw, |(code, _)| code);
168 if let Some(continued) = code.strip_suffix('\\') {
169 current.push_str(continued);
170 current.push(' ');
171 continue;
172 }
173 current.push_str(code);
174 out.push(std::mem::take(&mut current));
175 }
176 if !current.is_empty() {
177 out.push(current);
178 }
179 out
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
186pub struct ErlangMkManifest {
187 pub project: Option<ApplicationName>,
188 pub requires: ManifestDeps,
189}
190
191impl ErlangMkManifest {
192 #[must_use]
193 pub fn parse(text: &str) -> Self {
194 let variables = MakeVariables::parse(text);
195 let project = variables
196 .words("PROJECT")
197 .ok()
198 .and_then(|words| words.first())
199 .and_then(|word| ApplicationName::from_str(word).ok());
200 Self {
201 project,
202 requires: read_requires(&variables),
203 }
204 }
205
206 #[must_use]
207 pub fn into_manifest(self, name: ApplicationName) -> AppManifest {
208 AppManifest {
209 name,
210 source: ManifestSource::ErlangMk,
211 requires: self.requires,
212 }
213 }
214}
215
216fn read_requires(variables: &MakeVariables) -> ManifestDeps {
217 let mut requires = BTreeSet::new();
218 for variable in ["DEPS", "LOCAL_DEPS"] {
219 let words = match variables.words(variable) {
220 Ok(words) => words,
221 Err(ManifestUnreadable::Missing) => continue,
222 Err(reason) => return ManifestDeps::Unreadable(reason),
223 };
224 for word in words {
225 match ApplicationName::from_str(word) {
226 Ok(app) => requires.insert(app),
227 Err(_) => return ManifestDeps::Unreadable(ManifestUnreadable::ParseFailed),
228 };
229 }
230 }
231 ManifestDeps::Listed(requires)
232}
233
234static DEP_RE: LazyLock<Regex> = LazyLock::new(|| {
235 Regex::new(r"^\s*dep_([a-z0-9_]+)\s*=\s*(hex|git_rmq|git)\s+(.+?)\s*$").expect("regex")
236});
237
238impl DepPin {
239 #[must_use]
241 pub fn display(&self) -> String {
242 format!("{} {}", self.source.as_label(), self.version)
243 }
244
245 #[must_use]
247 pub fn parse_display(name: &DependencyName, s: &str) -> Option<Self> {
248 let (label, version) = s.split_once(' ')?;
249 Some(Self {
250 name: name.clone(),
251 version: DependencyVersion::new(version).ok()?,
252 source: DepSource::from_label(label)?,
253 })
254 }
255}
256
257enum PinLine {
259 Pin(DepPin),
260 Refused(String),
261}
262
263fn scan_pin_line(line: &str) -> Option<PinLine> {
264 if line.trim_start().starts_with('#') {
265 return None;
266 }
267 let caps = DEP_RE.captures(line)?;
268 let name = &caps[1];
269 if name.ends_with("_commit") || name.ends_with("_branch") || name.ends_with("_repo") {
270 return None;
271 }
272 let source = DepSource::from_label(&caps[2])?;
273 let rest = caps[3].trim();
274 let mut words = rest.split_whitespace();
275 let version = match source {
276 DepSource::Hex => words.next()?,
277 DepSource::Git | DepSource::GitRmq => words.nth(1)?,
278 };
279 Some(
280 match (DependencyName::new(name), DependencyVersion::new(version)) {
281 (Ok(name), Ok(version)) => PinLine::Pin(DepPin {
282 name,
283 version,
284 source,
285 }),
286 _ => PinLine::Refused(name.to_owned()),
287 },
288 )
289}
290
291#[must_use]
294pub fn parse_pin_line(line: &str) -> Option<DepPin> {
295 match scan_pin_line(line)? {
296 PinLine::Pin(pin) => Some(pin),
297 PinLine::Refused(_) => None,
298 }
299}
300
301#[derive(Debug, Clone, Default, PartialEq, Eq)]
304pub struct PinScan {
305 pub pins: Vec<DepPin>,
306 pub refused_names: Vec<String>,
307}
308
309#[must_use]
310pub fn scan_components_mk(text: &str) -> PinScan {
311 let mut scan = PinScan::default();
312 for line in text.lines().filter_map(scan_pin_line) {
313 match line {
314 PinLine::Pin(pin) => scan.pins.push(pin),
315 PinLine::Refused(name) => scan.refused_names.push(name),
316 }
317 }
318 scan
319}
320
321#[must_use]
322pub fn parse_components_mk(text: &str) -> Vec<DepPin> {
323 scan_components_mk(text).pins
324}
325
326#[must_use]
329pub fn version_to_tag(version: &str, tag_prefix: &str) -> String {
330 if version.starts_with(tag_prefix) {
331 version.to_owned()
332 } else {
333 format!("{tag_prefix}{version}")
334 }
335}
336
337#[must_use]
339pub fn dep_to_tag(pin: &DepPin, tag_prefix: &str) -> String {
340 match pin.source {
341 DepSource::Hex => version_to_tag(pin.version.as_str(), tag_prefix),
342 DepSource::Git | DepSource::GitRmq => pin.version.to_string(),
343 }
344}