package fr.monlouyan.bakefile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; /** * Parseur de fichier Bakefile. * Cette classe est responsable de l'analyse syntaxique du fichier Bakefile * pour extraire les règles de build, les dépendances et les commandes associées. * * @author Moncef STITI, Yanis HAMOUDI, Louay DARDOURI * @version 1.0 */ public class BakefileParser { /** * Nom du fichier Bakefile à parser (donc Bakefile...). */ private String filename; /** * 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 */ private static final Pattern TARGET_PATTERN = Pattern.compile("^([^\\t:#][^:#]+?)\\s*:\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les lignes de commande associées à une target. * Format : " gcc -o program program.c" (ligne indentée) */ private static final Pattern COMMAND_PATTERN = Pattern.compile("^\\t(.+)$"); /** * Regex pour détecter les définitions de variables. * Format : "FLAGS = -ansi -pedantic" */ private static final Pattern VARIABLE_PATTERN = Pattern.compile("^(\\w+)\\s*=\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les déclarations .PHONY * Format : ".PHONY: clean all" */ private static final Pattern PHONY_PATTERN = Pattern.compile("^\\.PHONY:\\s*([^#]*?)\\s*(?:#.*)?$"); /** * Regex pour détecter les lignes de continuation. * Format : " gcc -o program program.c \" */ private static final Pattern CONTINUATION_PATTERN = Pattern.compile("^(.*)\\\\\\s*$"); /** * Regex pour détecter les références de variables. * Format : "${VAR}" ou "$(VAR)" */ private static final Pattern VARIABLE_REFERENCE = Pattern.compile("\\$\\{(\\w+)\\}|\\$\\((\\w+)\\)"); /** * Première cible trouvée dans le fichier Bakefile. */ private static String firstTarget; /** * Stocke les variables définies dans le Bakefile. */ private Map variables = new HashMap<>(); /** * Constructeur de la classe BakefileParser. * @param filename Nom du fichier Bakefile à parser */ public BakefileParser(String filename) { this.filename = filename; firstTarget = null; } /** * Gérer les lignes de continuation. * @param lines Liste des lignes du fichier Bakefile * @param startIndex Index de la première ligne de continuation * @return La ligne combinée */ private String handleContinuationLines(List 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 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. * @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; Set 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. * @param items Liste de chaînes à traiter * @return Liste de chaînes avec les variables remplacées */ private List replaceVariablesInList(List items) { return items.stream() .map(this::replaceVariables) .collect(Collectors.toList()); } /** * Découper les dépendances en une liste de chaînes. * @param depStr Chaîne de dépendances * @return Liste de dépendances */ private List splitDependencies(String depStr) { if (depStr == null || depStr.trim().isEmpty()) { return new ArrayList<>(); } String resolvedStr = replaceVariables(depStr.trim()); return Arrays.stream(resolvedStr.split("\\s+")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } /** * Découper les cibles en une liste de chaînes. * @param targetStr Chaîne de cibles * @return Liste de cibles */ private List splitTargets(String targetStr) { if (targetStr == null || targetStr.trim().isEmpty()) { return new ArrayList<>(); } String resolvedStr = replaceVariables(targetStr.trim()); return Arrays.stream(resolvedStr.split("\\s+")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } /** * Analyser le fichier Bakefile pour extraire les règles de build. * @return Liste des règles extraites */ public List parse() { List rules = new ArrayList<>(); Set phonyTargets = new HashSet<>(); if (!Files.exists(Paths.get(filename))) { System.out.println("*** No targets specified and no bakefile found. Stop."); System.exit(2); } try { List lines = Files.readAllLines(Paths.get(filename)); List currentTargets = null; List dependencies = new ArrayList<>(); List commands = new ArrayList<>(); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); // Vérifier si la ligne a un caractère de continuation Matcher contMatcher = CONTINUATION_PATTERN.matcher(line); if (contMatcher.matches()) { // Récupérer toute la définition multi-ligne line = handleContinuationLines(lines, i); // Ajuster i pour sauter les lignes traitées while (i + 1 < lines.size() && CONTINUATION_PATTERN.matcher(lines.get(i)).matches()) { i++; } } if (line.trim().isEmpty()) { continue; } if (line.matches("^ +.*$")) { System.err.println(filename + ":" + (i+1) + ": *** missing separator. Stop."); System.exit(2); } Matcher varMatcher = VARIABLE_PATTERN.matcher(line); Matcher targetMatcher = TARGET_PATTERN.matcher(line); Matcher commandMatcher = COMMAND_PATTERN.matcher(line); Matcher phonyMatcher = PHONY_PATTERN.matcher(line); if (phonyMatcher.matches()) { String[] phonies = phonyMatcher.group(1).trim().split("\\s+"); Collections.addAll(phonyTargets, phonies); continue; } if (varMatcher.matches()) { String varName = varMatcher.group(1); String varValue = varMatcher.group(2).trim(); // Évaluer les variables référencées dans la valeur varValue = replaceVariables(varValue); variables.put(varName, varValue); } else if (targetMatcher.matches()) { if (currentTargets != null) { // Créer une règle pour chaque cible avec les mêmes dépendances et commandes for (String target : currentTargets) { String resolvedTarget = replaceVariables(target.trim()); rules.add(new Rule( resolvedTarget, splitDependencies(dependencies.stream() .collect(Collectors.joining(" "))), replaceVariablesInList(commands), phonyTargets.contains(resolvedTarget) )); if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { firstTarget = resolvedTarget; } } } String targetStr = targetMatcher.group(1); currentTargets = splitTargets(targetStr); String depStr = targetMatcher.group(2); dependencies = splitDependencies(depStr); commands = new ArrayList<>(); } else if (commandMatcher.matches()) { commands.add(commandMatcher.group(1)); } } if (currentTargets != null) { // Créer une règle pour chaque cible avec les mêmes dépendances et commandes for (String target : currentTargets) { String resolvedTarget = replaceVariables(target.trim()); rules.add(new Rule( resolvedTarget, replaceVariablesInList(dependencies), replaceVariablesInList(commands), phonyTargets.contains(resolvedTarget) )); if (firstTarget == null && !phonyTargets.contains(resolvedTarget)) { firstTarget = resolvedTarget; } } } } catch (IOException e) { e.printStackTrace(); } return rules; } /** * Récupérer la première cible * @return String la première cible */ public static String getFirstTarget() { return firstTarget; } }