| #!/usr/bin/env python3 |
| # Copyright 2025 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """ |
| A wrapper for autoninja that automatically recompiles when files change. |
| |
| This script monitors the current directory and its subdirectories for file |
| changes by periodically checking file modification times. When a change is |
| detected, it automatically triggers a build using `autoninja` with the |
| arguments provided to this script. |
| |
| The build output is logged to `ininja.logs` in the specified output |
| directory. |
| |
| Two common scenarios: |
| 1. Regular local development |
| Example usage: |
| ``` |
| ./tools/ininja/ininja.py -C out/default chrome |
| ``` |
| |
| 2. Gemini CLI integration |
| When Gemini CLI makes changes, it will check the compile failures in ininja.logs |
| and try to fix compile failures. See the following instructions for how to |
| prompt Gemini CLI to do this. |
| Example usage: |
| * In terminal 1, run |
| ``` |
| ./tools/ininja/ininja.py -C out/default chrome -quiet |
| ``` |
| You need `-quiet` to only output error logs to save Gemini tokens. Or else you |
| may hit quota error quickly. If you don't use `-quiet`, you need to change the |
| prompt below to check more lines for error logs. |
| |
| * In terminal 2, run Gemini CLI. Then use this prompt: |
| |
| "After you make changes, always check <$pwd>/out/linux/ininja.logs file to |
| verify your change. The file is a log file for auto re-compile system. After |
| you make changes, mark the unix timestamp as 'Change timestamp'. The logs |
| contain build finish line '--- Build finished at <unix build timestamp>' as |
| 'Build timestamp'. If the 'Change timestamp' is newer than the |
| 'Build timestamp', wait for another 10 seconds and try again. If you see |
| 'Gemini CLI should stop waiting', then you should stop waiting. The compile |
| can take up to 5 minutes so be patient. Don't run any compile within Gemini |
| CLI. You should make sure the 'Change timestamp' is earlier than the |
| 'Build timestamp'. If there is a compile failure, try to fix it and check |
| the logs again. Only fix the files with errors to save time. When getting |
| compile errors, only check the last 50 lines to save Gemini tokens. Also |
| only look for errors after the latest '--- Build started at'. All previous |
| logs are stale. When the user asks to 'fix compile failures', check the logs |
| again with the same rule. The compile failures might be caused by user |
| changes, not generated by Gemini." |
| |
| * Then you can interact with Gemini CLI to make changes and Gemini CLI will |
| correct itself. When you make local changes out of Gemini CLI, you can also |
| ask Gemini CLI to 'fix compile failures'. And it should read `ininja.logs` and |
| fix it. |
| """ |
| |
| import sys |
| import os |
| import subprocess |
| import time |
| |
| # --- Configuration --- |
| # Directory to watch ('.' for current directory) |
| WATCH_PATH = '.' |
| # Directory to exclude from watching |
| EXCLUDE_DIR = os.path.join(WATCH_PATH, 'out') |
| # Time to wait between checking for file changes, in seconds |
| POLL_INTERVAL = 2 |
| # Log file name |
| LOG_FILE_NAME = 'ininja.logs' |
| # --- End Configuration --- |
| |
| |
| def get_changed_file_states(): |
| """ |
| Uses git to find changed/untracked files and returns their mtimes. |
| This is a hybrid approach that is both fast and accurate. A change |
| is detected if the list of files changes or if any of their |
| modification times change. |
| """ |
| try: |
| result = subprocess.run( |
| ['git', 'status', '-z', '--porcelain', '--untracked-files=all'], |
| capture_output=True, |
| text=True, |
| encoding='utf-8', |
| check=True) |
| except (subprocess.CalledProcessError, FileNotFoundError) as e: |
| print(f"Error running 'git status': {e}") |
| print("Please ensure you are in a git repository.") |
| sys.exit(1) |
| |
| file_states = {} |
| # Handle the case where there are no changes. |
| if not result.stdout.strip(): |
| return file_states |
| |
| # The output of `git status -z --porcelain`` gives lines like: |
| # 'R newfile\0oldfile\0 M path/to/modified/file.py\0 |
| # ?? path/to/untracked/file.py' |
| # We need to reliably extract the path. |
| for line in result.stdout.split('\0'): |
| parts = line.split() |
| if len(parts) < 2: |
| continue |
| filepath = parts[1] |
| |
| try: |
| file_states[filepath] = os.path.getmtime(filepath) |
| except OSError: |
| # File was likely deleted. We'll mark it with a special |
| # timestamp so the change is detected. |
| file_states[filepath] = -1 |
| return file_states |
| |
| |
| def run_ninja(ninja_args, log_path): |
| """ |
| Executes the autoninja command with the provided arguments and logs output. |
| """ |
| print("-> Kicking off ninja build...") |
| command = ['autoninja'] + ninja_args |
| |
| start_time = int(time.time()) |
| start_message = f"--- Build started at {start_time} ---" |
| print(start_message) |
| return_code = -1 |
| |
| try: |
| # Open the log file in write mode 'w' to clear it for the new build. |
| with open(log_path, 'w', encoding='utf-8') as log_file: |
| log_file.write(start_message + '\n') |
| |
| # Use Popen to stream output in real-time. |
| process = subprocess.Popen(command, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True, |
| encoding='utf-8') |
| |
| for line in iter(process.stdout.readline, ''): |
| print(line, end='') |
| log_file.write(line) |
| |
| return_code = process.wait() |
| |
| except FileNotFoundError: |
| print("Error: 'autoninja' command not found.") |
| print("Is depot_tools in your PATH?") |
| sys.exit(1) |
| except Exception as e: |
| print(f"An error occurred while running autoninja: {e}") |
| print("Gemini CLI should stop waiting if you are using it.") |
| sys.exit(1) |
| |
| finish_time = int(time.time()) |
| finish_message = (f"--- Build finished at {finish_time} " |
| f"with exit code {return_code} ---") |
| print(finish_message) |
| |
| # Re-open in append mode to add the finish message. |
| with open(log_path, 'a', encoding='utf-8') as log_file: |
| log_file.write(finish_message + '\n\n') |
| |
| if return_code == 0: |
| print("\nā
Build successful.") |
| else: |
| print(f"\nā Build failed with exit code {return_code}.") |
| |
| |
| def main(): |
| """Main function to set up the file watcher and run the initial build.""" |
| # Use a basic approach to find the output directory |
| # to avoid adding too much complexity with argparse for this simple case. |
| output_dir = None |
| ninja_args = sys.argv[1:] |
| |
| for i, arg in enumerate(ninja_args): |
| if arg == '-C' and i + 1 < len(ninja_args): |
| output_dir = ninja_args[i + 1] |
| break |
| elif arg.startswith('-C'): |
| output_dir = arg[2:] |
| break |
| |
| if not ninja_args or output_dir is None: |
| print("Usage: ./tools/ininja/ininja.py -C <output_directory> " |
| "target1 target2 ...") |
| print("Example: ./tools/ininja/ininja.py -C out/default chrome") |
| return |
| |
| log_path = os.path.join(output_dir, LOG_FILE_NAME) |
| print(f"š Logging build output to {log_path}") |
| |
| # Perform the initial build. |
| print("ā¶ļø Starting initial build...") |
| run_ninja(ninja_args, log_path) |
| |
| # Get the initial state of the files. |
| previous_states = get_changed_file_states() |
| print(f"š Watching for file changes (polling every {POLL_INTERVAL}s)...") |
| |
| # Start polling for changes. |
| try: |
| while True: |
| time.sleep(POLL_INTERVAL) |
| current_states = get_changed_file_states() |
| |
| if current_states != previous_states: |
| print("\n-> Change detected.") |
| run_ninja(ninja_args, log_path) |
| print( |
| f"\nš Watching for file changes (polling every {POLL_INTERVAL}s)..." |
| ) |
| previous_states = current_states |
| |
| except KeyboardInterrupt: |
| print("\nš Watcher stopped. Exiting.") |
| |
| |
| if __name__ == '__main__': |
| main() |