Retour à une version plus stable

This commit is contained in:
Moncef STITI 2025-03-15 17:02:26 +01:00
parent b40f095571
commit 4b1558e529
5 changed files with 191 additions and 294 deletions

@ -59,4 +59,4 @@ public class BakeCLI {
* @return La liste des arguments * @return La liste des arguments
*/ */
public static List<String> getTargets(){ return targets; } public static List<String> getTargets(){ return targets; }
} }

@ -3,7 +3,6 @@ package fr.monlouyan.bakefile;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Moteur principal de l'application Bake. * Moteur principal de l'application Bake.
@ -86,12 +85,4 @@ public class BakeEngine {
executor.execute(rule); executor.execute(rule);
} }
} }
/**
* Récupère les noms de toutes les règles.
* @return Un ensemble contenant les noms de toutes les règles
*/
public static Set<String> getAllRuleNames() {
return ruleMap.keySet();
}
} }

@ -26,10 +26,9 @@ public class BakefileParser {
/** /**
* Regex pour détecter les targets et leurs dépendances. * Regex pour détecter les targets et leurs dépendances.
* Format : "nom1 nom2 nom3 : dépendance1 dépendance2" * Format : "nom1 nom2 nom3 : dépendance1 dépendance2"
* La nouvelle regex assure que la ligne ne commence pas par une tabulation * La nouvelle regex gère plusieurs cibles séparées par des espaces
* et vérifie que le premier caractère non-espace n'est pas un symbole de commentaire
*/ */
private static final Pattern TARGET_PATTERN = Pattern.compile("^([^\\t:#][^:#]+?)\\s*:\\s*([^#]*?)\\s*(?:#.*)?$"); private static final Pattern TARGET_PATTERN = Pattern.compile("^([^:#]+?)\\s*:\\s*([^#]*?)\\s*(?:#.*)?$");
/** /**
* Regex pour détecter les lignes de commande associées à une target. * Regex pour détecter les lignes de commande associées à une target.
@ -87,71 +86,68 @@ public class BakefileParser {
* @return La ligne combinée * @return La ligne combinée
*/ */
private String handleContinuationLines(List<String> lines, int startIndex) { private String handleContinuationLines(List<String> lines, int startIndex) {
StringBuilder combinedLine = new StringBuilder(); StringBuilder combinedLine = new StringBuilder();
int i = startIndex; int i = startIndex;
while (i < lines.size()) { while (i < lines.size()) {
String line = lines.get(i); String line = lines.get(i);
Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); Matcher contMatcher = CONTINUATION_PATTERN.matcher(line);
if (contMatcher.matches()) { if (contMatcher.matches()) {
// Ajouter la ligne sans le backslash mais conserver le contenu entier // Ajouter la ligne sans le backslash
// Ne pas ajouter d'espace après certains opérateurs comme && combinedLine.append(contMatcher.group(1).trim()).append(" ");
String content = contMatcher.group(1); i++;
combinedLine.append(content); } else {
// Ajouter la dernière ligne et sortir
// Si la ligne ne se termine pas déjà par un opérateur tel que &&, ajouter un espace combinedLine.append(line.trim());
if (!content.trim().endsWith("&&") && !content.trim().endsWith("|") && break;
!content.trim().endsWith(";")) { }
combinedLine.append(" "); }
} else {
// Si elle se termine par &&, |, ou ;, ajouter juste un espace après return combinedLine.toString();
combinedLine.append(" "); }
}
i++;
} else {
// Ajouter la dernière ligne et sortir
combinedLine.append(line.trim());
break;
}
}
return combinedLine.toString();
}
/** /**
* Remplacer les variables dans une chaîne. * Remplacer les variables dans une chaîne.
* Cette méthode remplace toutes les références de variables (${VAR} ou $(VAR))
* par leur valeur. Si une variable n'est pas définie, elle est remplacée par une chaîne vide.
*
* @param input Chaîne à traiter * @param input Chaîne à traiter
* @return Chaîne avec les variables remplacées * @return Chaîne avec les variables remplacées
*/ */
private String replaceVariables(String input) { private String replaceVariables(String input) {
if (input == null) return null; if (input == null) return null;
String result = input; String result = input;
Set<String> processedVars = new HashSet<>();
// Détecter et remplacer toutes les occurrences de variables boolean changed;
Matcher matcher = VARIABLE_REFERENCE.matcher(result);
StringBuffer sb = new StringBuffer(); do {
changed = false;
while (matcher.find()) { Matcher matcher = VARIABLE_REFERENCE.matcher(result);
String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); StringBuffer sb = new StringBuffer();
// Remplacer par la valeur de la variable si elle existe, sinon par une chaîne vide
String replacement = variables.containsKey(varName) ? variables.get(varName) : ""; while (matcher.find()) {
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2);
} if (!processedVars.contains(varName) && variables.containsKey(varName)) {
matcher.appendTail(sb); String replacement = variables.get(varName);
result = sb.toString(); matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
changed = true;
// Vérifier les références imbriquées de variables et continuer à remplacer si nécessaire processedVars.add(varName);
if (VARIABLE_REFERENCE.matcher(result).find()) { }
result = replaceVariables(result); // Appel récursif pour gérer les variables imbriquées }
} matcher.appendTail(sb);
result = sb.toString();
return result.trim();
} // Si aucun changement n'a été fait dans ce passage, arrêter
if (!changed) {
break;
}
// Réinitialiser processedVars pour le prochain passage si nécessaire
processedVars.clear();
} while (changed);
return result.trim();
}
/** /**
* Remplacer les variables dans une liste de chaînes. * Remplacer les variables dans une liste de chaînes.
@ -205,10 +201,9 @@ public class BakefileParser {
public List<Rule> parse() { public List<Rule> parse() {
List<Rule> rules = new ArrayList<>(); List<Rule> rules = new ArrayList<>();
Set<String> phonyTargets = new HashSet<>(); Set<String> phonyTargets = new HashSet<>();
List<String> allTargetNames = new ArrayList<>(); // Pour suivre l'ordre des cibles
if (!Files.exists(Paths.get(filename))) { if (!Files.exists(Paths.get(filename))) {
System.out.println("*** No targets specified and no bakefile found. Stop."); System.out.println("*** No targets specified and no makefile found. Stop.");
System.exit(2); System.exit(2);
} }
@ -218,18 +213,6 @@ public class BakefileParser {
List<String> dependencies = new ArrayList<>(); List<String> dependencies = new ArrayList<>();
List<String> commands = new ArrayList<>(); List<String> commands = new ArrayList<>();
// Première passe : collecter toutes les cibles PHONY
for (String line : lines) {
if (line.trim().isEmpty()) continue;
Matcher phonyMatcher = PHONY_PATTERN.matcher(line);
if (phonyMatcher.matches()) {
String[] phonies = phonyMatcher.group(1).trim().split("\\s+");
Collections.addAll(phonyTargets, phonies);
}
}
// Deuxième passe : analyser les règles en tenant compte des PHONY
for (int i = 0; i < lines.size(); i++) { for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i); String line = lines.get(i);
@ -260,7 +243,8 @@ public class BakefileParser {
Matcher phonyMatcher = PHONY_PATTERN.matcher(line); Matcher phonyMatcher = PHONY_PATTERN.matcher(line);
if (phonyMatcher.matches()) { if (phonyMatcher.matches()) {
// Déjà traité dans la première passe String[] phonies = phonyMatcher.group(1).trim().split("\\s+");
Collections.addAll(phonyTargets, phonies);
continue; continue;
} }
@ -283,8 +267,9 @@ public class BakefileParser {
phonyTargets.contains(resolvedTarget) phonyTargets.contains(resolvedTarget)
)); ));
// Enregistrer le nom de la cible pour suivre l'ordre if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) {
allTargetNames.add(resolvedTarget); firstTarget = resolvedTarget;
}
} }
} }
@ -309,32 +294,15 @@ public class BakefileParser {
phonyTargets.contains(resolvedTarget) phonyTargets.contains(resolvedTarget)
)); ));
// Enregistrer le nom de la cible pour suivre l'ordre if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) {
allTargetNames.add(resolvedTarget); firstTarget = resolvedTarget;
}
}
// Définir la première cible (similaire à Make)
// Make prend la première cible non-PHONY, ou la première cible si toutes sont PHONY
if (!allTargetNames.isEmpty()) {
// Chercher d'abord une cible non-PHONY dans l'ordre d'apparition
for (String targetName : allTargetNames) {
if (!phonyTargets.contains(targetName)) {
firstTarget = targetName;
break;
} }
} }
// Si toutes les cibles sont PHONY, prendre simplement la première
if (firstTarget == null) {
firstTarget = allTargetNames.get(0);
}
} }
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
return rules; return rules;
} }

@ -79,14 +79,6 @@ public class DependencyResolver {
continue; continue;
} }
// Ignorer les options de compilation qui pourraient avoir été mal interprétées comme des règles
if (ruleName.startsWith("-") || ruleName.contains(":")) {
if (debug) {
System.out.println("Debug: Skipping compiler option: " + ruleName);
}
continue;
}
Rule rule = ruleMap.get(ruleName); Rule rule = ruleMap.get(ruleName);
if (rule != null) { if (rule != null) {
rulesToBuild.add(rule); rulesToBuild.add(rule);
@ -116,12 +108,6 @@ public class DependencyResolver {
if (dep.startsWith("~")) { if (dep.startsWith("~")) {
continue; continue;
} }
// Ignorer les options de compilation qui pourraient avoir un ':' dedans
if (dep.startsWith("-") || dep.contains(":")) {
continue;
}
topologicalSort(dep, processed, buildOrder); topologicalSort(dep, processed, buildOrder);
} }
@ -162,11 +148,6 @@ public class DependencyResolver {
continue; continue;
} }
// Ignorer les options de compilation qui pourraient avoir un ':' dedans
if (dependency.startsWith("-") || dependency.contains(":")) {
continue;
}
if (ruleMap.containsKey(dependency)) { if (ruleMap.containsKey(dependency)) {
detectCycle(dependency, visited, stack, ruleName); detectCycle(dependency, visited, stack, ruleName);
} }

@ -1,7 +1,6 @@
package fr.monlouyan.bakefile; package fr.monlouyan.bakefile;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -74,171 +73,129 @@ public class Rule {
* Vérifie si la règle doit être mise à jour. * Vérifie si la règle doit être mise à jour.
* @return true si la règle doit être mise à jour, false sinon * @return true si la règle doit être mise à jour, false sinon
*/ */
public boolean needsUpdate() { public boolean needsUpdate() {
if (BakeCLI.isDebug()){ if (BakeCLI.isDebug()){
System.out.println("Debug : Checking if rule " + name + " needs update"); System.out.println("Debug : Checking if rule " + name + " needs update");
} }
// Les règles phony sont toujours mises à jour
if (isPhony) {
if (BakeCLI.isDebug()) {
System.out.println("Debug : Rule " + name + " is phony, always needs update");
}
return true;
}
// Skip targets with tilde in path (home directory) to match make behavior
if (name.startsWith("~")) {
if (BakeCLI.isDebug()) {
System.out.println("Debug : Skipping home directory path: " + name);
}
return false;
}
// Vérifier d'abord toutes les dépendances avant d'exécuter quoi que ce soit
for (String dependency : dependencies) {
// Skip dependencies with tilde in path
if (dependency.startsWith("~")) {
continue;
}
File depFile = new File(dependency);
boolean hasRule = BakeEngine.hasRule(dependency);
if (!depFile.exists() && !dependency.isEmpty() && !hasRule) {
System.out.println("bake: *** No rule to make target `" + dependency + "', needed by `" + name + "'. Stop.");
System.exit(2);
}
}
// Les règles phony sont toujours mises à jour // Si le fichier cible n'existe pas et qu'il y a des commandes, il doit être mis à jour
if (isPhony) { File targetFile = new File(name);
if (BakeCLI.isDebug()) { if (!targetFile.exists() && !commands.isEmpty()) {
System.out.println("Debug : Rule " + name + " is phony, always needs update"); if (BakeCLI.isDebug()) {
} System.out.println("Debug : Target file " + name + " does not exist and has commands, needs update");
return true; }
} return true;
}
// Skip targets with tilde in path (home directory) to match make behavior
if (name.startsWith("~")) { // Si la règle n'a pas de commandes, on vérifie seulement si une dépendance doit être mise à jour
if (BakeCLI.isDebug()) { if (commands.isEmpty()) {
System.out.println("Debug : Skipping home directory path: " + name); for (String dependency : dependencies) {
} // Skip dependencies with tilde in path
return false; if (dependency.startsWith("~")) {
} continue;
}
// Si le fichier cible n'existe pas et qu'il y a des commandes, il doit être mis à jour
File targetFile = new File(name); Rule depRule = BakeEngine.getRule(dependency);
if (!targetFile.exists() && !commands.isEmpty()) { if (depRule != null && depRule.needsUpdate()) {
if (BakeCLI.isDebug()) { if (BakeCLI.isDebug()) {
System.out.println("Debug : Target file " + name + " does not exist and has commands, needs update"); System.out.println("Debug : Dependency rule " + dependency + " needs update");
} }
return true; return true;
} }
}
// Vérifier d'abord toutes les dépendances avant d'exécuter quoi que ce soit return false;
for (String dependency : dependencies) { }
// Skip dependencies with tilde in path
if (dependency.startsWith("~")) { // Pour les règles avec des commandes, on vérifie aussi les timestamps
continue; if (BakeCLI.isDebug()){
} System.out.println("Debug : Checking if target file " + name + " exist and is up to date");
}
// Ignorer les options de compilation qui pourraient avoir un ':' dedans
if (dependency.startsWith("-") || dependency.contains(":")) { long targetTimestamp = targetFile.exists() ? TimestampManager.getTimestamp(targetFile) : 0;
continue;
} if (BakeCLI.isDebug()) {
System.out.println("Debug : Target file '" + name + "' last modified at " + TimestampManager.formatTimestamp(targetTimestamp));
File depFile = new File(dependency); }
boolean hasRule = BakeEngine.hasRule(dependency);
long currentTime = System.currentTimeMillis();
if (!depFile.exists() && !dependency.isEmpty() && !hasRule) {
// Vérifier si on est en situation de dépendance circulaire déjà traitée for (String dependency : dependencies) {
boolean isPartOfCircularDependency = false; // Skip dependencies with tilde in path
if (dependency.startsWith("~")) {
// Vérifier si cette dépendance est impliquée dans une relation circulaire continue;
for (Rule rule : getAllRules()) { }
if (rule.getName().equals(dependency) && rule.getDependencies().contains(name)) {
if (BakeCLI.isDebug()) { File depFile = new File(dependency);
System.out.println("Debug: Found circular dependency between " + name + " and " + dependency); if (!depFile.exists()) {
} continue;
isPartOfCircularDependency = true; }
break;
} long depTimestamp = TimestampManager.getTimestamp(depFile);
}
// Vérifier si le timestamp de la dépendance est dans le futur
if (isPartOfCircularDependency) { if (depTimestamp > currentTime) {
if (BakeCLI.isDebug()) { // Avertissement similaire à make
System.out.println("Debug: Ignoring circular dependency: " + dependency); System.out.println("bake: Warning: File '" + dependency + "' has modification time "
} + ((depTimestamp - currentTime) / 1000) + " s in the future");
continue; if (BakeCLI.isDebug()) {
} System.out.println("Debug : Dependency " + dependency + " has a timestamp in the future, needs update");
}
System.out.println("bake: *** No rule to make target `" + dependency + "', needed by `" + name + "'. Stop."); return true;
System.exit(2); }
}
} if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency '" + dependency + "' last modified at " + TimestampManager.formatTimestamp(depTimestamp));
// Si la règle n'a pas de commandes, on vérifie seulement si une dépendance doit être mise à jour }
if (commands.isEmpty()) {
for (String dependency : dependencies) { // Si la dépendance est une règle et qu'elle a besoin d'être mise à jour
// Skip dependencies with tilde in path or compiler options Rule depRule = BakeEngine.getRule(dependency);
if (dependency.startsWith("~") || dependency.startsWith("-") || dependency.contains(":")) { if (depRule != null && depRule.needsUpdate()) {
continue; if (BakeCLI.isDebug()) {
} System.out.println("Debug : Dependency rule " + dependency + " needs update");
}
Rule depRule = BakeEngine.getRule(dependency); return true;
if (depRule != null && depRule.needsUpdate()) { }
if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency rule " + dependency + " needs update"); // Vérifier les timestamps seulement si le fichier existe
} if (depTimestamp > targetTimestamp) {
return true; if (BakeCLI.isDebug()) {
} System.out.println("Debug : Dependency " + dependency + " is newer than target file " + name + ", needs update");
} }
return false; return true;
} }
}
// Pour les règles avec des commandes, on vérifie aussi les timestamps
if (BakeCLI.isDebug()){ return false;
System.out.println("Debug : Checking if target file " + name + " exist and is up to date"); }
}
long targetTimestamp = targetFile.exists() ? TimestampManager.getTimestamp(targetFile) : 0;
if (BakeCLI.isDebug()) {
System.out.println("Debug : Target file '" + name + "' last modified at " + TimestampManager.formatTimestamp(targetTimestamp));
}
long currentTime = System.currentTimeMillis();
for (String dependency : dependencies) {
// Skip dependencies with tilde in path or compiler options
if (dependency.startsWith("~") || dependency.startsWith("-") || dependency.contains(":")) {
continue;
}
File depFile = new File(dependency);
if (!depFile.exists()) {
continue;
}
long depTimestamp = TimestampManager.getTimestamp(depFile);
// Vérifier si le timestamp de la dépendance est dans le futur
if (depTimestamp > currentTime) {
// Avertissement similaire à make
System.out.println("bake: Warning: File '" + dependency + "' has modification time "
+ ((depTimestamp - currentTime) / 1000) + " s in the future");
if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency " + dependency + " has a timestamp in the future, needs update");
}
return true;
}
if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency '" + dependency + "' last modified at " + TimestampManager.formatTimestamp(depTimestamp));
}
// Si la dépendance est une règle et qu'elle a besoin d'être mise à jour
Rule depRule = BakeEngine.getRule(dependency);
if (depRule != null && depRule.needsUpdate()) {
if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency rule " + dependency + " needs update");
}
return true;
}
// Vérifier les timestamps seulement si le fichier existe
if (depTimestamp > targetTimestamp) {
if (BakeCLI.isDebug()) {
System.out.println("Debug : Dependency " + dependency + " is newer than target file " + name + ", needs update");
}
return true;
}
}
return false;
}
/**
* Permet de récupérer la liste de toutes les règles
* @return La liste de toutes les règles
*/
private List<Rule> getAllRules() {
List<Rule> allRules = new ArrayList<>();
for (String ruleName : BakeEngine.getAllRuleNames()) {
Rule rule = BakeEngine.getRule(ruleName);
if (rule != null) {
allRules.add(rule);
}
}
return allRules;
}
} }