debian_analyzer/
rules.rs

1//! This module provides functions to manipulate debian/rules file.
2
3use makefile_lossless::{Makefile, Rule};
4
5/// Add a particular value to a with argument.
6pub fn dh_invoke_add_with(line: &str, with_argument: &str) -> String {
7    if line.contains(with_argument) {
8        return line.to_owned();
9    }
10    if !line.contains(" --with") {
11        return format!("{} --with={}", line, with_argument);
12    }
13
14    lazy_regex::regex_replace!(
15        r"([ \t])--with([ =])([^ \t]+)",
16        line,
17        |_, head, _with, tail| format!("{}--with={},{}", head, with_argument, tail)
18    )
19    .to_string()
20}
21
22/// Obtain the value of a with argument.
23pub fn dh_invoke_get_with(line: &str) -> Vec<String> {
24    let mut ret = Vec::new();
25    for cap in lazy_regex::regex!("[ \t]--with[ =]([^ \t]+)").captures_iter(line) {
26        if let Some(m) = cap.get(1) {
27            ret.extend(m.as_str().split(',').map(|s| s.to_owned()));
28        }
29    }
30    ret
31}
32
33/// Drop a particular value from a with argument.
34///
35/// # Arguments
36/// * `line` - The command line to modify
37/// * `with_argument` - The with argument to remove
38///
39/// # Returns
40/// The modified line with the argument removed
41///
42/// # Examples
43/// ```rust
44/// use debian_analyzer::rules::dh_invoke_drop_with;
45/// assert_eq!(
46///     dh_invoke_drop_with("dh $@ --with=foo,bar", "foo"),
47///     "dh $@ --with=bar"
48/// );
49/// assert_eq!(
50///     dh_invoke_drop_with("dh $@ --with=foo", "foo"),
51///     "dh $@"
52/// );
53/// ```
54pub fn dh_invoke_drop_with(line: &str, with_argument: &str) -> String {
55    if !line.contains(with_argument) {
56        return line.to_owned();
57    }
58
59    let mut result = line.to_owned();
60    let escaped = regex::escape(with_argument);
61
62    // It's the only with argument
63    if let Ok(re) = regex::Regex::new(&format!(r"[ \t]--with[ =]{}( .+|)$", escaped)) {
64        result = re.replace(&result, "$1").to_string();
65    }
66
67    // It's at the beginning
68    if let Ok(re) = regex::Regex::new(&format!(r"([ \t])--with([ =]){},", escaped)) {
69        result = re.replace(&result, "${1}--with${2}").to_string();
70    }
71
72    // It's in the middle or end
73    if let Ok(re) = regex::Regex::new(&format!(r"([ \t])--with([ =])(.+),{}([ ,])", escaped)) {
74        result = re.replace(&result, "${1}--with${2}${3}${4}").to_string();
75    }
76
77    // It's at the end
78    if let Ok(re) = regex::Regex::new(&format!(r"([ \t])--with([ =])(.+),{}$", escaped)) {
79        result = re.replace(&result, "${1}--with${2}${3}").to_string();
80    }
81
82    result
83}
84
85/// Drop a particular argument from a dh invocation.
86///
87/// # Arguments
88/// * `line` - The command line to modify
89/// * `argument` - The argument to remove
90///
91/// # Returns
92/// The modified line with the argument removed
93///
94/// # Examples
95/// ```rust
96/// use debian_analyzer::rules::dh_invoke_drop_argument;
97/// assert_eq!(
98///     dh_invoke_drop_argument("dh $@ --foo --bar", "--foo"),
99///     "dh $@ --bar"
100/// );
101/// ```
102pub fn dh_invoke_drop_argument(line: &str, argument: &str) -> String {
103    if !line.contains(argument) {
104        return line.to_owned();
105    }
106
107    let mut result = line.to_owned();
108    let escaped = regex::escape(argument);
109
110    // At the end
111    if let Ok(re) = regex::Regex::new(&format!(r"[ \t]+{}$", escaped)) {
112        result = re.replace(&result, "").to_string();
113    }
114
115    // In the middle
116    if let Ok(re) = regex::Regex::new(&format!(r"([ \t]){}[ \t]", escaped)) {
117        result = re.replace(&result, "$1").to_string();
118    }
119
120    result
121}
122
123/// Replace one argument with another in a dh invocation.
124///
125/// # Arguments
126/// * `line` - The command line to modify
127/// * `old` - The argument to replace
128/// * `new` - The new argument value
129///
130/// # Returns
131/// The modified line with the argument replaced
132///
133/// # Examples
134/// ```rust
135/// use debian_analyzer::rules::dh_invoke_replace_argument;
136/// assert_eq!(
137///     dh_invoke_replace_argument("dh $@ --foo", "--foo", "--bar"),
138///     "dh $@ --bar"
139/// );
140/// ```
141pub fn dh_invoke_replace_argument(line: &str, old: &str, new: &str) -> String {
142    if !line.contains(old) {
143        return line.to_owned();
144    }
145
146    let mut result = line.to_owned();
147    let escaped = regex::escape(old);
148
149    // At the end
150    if let Ok(re) = regex::Regex::new(&format!(r"([ \t]){}$", escaped)) {
151        result = re.replace(&result, format!("$1{}", new)).to_string();
152    }
153
154    // In the middle
155    if let Ok(re) = regex::Regex::new(&format!(r"([ \t]){}([ \t])", escaped)) {
156        result = re.replace(&result, format!("$1{}$2", new)).to_string();
157    }
158
159    result
160}
161
162/// Check if a debian/rules file uses CDBS.
163///
164/// # Arguments
165/// * `path` - Path to the debian/rules file
166///
167/// # Returns
168/// true if the file includes CDBS, false otherwise
169///
170/// # Examples
171/// ```rust,no_run
172/// use debian_analyzer::rules::check_cdbs;
173/// use std::path::Path;
174/// assert!(!check_cdbs(Path::new("debian/rules")));
175/// ```
176pub fn check_cdbs(path: &std::path::Path) -> bool {
177    let Ok(contents) = std::fs::read(path) else {
178        return false;
179    };
180
181    for line in contents.split(|&b| b == b'\n') {
182        let trimmed = line.strip_prefix(b"-").unwrap_or(line);
183        if trimmed.starts_with(b"include /usr/share/cdbs/") {
184            return true;
185        }
186    }
187    false
188}
189
190/// Discard a pointless override rule from a Makefile.
191///
192/// A pointless override is one that just calls the base command without any modifications.
193/// For example:
194/// ```makefile
195/// override_dh_auto_build:
196///     dh_auto_build
197/// ```
198///
199/// Note: The makefile-lossless crate's `recipes()` method only returns actual command lines,
200/// not comment lines, so comment lines are automatically ignored.
201///
202/// # Arguments
203/// * `makefile` - The makefile to modify
204/// * `rule` - The rule to check and potentially remove
205///
206/// # Returns
207/// `true` if the rule was removed, `false` otherwise
208pub fn discard_pointless_override(makefile: &mut Makefile, rule: &Rule) -> bool {
209    // Check if any target starts with "override_"
210    let Some(target) = rule.targets().find(|t| t.starts_with("override_")) else {
211        return false;
212    };
213
214    // Get the command name (strip "override_" prefix)
215    let command = &target["override_".len()..];
216
217    // Get the recipes (commands) for this rule and filter out empty lines
218    let effective_recipes: Vec<String> = rule
219        .recipes()
220        .filter(|line| !line.trim().is_empty())
221        .collect();
222
223    // Check if there's exactly one effective recipe and it matches the command
224    if effective_recipes.len() != 1 || effective_recipes[0].trim() != command {
225        return false;
226    }
227
228    // Check if there are any prerequisites
229    if rule.prerequisites().next().is_some() {
230        return false;
231    }
232
233    // Remove the rule and also from .PHONY if present
234    let _ = makefile.remove_phony_target(&target);
235    rule.clone().remove().is_ok()
236}
237
238/// Discard all pointless override rules from a Makefile.
239///
240/// # Arguments
241/// * `makefile` - The makefile to modify
242///
243/// # Returns
244/// The number of rules that were removed
245pub fn discard_pointless_overrides(makefile: &mut Makefile) -> usize {
246    let mut removed = 0;
247
248    // Collect all rules first to avoid modifying while iterating
249    let rules: Vec<Rule> = makefile.rules().collect();
250
251    for rule in rules {
252        if discard_pointless_override(makefile, &rule) {
253            removed += 1;
254        }
255    }
256
257    removed
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_dh_invoke_add_with() {
266        assert_eq!(dh_invoke_add_with("dh", "blah"), "dh --with=blah");
267        assert_eq!(
268            dh_invoke_add_with("dh --with=foo", "blah"),
269            "dh --with=blah,foo"
270        );
271        assert_eq!(
272            dh_invoke_add_with("dh --with=foo --other", "blah"),
273            "dh --with=blah,foo --other"
274        );
275    }
276
277    #[test]
278    fn test_dh_invoke_get_with() {
279        assert_eq!(dh_invoke_get_with("dh --with=blah --foo"), vec!["blah"]);
280        assert_eq!(dh_invoke_get_with("dh --with=blah"), vec!["blah"]);
281        assert_eq!(
282            dh_invoke_get_with("dh --with=blah,blie"),
283            vec!["blah", "blie"]
284        );
285        // Regression test for bug where find_iter() returned full match instead of captured group
286        // The function should return just "python3", not " --with python3"
287        assert_eq!(dh_invoke_get_with("dh $@ --with python3"), vec!["python3"]);
288        // Test with multiple addons using space separator
289        assert_eq!(
290            dh_invoke_get_with("dh $@ --with foo,bar,baz"),
291            vec!["foo", "bar", "baz"]
292        );
293    }
294
295    #[test]
296    fn test_dh_invoke_drop_with() {
297        assert_eq!(dh_invoke_drop_with("dh --with=blah", "blah"), "dh");
298        assert_eq!(
299            dh_invoke_drop_with("dh --with=blah,foo", "blah"),
300            "dh --with=foo"
301        );
302        assert_eq!(
303            dh_invoke_drop_with("dh --with=blah,foo --other", "blah"),
304            "dh --with=foo --other"
305        );
306        assert_eq!(dh_invoke_drop_with("dh --with=blah", "blah"), "dh");
307        assert_eq!(
308            dh_invoke_drop_with("dh --with=foo,blah", "blah"),
309            "dh --with=foo"
310        );
311        assert_eq!(
312            dh_invoke_drop_with(
313                "dh $@ --verbose --with autoreconf,systemd,cme-upgrade",
314                "systemd"
315            ),
316            "dh $@ --verbose --with autoreconf,cme-upgrade"
317        );
318        assert_eq!(
319            dh_invoke_drop_with(
320                "dh $@ --with gir,python3,sphinxdoc,systemd --without autoreconf --buildsystem=cmake",
321                "systemd"
322            ),
323            "dh $@ --with gir,python3,sphinxdoc --without autoreconf --buildsystem=cmake"
324        );
325        assert_eq!(
326            dh_invoke_drop_with("dh $@ --with systemd", "systemd"),
327            "dh $@"
328        );
329    }
330
331    #[test]
332    fn test_dh_invoke_drop_argument() {
333        assert_eq!(
334            dh_invoke_drop_argument("dh $@ --foo --bar", "--foo"),
335            "dh $@ --bar"
336        );
337        assert_eq!(
338            dh_invoke_drop_argument("dh $@ --foo --bar", "--bar"),
339            "dh $@ --foo"
340        );
341        assert_eq!(dh_invoke_drop_argument("dh $@ --foo", "--foo"), "dh $@");
342    }
343
344    #[test]
345    fn test_dh_invoke_replace_argument() {
346        assert_eq!(
347            dh_invoke_replace_argument("dh $@ --foo", "--foo", "--bar"),
348            "dh $@ --bar"
349        );
350        assert_eq!(
351            dh_invoke_replace_argument("dh $@ --foo --baz", "--foo", "--bar"),
352            "dh $@ --bar --baz"
353        );
354    }
355
356    #[test]
357    fn test_discard_pointless_override() {
358        // Test a pointless override that should be removed
359        let makefile_text = r#"
360override_dh_auto_build:
361	dh_auto_build
362"#;
363        let mut makefile = makefile_text.parse::<Makefile>().unwrap();
364        let rules: Vec<Rule> = makefile.rules().collect();
365        assert_eq!(rules.len(), 1);
366
367        let removed = discard_pointless_override(&mut makefile, &rules[0]);
368        assert!(removed, "Should have removed the pointless override");
369
370        let remaining_rules: Vec<Rule> = makefile.rules().collect();
371        assert_eq!(remaining_rules.len(), 0, "Rule should be removed");
372    }
373
374    #[test]
375    fn test_discard_pointless_override_with_args() {
376        // Test an override with arguments - should NOT be removed
377        let makefile_text = r#"
378override_dh_auto_build:
379	dh_auto_build --foo
380"#;
381        let mut makefile = makefile_text.parse::<Makefile>().unwrap();
382        let rules: Vec<Rule> = makefile.rules().collect();
383        assert_eq!(rules.len(), 1);
384
385        let removed = discard_pointless_override(&mut makefile, &rules[0]);
386        assert!(!removed, "Should NOT remove override with arguments");
387
388        let remaining_rules: Vec<Rule> = makefile.rules().collect();
389        assert_eq!(remaining_rules.len(), 1, "Rule should remain");
390    }
391
392    #[test]
393    fn test_discard_pointless_override_with_comment() {
394        // Test an override with just a comment - since recipes() doesn't return comments,
395        // this should still be removed because only the actual command matters
396        let makefile_text = r#"
397override_dh_auto_build:
398	# This is a comment
399	dh_auto_build
400"#;
401        let mut makefile = makefile_text.parse::<Makefile>().unwrap();
402        let rules: Vec<Rule> = makefile.rules().collect();
403        assert_eq!(rules.len(), 1);
404
405        // The recipes() method doesn't return comment lines, so this is still pointless
406        let removed = discard_pointless_override(&mut makefile, &rules[0]);
407        assert!(
408            removed,
409            "Should remove - recipes() doesn't include comments"
410        );
411    }
412
413    #[test]
414    fn test_discard_pointless_override_not_override() {
415        // Test a regular rule that doesn't start with override_
416        let makefile_text = r#"
417build:
418	dh_auto_build
419"#;
420        let mut makefile = makefile_text.parse::<Makefile>().unwrap();
421        let rules: Vec<Rule> = makefile.rules().collect();
422        assert_eq!(rules.len(), 1);
423
424        let removed = discard_pointless_override(&mut makefile, &rules[0]);
425        assert!(!removed, "Should NOT remove non-override rules");
426    }
427
428    #[test]
429    fn test_discard_pointless_overrides() {
430        // Test removing multiple pointless overrides
431        let makefile_text = r#"
432override_dh_auto_build:
433	dh_auto_build
434
435override_dh_auto_test:
436	dh_auto_test
437
438override_dh_auto_install:
439	dh_auto_install --foo
440"#;
441        let mut makefile = makefile_text.parse::<Makefile>().unwrap();
442        let initial_rules = makefile.rules().count();
443        assert_eq!(initial_rules, 3);
444
445        let removed = discard_pointless_overrides(&mut makefile);
446        assert_eq!(removed, 2, "Should remove 2 pointless overrides");
447
448        let remaining_rules: Vec<Rule> = makefile.rules().collect();
449        assert_eq!(remaining_rules.len(), 1, "Should have 1 rule remaining");
450
451        // Verify the remaining rule is the one with arguments
452        let targets: Vec<String> = remaining_rules[0].targets().collect();
453        assert_eq!(targets, vec!["override_dh_auto_install"]);
454    }
455}