212 lines
6.2 KiB
Python
212 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
[fr] Script pour prédire quels fichiers C++ de LibreOffice seront recompilés
|
||
après la modification d’un fichier source ou header.
|
||
Inclut un mode "estimation rapide".
|
||
|
||
[en] Script to predict which LibreOffice C++ source files will be recompiled
|
||
after a source or header file is modified.
|
||
Includes a "quick estimation" mode.
|
||
|
||
Usage:
|
||
./predict_rebuild.py path/to/modified_file
|
||
./predict_rebuild.py --quick path/to/modified_file
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import re
|
||
from collections import defaultdict, deque
|
||
|
||
# [fr] Racine du dépôt LibreOffice (par défaut le répertoire courant)
|
||
# [en] Root of the LibreOffice source tree (default: current directory)
|
||
ROOT = "."
|
||
|
||
# [fr] Expression régulière pour extraire les directives #include
|
||
# [en] Regular expression to extract #include directives
|
||
include_re = re.compile(r'#\s*include\s*[<"]([^">]+)[">]')
|
||
|
||
|
||
def find_source_files():
|
||
"""
|
||
[fr] Recherche récursivement tous les fichiers sources pertinents
|
||
(.cxx, .hxx, .hpp, .h) dans l’arborescence.
|
||
|
||
[en] Recursively finds all relevant source files
|
||
(.cxx, .hxx, .hpp, .h) in the source tree.
|
||
"""
|
||
files = []
|
||
for root, _, filenames in os.walk(ROOT):
|
||
for name in filenames:
|
||
if name.endswith((".cxx", ".hxx", ".hpp", ".h")):
|
||
files.append(os.path.join(root, name))
|
||
return files
|
||
|
||
|
||
def parse_includes(filepath):
|
||
"""
|
||
[fr] Extrait toutes les directives #include d’un fichier source.
|
||
[en] Extracts all #include directives from a source file.
|
||
"""
|
||
includes = []
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
||
for line in f:
|
||
match = include_re.search(line)
|
||
if match:
|
||
includes.append(match.group(1))
|
||
except OSError:
|
||
pass
|
||
return includes
|
||
|
||
|
||
def build_dependency_graph(files):
|
||
"""
|
||
[fr] Construit un graphe de dépendances (forward + reverse).
|
||
[en] Builds a dependency graph (forward + reverse).
|
||
"""
|
||
forward = defaultdict(set)
|
||
reverse = defaultdict(set)
|
||
|
||
# [fr] Association "nom de fichier" -> "chemin complet"
|
||
# [en] Map "filename" -> "full path"
|
||
file_map = {}
|
||
for f in files:
|
||
file_map[os.path.basename(f)] = f
|
||
|
||
for f in files:
|
||
includes = parse_includes(f)
|
||
for inc in includes:
|
||
inc_name = os.path.basename(inc)
|
||
if inc_name in file_map:
|
||
target = file_map[inc_name]
|
||
forward[f].add(target)
|
||
reverse[target].add(f)
|
||
|
||
return forward, reverse
|
||
|
||
|
||
def compute_affected(modified_file, reverse_graph):
|
||
"""
|
||
[fr] Calcule tous les fichiers impactés via le graphe inverse.
|
||
[en] Computes all affected files using the reverse graph.
|
||
"""
|
||
affected = set()
|
||
queue = deque([modified_file])
|
||
|
||
while queue:
|
||
current = queue.popleft()
|
||
for parent in reverse_graph.get(current, []):
|
||
if parent not in affected:
|
||
affected.add(parent)
|
||
queue.append(parent)
|
||
|
||
return affected
|
||
|
||
|
||
def group_by_module(files):
|
||
"""
|
||
[fr] Regroupe les fichiers par module LibreOffice.
|
||
[en] Groups files by LibreOffice module.
|
||
"""
|
||
modules = defaultdict(list)
|
||
for f in files:
|
||
rel = os.path.relpath(f, ROOT)
|
||
module = rel.split(os.sep)[0]
|
||
modules[module].append(f)
|
||
return modules
|
||
|
||
|
||
def quick_estimation(modified_file, to_recompile, modules):
|
||
"""
|
||
[fr] Fournit une estimation rapide de l’impact du rebuild.
|
||
[en] Provides a quick estimation of rebuild impact.
|
||
|
||
Heuristique:
|
||
- FAIBLE: <=3 fichiers, 1 module
|
||
- MOYEN: 4–30 fichiers ou >=2 modules
|
||
- MASSIF: >30 fichiers ou fichier modifié dans 'include/'
|
||
"""
|
||
num_files = len(to_recompile)
|
||
num_modules = len(modules)
|
||
|
||
# [fr] Cas critique : modification dans include/
|
||
# [en] Critical case: modification in include/
|
||
if os.path.normpath(modified_file).startswith(os.path.join(ROOT, "include")):
|
||
return "🔴 MASSIF", "Header global (include/) modifié"
|
||
|
||
if num_files <= 3 and num_modules == 1:
|
||
return "🟢 FAIBLE", "Modification locale"
|
||
|
||
if num_files <= 30 and num_modules <= 3:
|
||
return "🟡 MOYEN", "Plusieurs fichiers ou modules impactés"
|
||
|
||
return "🔴 MASSIF", "Nombre élevé de fichiers ou modules"
|
||
|
||
|
||
def main():
|
||
"""
|
||
[fr] Point d’entrée principal du script.
|
||
[en] Main entry point of the script.
|
||
"""
|
||
quick_mode = False
|
||
|
||
if len(sys.argv) == 3 and sys.argv[1] == "--quick":
|
||
quick_mode = True
|
||
modified_arg = sys.argv[2]
|
||
elif len(sys.argv) == 2:
|
||
modified_arg = sys.argv[1]
|
||
else:
|
||
print("Usage: predict_rebuild.py [--quick] path/to/modified_file")
|
||
sys.exit(1)
|
||
|
||
modified = os.path.abspath(modified_arg)
|
||
|
||
if not os.path.exists(modified):
|
||
print(f"File not found: {modified}")
|
||
sys.exit(1)
|
||
|
||
print("🔍 Scanning source tree...")
|
||
files = find_source_files()
|
||
|
||
print("🧩 Building dependency graph...")
|
||
_, reverse = build_dependency_graph(files)
|
||
|
||
print(f"✏️ Modified file: {modified}")
|
||
|
||
affected = compute_affected(modified, reverse)
|
||
|
||
# [fr] Seuls les .cxx sont recompilés
|
||
# [en] Only .cxx files are recompiled
|
||
to_recompile = sorted(f for f in affected if f.endswith(".cxx"))
|
||
|
||
modules = group_by_module(to_recompile)
|
||
|
||
if quick_mode:
|
||
# [fr] Mode estimation rapide
|
||
# [en] Quick estimation mode
|
||
level, reason = quick_estimation(modified, to_recompile, modules)
|
||
print("\n⚡ Estimation rapide / Quick estimation:")
|
||
print(f" Niveau / Level : {level}")
|
||
print(f" Raison / Reason: {reason}")
|
||
print(f" Fichiers / Files : {len(to_recompile)}")
|
||
print(f" Modules : {len(modules)}")
|
||
return
|
||
|
||
# [fr] Mode détaillé (par défaut)
|
||
# [en] Detailed mode (default)
|
||
print("\n📦 Files likely to be recompiled:")
|
||
for f in to_recompile:
|
||
print(" ", f)
|
||
|
||
print("\n🗂 Modules affected:")
|
||
for m, fs in sorted(modules.items()):
|
||
print(f" {m}: {len(fs)} file(s)")
|
||
|
||
print(f"\n✅ Total: {len(to_recompile)} source file(s)")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|