diff --git a/src/fr/monlouyan/bakefile/BakeCLI.java b/src/fr/monlouyan/bakefile/BakeCLI.java index 3fba9cc..775c69b 100644 --- a/src/fr/monlouyan/bakefile/BakeCLI.java +++ b/src/fr/monlouyan/bakefile/BakeCLI.java @@ -59,4 +59,4 @@ public class BakeCLI { * @return La liste des arguments */ public static List<String> getTargets(){ return targets; } -} +} \ No newline at end of file diff --git a/src/fr/monlouyan/bakefile/BakeEngine.java b/src/fr/monlouyan/bakefile/BakeEngine.java index 4b2dd29..22b5a7d 100644 --- a/src/fr/monlouyan/bakefile/BakeEngine.java +++ b/src/fr/monlouyan/bakefile/BakeEngine.java @@ -3,7 +3,6 @@ package fr.monlouyan.bakefile; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; /** * Moteur principal de l'application Bake. @@ -86,12 +85,4 @@ public class BakeEngine { 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(); - } } \ No newline at end of file diff --git a/src/fr/monlouyan/bakefile/BakefileParser.java b/src/fr/monlouyan/bakefile/BakefileParser.java index 07b8d71..6f6b039 100644 --- a/src/fr/monlouyan/bakefile/BakefileParser.java +++ b/src/fr/monlouyan/bakefile/BakefileParser.java @@ -26,10 +26,9 @@ public class BakefileParser { /** * Regex pour détecter les targets et leurs dépendances. * Format : "nom1 nom2 nom3 : dépendance1 dépendance2" - * La nouvelle regex assure que la ligne ne commence pas par une tabulation - * et vérifie que le premier caractère non-espace n'est pas un symbole de commentaire + * La nouvelle regex gère plusieurs cibles séparées par des espaces */ - 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. @@ -87,71 +86,68 @@ public class BakefileParser { * @return La ligne combinée */ private String handleContinuationLines(List<String> lines, int startIndex) { - StringBuilder combinedLine = new StringBuilder(); - int i = startIndex; - - while (i < lines.size()) { - String line = lines.get(i); - Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); - - if (contMatcher.matches()) { - // Ajouter la ligne sans le backslash mais conserver le contenu entier - // Ne pas ajouter d'espace après certains opérateurs comme && - String content = contMatcher.group(1); - combinedLine.append(content); - - // Si la ligne ne se termine pas déjà par un opérateur tel que &&, ajouter un espace - if (!content.trim().endsWith("&&") && !content.trim().endsWith("|") && - !content.trim().endsWith(";")) { - combinedLine.append(" "); - } else { - // Si elle se termine par &&, |, ou ;, ajouter juste un espace après - combinedLine.append(" "); - } - i++; - } else { - // Ajouter la dernière ligne et sortir - combinedLine.append(line.trim()); - break; - } - } - - return combinedLine.toString(); - } + StringBuilder combinedLine = new StringBuilder(); + int i = startIndex; + + while (i < lines.size()) { + String line = lines.get(i); + Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); + + if (contMatcher.matches()) { + // Ajouter la ligne sans le backslash + combinedLine.append(contMatcher.group(1).trim()).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. - * 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 * @return Chaîne avec les variables remplacées */ - private String replaceVariables(String input) { - if (input == null) return null; - - String result = input; - - // Détecter et remplacer toutes les occurrences de variables - Matcher matcher = VARIABLE_REFERENCE.matcher(result); - StringBuffer sb = new StringBuffer(); - - while (matcher.find()) { - String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); - // Remplacer par la valeur de la variable si elle existe, sinon par une chaîne vide - String replacement = variables.containsKey(varName) ? variables.get(varName) : ""; - matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); - } - matcher.appendTail(sb); - result = sb.toString(); - - // Vérifier les références imbriquées de variables et continuer à remplacer si nécessaire - if (VARIABLE_REFERENCE.matcher(result).find()) { - result = replaceVariables(result); // Appel récursif pour gérer les variables imbriquées - } - - return result.trim(); - } + private String replaceVariables(String input) { + if (input == null) return null; + + String result = input; + Set<String> processedVars = new HashSet<>(); + boolean changed; + + do { + changed = false; + Matcher matcher = VARIABLE_REFERENCE.matcher(result); + StringBuffer sb = new StringBuffer(); + + while (matcher.find()) { + String varName = matcher.group(1) != null ? matcher.group(1) : matcher.group(2); + if (!processedVars.contains(varName) && variables.containsKey(varName)) { + String replacement = variables.get(varName); + matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement)); + changed = true; + processedVars.add(varName); + } + } + matcher.appendTail(sb); + result = sb.toString(); + + // 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. @@ -205,10 +201,9 @@ public class BakefileParser { public List<Rule> parse() { List<Rule> rules = new ArrayList<>(); Set<String> phonyTargets = new HashSet<>(); - List<String> allTargetNames = new ArrayList<>(); // Pour suivre l'ordre des cibles 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); } @@ -218,18 +213,6 @@ public class BakefileParser { List<String> dependencies = 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++) { String line = lines.get(i); @@ -260,7 +243,8 @@ public class BakefileParser { Matcher phonyMatcher = PHONY_PATTERN.matcher(line); if (phonyMatcher.matches()) { - // Déjà traité dans la première passe + String[] phonies = phonyMatcher.group(1).trim().split("\\s+"); + Collections.addAll(phonyTargets, phonies); continue; } @@ -283,8 +267,9 @@ public class BakefileParser { phonyTargets.contains(resolvedTarget) )); - // Enregistrer le nom de la cible pour suivre l'ordre - allTargetNames.add(resolvedTarget); + if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { + firstTarget = resolvedTarget; + } } } @@ -309,32 +294,15 @@ public class BakefileParser { phonyTargets.contains(resolvedTarget) )); - // Enregistrer le nom de la cible pour suivre l'ordre - allTargetNames.add(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; + if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { + firstTarget = resolvedTarget; } } - - // Si toutes les cibles sont PHONY, prendre simplement la première - if (firstTarget == null) { - firstTarget = allTargetNames.get(0); - } } } catch (IOException e) { e.printStackTrace(); } - return rules; } diff --git a/src/fr/monlouyan/bakefile/DependencyResolver.java b/src/fr/monlouyan/bakefile/DependencyResolver.java index eb01f0d..b2574b4 100644 --- a/src/fr/monlouyan/bakefile/DependencyResolver.java +++ b/src/fr/monlouyan/bakefile/DependencyResolver.java @@ -79,14 +79,6 @@ public class DependencyResolver { 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); if (rule != null) { rulesToBuild.add(rule); @@ -116,12 +108,6 @@ public class DependencyResolver { if (dep.startsWith("~")) { continue; } - - // Ignorer les options de compilation qui pourraient avoir un ':' dedans - if (dep.startsWith("-") || dep.contains(":")) { - continue; - } - topologicalSort(dep, processed, buildOrder); } @@ -162,11 +148,6 @@ public class DependencyResolver { continue; } - // Ignorer les options de compilation qui pourraient avoir un ':' dedans - if (dependency.startsWith("-") || dependency.contains(":")) { - continue; - } - if (ruleMap.containsKey(dependency)) { detectCycle(dependency, visited, stack, ruleName); } diff --git a/src/fr/monlouyan/bakefile/Rule.java b/src/fr/monlouyan/bakefile/Rule.java index 2ad047b..f2f3648 100644 --- a/src/fr/monlouyan/bakefile/Rule.java +++ b/src/fr/monlouyan/bakefile/Rule.java @@ -1,7 +1,6 @@ package fr.monlouyan.bakefile; import java.io.File; -import java.util.ArrayList; import java.util.List; /** @@ -74,171 +73,129 @@ public class Rule { * Vérifie si la règle doit être mise à jour. * @return true si la règle doit être mise à jour, false sinon */ - public boolean needsUpdate() { - if (BakeCLI.isDebug()){ - System.out.println("Debug : Checking if rule " + name + " needs update"); - } + public boolean needsUpdate() { + if (BakeCLI.isDebug()){ + 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 - 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; - } - - // Si le fichier cible n'existe pas et qu'il y a des commandes, il doit être mis à jour - File targetFile = new File(name); - if (!targetFile.exists() && !commands.isEmpty()) { - if (BakeCLI.isDebug()) { - System.out.println("Debug : Target file " + name + " does not exist and has commands, needs update"); - } - return true; - } - - // 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; - } - - // Ignorer les options de compilation qui pourraient avoir un ':' dedans - if (dependency.startsWith("-") || dependency.contains(":")) { - continue; - } - - File depFile = new File(dependency); - boolean hasRule = BakeEngine.hasRule(dependency); - - if (!depFile.exists() && !dependency.isEmpty() && !hasRule) { - // Vérifier si on est en situation de dépendance circulaire déjà traitée - boolean isPartOfCircularDependency = false; - - // Vérifier si cette dépendance est impliquée dans une relation circulaire - for (Rule rule : getAllRules()) { - if (rule.getName().equals(dependency) && rule.getDependencies().contains(name)) { - if (BakeCLI.isDebug()) { - System.out.println("Debug: Found circular dependency between " + name + " and " + dependency); - } - isPartOfCircularDependency = true; - break; - } - } - - if (isPartOfCircularDependency) { - if (BakeCLI.isDebug()) { - System.out.println("Debug: Ignoring circular dependency: " + dependency); - } - continue; - } - - System.out.println("bake: *** No rule to make target `" + dependency + "', needed by `" + name + "'. Stop."); - System.exit(2); - } - } - - // 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) { - // Skip dependencies with tilde in path or compiler options - if (dependency.startsWith("~") || dependency.startsWith("-") || dependency.contains(":")) { - continue; - } - - Rule depRule = BakeEngine.getRule(dependency); - if (depRule != null && depRule.needsUpdate()) { - if (BakeCLI.isDebug()) { - System.out.println("Debug : Dependency rule " + dependency + " needs update"); - } - return true; - } - } - return false; - } - - // Pour les règles avec des commandes, on vérifie aussi les timestamps - if (BakeCLI.isDebug()){ - 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; - } + // Si le fichier cible n'existe pas et qu'il y a des commandes, il doit être mis à jour + File targetFile = new File(name); + if (!targetFile.exists() && !commands.isEmpty()) { + if (BakeCLI.isDebug()) { + System.out.println("Debug : Target file " + name + " does not exist and has commands, needs update"); + } + return true; + } + + // 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) { + // Skip dependencies with tilde in path + if (dependency.startsWith("~")) { + continue; + } + + Rule depRule = BakeEngine.getRule(dependency); + if (depRule != null && depRule.needsUpdate()) { + if (BakeCLI.isDebug()) { + System.out.println("Debug : Dependency rule " + dependency + " needs update"); + } + return true; + } + } + return false; + } + + // Pour les règles avec des commandes, on vérifie aussi les timestamps + if (BakeCLI.isDebug()){ + 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 + if (dependency.startsWith("~")) { + 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; + } } \ No newline at end of file