Skip to content

Commit 72927f4

Browse files
authored
Merge pull request #559 from rustcoreutils/updates
make: Fix intermittent dash_r_with_file test failure and inference ru…
2 parents 3c882ba + f4ebb6c commit 72927f4

File tree

9 files changed

+306
-69
lines changed

9 files changed

+306
-69
lines changed

make/src/lib.rs

Lines changed: 122 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ const DEFAULT_SHELL: &str = "/bin/sh";
3939
/// The only way to create a Make is from a Makefile and a Config.
4040
pub struct Make {
4141
macros: Vec<VariableDefinition>,
42+
/// Target rules (non-special, non-inference).
43+
/// Invariant: inference rules are never stored here, so `first_target()`
44+
/// always returns a valid default target per POSIX.
4245
rules: Vec<Rule>,
46+
/// Inference rules (e.g. `.c.o:`, `.txt.out:`).
47+
inference_rules: Vec<Rule>,
4348
default_rule: Option<Rule>, // .DEFAULT
4449
pub config: Config,
4550
}
@@ -58,25 +63,84 @@ impl Make {
5863
}
5964

6065
pub fn first_target(&self) -> Result<&Target, ErrorCode> {
61-
let rule = self.rules.first().ok_or(NoTarget { target: None })?;
66+
// Per POSIX: "the first target that make encounters that is not a special
67+
// target or an inference rule shall be used."
68+
// If there are no non-special, non-inference targets, fall back to the
69+
// first inference rule (which will scan CWD for matching files).
70+
let rule = self
71+
.rules
72+
.first()
73+
.or_else(|| self.inference_rules.first())
74+
.ok_or(NoTarget { target: None })?;
6275
rule.targets().next().ok_or(NoTarget { target: None })
6376
}
6477

