package fr.monlouyan.bakefile; import java.io.File; import java.io.IOException; import java.util.List; /** * Exécuteur des commandes définies dans les règles. * Cette classe est responsable de l'exécution des commandes définies dans les règles * du fichier Bakefile. * * @author Moncef STITI, Yanis HAMOUDI, Louay DARDOURI * @version 1.0 */ public class CommandExecutor { /** * true si le mode debug est activé, false sinon */ private boolean debug; /** * Pour tracker si quelque chose doit être mis à jour */ private boolean needsUpdate = false; /** * Pour tracker si un cycle a été détecté */ private boolean isCircular = false; /** * Pour détecter les timestamps dans le futur */ private boolean futureTimestampDetected = false; /** * Constructeur de la classe CommandExecutor. * @param debug true si le mode debug est activé, false sinon * @param isCircular true si on est en mode circulaire, false sinon */ public CommandExecutor(boolean debug, boolean isCircular) { this.debug = debug; this.isCircular = isCircular; } /** * Exécute les commandes d'une règle. * @param rule La règle à exécuter */ public void execute(Rule rule) { // On vérifie d'abord si cette règle a besoin d'être mise à jour boolean ruleNeedsUpdate = rule.needsUpdate(); if (ruleNeedsUpdate) { needsUpdate = true; // Au moins une règle doit être mise à jour // Vérifier les timestamps des dépendances pour détecter ceux dans le futur for (String dependency : rule.getDependencies()) { if (dependency.startsWith("~")) { continue; } File depFile = new File(dependency); if (depFile.exists() && TimestampManager.getTimestamp(depFile) > System.currentTimeMillis()) { futureTimestampDetected = true; } } } // Si la règle a besoin d'être mise à jour et qu'il n'y a pas de commandes if (ruleNeedsUpdate && rule.getCommands().isEmpty()) { return; // On ne fait rien mais on ne montre pas de message } List<String> executableCommands = rule.getCommands(); List<List<String>> displayCommands = rule.getDisplayCommands(); for (int i = 0; i < executableCommands.size(); i++) { String command = executableCommands.get(i); List<String> displayLines = (i < displayCommands.size()) ? displayCommands.get(i) : null; // Vérifier si la commande commence par @ (ne pas afficher la commande) boolean silent = command.startsWith("@"); // Enlever le @ si présent pour exécuter la commande correctement String actualCommand = silent ? command.substring(1) : command; boolean ignoreErrors = actualCommand.startsWith("-"); if(ignoreErrors){ actualCommand = actualCommand.substring(1).trim(); } if (isCircular){ if (!silent && displayLines != null && !displayLines.isEmpty()) { boolean isFirstLine = true; for (String line : displayLines) { if (isFirstLine) { // Pour la première ligne, supprimer l'indentation if (line.startsWith("\t")) { System.out.println(line.substring(1)); } else { System.out.println(line); } isFirstLine = false; } else { // Pour les lignes suivantes, conserver l'indentation System.out.println(line); } } } } // On n'exécute que si nécessaire if (ruleNeedsUpdate) { try { if(!isCircular && !silent){ // Afficher les lignes formatées avec traitement spécial pour les continuations if (displayLines != null && !displayLines.isEmpty()) { boolean isFirstLine = true; for (String line : displayLines) { if (isFirstLine) { // Pour la première ligne, toujours supprimer l'indentation if (line.startsWith("\t")) { System.out.println(line.substring(1)); } else { System.out.println(line); } isFirstLine = false; } else { // Pour les lignes suivantes d'une continuation, conserver l'indentation System.out.println(line); } } } else { // Cas d'une commande simple (une seule ligne) if (command.startsWith("\t")) { System.out.println(command.substring(1)); } else { System.out.println(command); } } } if (debug) System.out.println("Debug: Executing " + actualCommand); ProcessBuilder pb = new ProcessBuilder("sh", "-c", actualCommand); pb.inheritIO(); Process process = pb.start(); // Attendre la fin du processus int exitCode = process.waitFor(); if (exitCode != 0) { if (ignoreErrors) { System.err.println("bake: [" + rule.getName() + "] Error " + exitCode + " (ignored)"); } else { System.err.println("bake: *** [" + rule.getName() + "] Error " + exitCode); System.exit(2); } } else if (exitCode != 0 && ignoreErrors && debug) { System.out.println("Debug: Command failed with exit code " + exitCode + ", but errors are ignored"); } } catch (IOException | InterruptedException e) { if(!ignoreErrors){ e.printStackTrace(); System.exit(2); } else if(debug){ System.out.println("Debug: Command execution failed, but errors are ignored"); e.printStackTrace(); } } } } // Si on a détecté des timestamps dans le futur, afficher un avertissement à la fin (comme make) if (futureTimestampDetected && !isCircular) { System.out.println("bake: warning: Clock skew detected. Your build may be incomplete."); } // Vérifier si cette règle est une cible directement demandée par l'utilisateur boolean isRequestedTarget = BakeCLI.getTargets().contains(rule.getName()) || (BakeCLI.getTargets().isEmpty() && rule.getName().equals(BakefileParser.getFirstTarget())); if (!isRequestedTarget || needsUpdate) { // Si ce n'est pas une cible demandée ou si une mise à jour a été nécessaire, // ne pas afficher de message return; } // Vérifier si la cible existe en tant que fichier File targetFile = new File(rule.getName()); boolean fileExists = targetFile.exists(); if (!rule.getCommands().isEmpty() && fileExists) { // Si la cible a des commandes et existe en tant que fichier, // afficher "up to date" System.out.println("bake: `" + rule.getName() + "' is up to date."); } else { // Si la cible n'a pas de commandes ou n'existe pas en tant que fichier, // afficher "Nothing to be done" System.out.println("bake: Nothing to be done for `" + rule.getName() + "'."); } } }