78+
/// Finds a matching inference rule for the given target name.
79+
///
80+
/// Per POSIX: the suffix of the target (.s1) is compared to .SUFFIXES.
81+
/// If found, inference rules are searched for the first .s2.s1 rule whose
82+
/// prerequisite file ($*.s2) exists.
83+
fn find_inference_rule(&self, name: &str) -> Option<&Rule> {
84+
let suffixes = self.config.rules.get(".SUFFIXES")?;
85+
86+
// Find the target's suffix (.s1)
87+
let target_suffix = suffixes
88+
.iter()
89+
.filter(|s| name.ends_with(s.as_str()))
90+
.max_by_key(|s| s.len())?;
91+
92+
let stem = &name[..name.len() - target_suffix.len()];
93+
94+
// Search inference rules for .s2.s1 where $*.s2 exists
95+
for rule in &self.inference_rules {
96+
let Some(rule_target) = rule.targets().next() else {
97+
continue;
98+
};
99+
if let Target::Inference { from, to, .. } = rule_target {
100+
let expected_suffix = format!(".{}", to);
101+
if expected_suffix == *target_suffix {
102+
let prereq_path = format!("{}.{}", stem, from);
103+
if std::path::Path::new(&prereq_path).exists() {
104+
return Some(rule);
105+
}
106+
}
107+
}
108+
}
109+
None
110+
}
111+
65112
/// Builds the target with the given name.
66113
///
67114
/// # Returns
68115
/// - Ok(true) if the target was built.
69116
/// - Ok(false) if the target was already up to date.
70117
/// - Err(_) if any errors occur.
71118
pub fn build_target(&self, name: impl AsRef<str>) -> Result<bool, ErrorCode> {
119+
// Search both regular rules and inference rules
72120
let rule = match self.rule_by_target_name(&name) {
73121
Some(rule) => rule,
74-
None => match &self.default_rule {
122+
None => match self
123+
.inference_rules
124+
.iter()
125+
.find(|rule| rule.targets().any(|t| t.as_ref() == name.as_ref()))
126+
{
75127
Some(rule) => rule,
76128
None => {
77-
return Err(NoTarget {
78-
target: Some(name.as_ref().to_string()),
79-
})
129+
// Per POSIX: "If a target exists and there is neither a target rule
130+
// nor an inference rule for the target, the target shall be considered
131+
// up-to-date."
132+
if get_modified_time(&name).is_some() {
133+
return Ok(false);
134+
}
135+
// No rule and file doesn't exist - try .DEFAULT or fail
136+
match &self.default_rule {
137+
Some(rule) => rule,
138+
None => {
139+
return Err(NoTarget {
140+
target: Some(name.as_ref().to_string()),
141+
})
142+
}
143+
}
80144
}
81145
},
82146
};
@@ -111,6 +175,18 @@ impl Make {
111175
for prerequisite in &newer_prerequisites {
112176
self.build_target(prerequisite)?;
113177
}
178+
179+
// Per POSIX: "When no target rule with commands is found to update a
180+
// target, the inference rules shall be checked." If the matched target
181+
// rule has no recipes, look for a matching inference rule and run it
182+
// for this specific target instead.
183+
if rule.recipes().count() == 0 {
184+
if let Some(inference_rule) = self.find_inference_rule(target.as_ref()) {
185+
inference_rule.run_for_target(&self.config, &self.macros, target, up_to_date)?;
186+
return Ok(true);
187+
}
188+
}
189+
114190
rule.run(&self.config, &self.macros, target, up_to_date)?;
115191

116192
Ok(true)
@@ -184,32 +260,60 @@ impl TryFrom<(Makefile, Config)> for Make {
184260
type Error = ErrorCode;
185261

186262
fn try_from((makefile, config): (Makefile, Config)) -> Result<Self, Self::Error> {
187-
let mut rules = vec![];
188-
let mut special_rules = vec![];
189-
let mut inference_rules = vec![];
190-
191-
for rule in makefile.rules() {
192-
let rule = Rule::from(rule);
263+
// Two-pass classification: .SUFFIXES must be processed before inference
264+
// rule classification so that user-defined suffixes (especially with -r)
265+
// are available when determining whether a rule like `.txt.out:` is an
266+
// inference rule.
267+
268+
let mut suffixes_rules = vec![];
269+
let mut remaining_parsed_rules = vec![];
270+
271+
// Pass 1: Separate .SUFFIXES rules from everything else and process
272+
// them immediately so config.rules[".SUFFIXES"] is populated.
273+
for parsed_rule in makefile.rules() {
274+
let rule = Rule::from(parsed_rule);
193275
let Some(target) = rule.targets().next() else {
194276
return Err(NoTarget { target: None });
195277
};
196-
197-
if SpecialTarget::try_from(target.clone()).is_ok() {
198-
special_rules.push(rule);
199-
} else if InferenceTarget::try_from((target.clone(), config.clone())).is_ok() {
200-
inference_rules.push(rule);
278+
if let Ok(SpecialTarget::Suffixes) = SpecialTarget::try_from(target.clone()) {
279+
suffixes_rules.push(rule);
201280
} else {
202-
rules.push(rule);
281+
remaining_parsed_rules.push(rule);
203282
}
204283
}
205284

285+
// Build the Make struct early so we can process .SUFFIXES via the
286+
// normal special_target::process path (which writes to make.config).
206287
let mut make = Self {
207-
rules,
288+
rules: vec![],
289+
inference_rules: vec![],
208290
macros: makefile.variable_definitions().collect(),
209291
default_rule: None,
210292
config,
211293
};
212294

295+
for rule in suffixes_rules {
296+
special_target::process(rule, &mut make)?;
297+
}
298+
299+
// Pass 2: Classify remaining rules. Now make.config.rules[".SUFFIXES"]
300+
// contains both built-in (unless -r) and user-defined suffixes.
301+
let mut special_rules = vec![];
302+
303+
for rule in remaining_parsed_rules {
304+
let Some(target) = rule.targets().next() else {
305+
return Err(NoTarget { target: None });
306+
};
307+
308+
if SpecialTarget::try_from(target.clone()).is_ok() {
309+
special_rules.push(rule);
310+
} else if InferenceTarget::try_from((target.clone(), make.config.clone())).is_ok() {
311+
make.inference_rules.push(rule);
312+
} else {
313+
make.rules.push(rule);
314+
}
315+
}
316+
213317
for rule in special_rules {
214318
special_target::process(rule, &mut make)?;
215319
}

make/src/rule.rs

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ impl Rule {
6666
self.recipes.iter()
6767
}
6868

69+
/// Runs an inference rule for a specific target (not a CWD scan).
70+
///
71+
/// This is used when POSIX requires applying an inference rule to a specific
72+
/// target that has no commands of its own. The internal macros ($<, $*, etc.)
73+
/// are substituted based on the target name and the inference rule's suffixes.
74+
pub fn run_for_target(
75+
&self,
76+
global_config: &GlobalConfig,
77+
macros: &[VariableDefinition],
78+
target: &Target,
79+
up_to_date: bool,
80+
) -> Result<(), ErrorCode> {
81+
// For an inference rule applied to a specific target, compute the
82+
// input/output pair from the target name and the rule's suffixes.
83+
let files = if let Some(Target::Inference { from, to, .. }) = self.targets().next() {
84+
let target_name = target.as_ref();
85+
let expected_suffix = format!(".{}", to);
86+
if let Some(stem) = target_name.strip_suffix(&expected_suffix) {
87+
let input = PathBuf::from(format!("{}.{}", stem, from));
88+
let output = PathBuf::from(target_name);
89+
vec![(input, output)]
90+
} else {
91+
vec![(PathBuf::from(""), PathBuf::from(""))]
92+
}
93+
} else {
94+
vec![(PathBuf::from(""), PathBuf::from(""))]
95+
};
96+
97+
self.run_with_files(global_config, macros, target, up_to_date, files)
98+
}
99+
69100
/// Runs the rule with the global config and macros passed in.
70101
///
71102
/// Returns `Ok` on success and `Err` on any errors while running the rule.
@@ -75,6 +106,32 @@ impl Rule {
75106
macros: &[VariableDefinition],
76107
target: &Target,
77108
up_to_date: bool,
109+
) -> Result<(), ErrorCode> {
110+
let files = match target {
111+
Target::Inference { from, to, .. } => find_files_with_extension(from)?
112+
.into_iter()
113+
.map(|input| {
114+
let mut output = input.clone();
115+
output.set_extension(to);
116+
(input, output)
117+
})
118+
.collect::<Vec<_>>(),
119+
_ => {
120+
vec![(PathBuf::from(""), PathBuf::from(""))]
121+
}
122+
};
123+
124+
self.run_with_files(global_config, macros, target, up_to_date, files)
125+
}
126+
127+
/// Internal helper: runs the rule's recipes for the given input/output file pairs.
128+
fn run_with_files(
129+
&self,
130+
global_config: &GlobalConfig,
131+
macros: &[VariableDefinition],
132+
target: &Target,
133+
up_to_date: bool,
134+
files: Vec<(PathBuf, PathBuf)>,
78135
) -> Result<(), ErrorCode> {
79136
let GlobalConfig {
80137
ignore: global_ignore,
@@ -97,20 +154,6 @@ impl Rule {
97154
phony: _,
98155
} = self.config;
99156

100-
let files = match target {
101-
Target::Inference { from, to, .. } => find_files_with_extension(from)?
102-
.into_iter()
103-
.map(|input| {
104-
let mut output = input.clone();
105-
output.set_extension(to);
106-
(input, output)
107-
})
108-
.collect::<Vec<_>>(),
109-
_ => {
110-
vec![(PathBuf::from(""), PathBuf::from(""))]
111-
}
112-
};
113-
114157
for inout in files {
115158
for recipe in self.recipes() {
116159
let RecipeConfig {

make/src/special_target.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ impl Processor<'_> {
200200
self.make
201201
.rules
202202
.iter_mut()
203+
.chain(self.make.inference_rules.iter_mut())
203204
.filter(|r| r.targets().any(|t| t.as_ref() == prerequisite.as_ref()))
204205
.for_each(f.clone());
205206
}
@@ -209,7 +210,11 @@ impl Processor<'_> {
209210
/// specified.
210211
fn global(&mut self, f: impl FnMut(&mut Rule) + Clone) {
211212
if self.rule.prerequisites().count() == 0 {
212-
self.make.rules.iter_mut().for_each(f);
213+
self.make
214+
.rules
215+
.iter_mut()
216+
.chain(self.make.inference_rules.iter_mut())
217+
.for_each(f);
213218
}
214219
}
215220
}

0 commit comments

Comments
 (0)