| #!/usr/bin/env python3 |
| """ |
| Interactive wizard for configuring the LLVM Instrumentor pass. |
| |
| This script helps users create custom instrumentation configurations by: |
| 1. Generating a default config file using opt |
| 2. Presenting available instrumentation options interactively |
| 3. Allowing users to enable/disable specific instrumentation opportunities |
| 4. Saving the customized configuration to a JSON file |
| """ |
| |
| import argparse |
| import json |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| from typing import Dict, List, Any, Optional, Tuple |
| |
| |
| class InstrumentorConfigWizard: |
| def __init__(self, opt_path: str = None): |
| """Initialize the wizard with the path to opt.""" |
| self.opt_path = opt_path or self.find_opt() |
| self.config = {} |
| self.enabled_opportunities = set() |
| self.same_pre_post = True |
| self.navigation_stack = [] |
| |
| def find_opt(self) -> str: |
| """Find the opt binary in the build directory.""" |
| # Try common locations relative to this script |
| script_dir = os.path.dirname(os.path.abspath(__file__)) |
| repo_root = os.path.dirname(os.path.dirname(script_dir)) |
| |
| # Check build/bin/opt |
| opt_candidates = [ |
| os.path.join(repo_root, "build", "bin", "opt"), |
| os.path.join(repo_root, "build", "Debug", "bin", "opt"), |
| os.path.join(repo_root, "build", "Release", "bin", "opt"), |
| "opt", # Try system PATH |
| ] |
| |
| for candidate in opt_candidates: |
| if os.path.exists(candidate): |
| return candidate |
| # Check if it's in PATH |
| try: |
| subprocess.run( |
| [candidate, "--version"], capture_output=True, check=True, timeout=5 |
| ) |
| return candidate |
| except ( |
| subprocess.CalledProcessError, |
| FileNotFoundError, |
| subprocess.TimeoutExpired, |
| ): |
| continue |
| |
| raise FileNotFoundError( |
| "Could not find 'opt' binary. Please specify the path using --opt-path" |
| ) |
| |
| def generate_default_config(self) -> Dict[str, Any]: |
| """Generate a default configuration by running opt.""" |
| print(f"Generating default configuration using: {self.opt_path}") |
| |
| # Create a minimal LLVM IR module to trigger config generation |
| minimal_ir = """ |
| define i32 @main() { |
| %1 = alloca i32 |
| store i32 0, ptr %1 |
| %2 = load i32, ptr %1 |
| ret i32 %2 |
| } |
| """ |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| ir_file = os.path.join(tmpdir, "input.ll") |
| config_file = os.path.join(tmpdir, "config.json") |
| |
| # Write minimal IR |
| with open(ir_file, "w") as f: |
| f.write(minimal_ir) |
| |
| # Run opt with instrumentor to generate config |
| try: |
| cmd = [ |
| self.opt_path, |
| "-passes=instrumentor", |
| f"-instrumentor-write-config-file={config_file}", |
| "-disable-output", |
| ir_file, |
| ] |
| |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) |
| |
| if result.returncode != 0: |
| print( |
| f"Warning: opt returned non-zero exit code: {result.returncode}" |
| ) |
| if result.stderr: |
| print(f"stderr: {result.stderr}") |
| |
| # Read the generated config |
| if not os.path.exists(config_file): |
| raise FileNotFoundError( |
| f"Config file was not generated at {config_file}" |
| ) |
| |
| with open(config_file, "r") as f: |
| config = json.load(f) |
| |
| print("✓ Default configuration generated successfully\n") |
| return config |
| |
| except subprocess.TimeoutExpired: |
| raise RuntimeError("opt command timed out") |
| except Exception as e: |
| raise RuntimeError(f"Failed to generate config: {e}") |
| |
| def clear_screen(self): |
| """Clear the terminal screen.""" |
| os.system("clear" if os.name != "nt" else "cls") |
| |
| def print_section_header(self, title: str): |
| """Print a formatted section header.""" |
| print("\n" + "=" * 70) |
| print(f" {title}") |
| print("=" * 70) |
| |
| def print_option(self, index: int, name: str, description: str, enabled: bool): |
| """Print a formatted option.""" |
| status = "[X]" if enabled else "[ ]" |
| if description: |
| print(f" {index:2d}. {status} {name:30s} - {description}") |
| else: |
| print(f" {index:2d}. {status} {name}") |
| |
| def get_user_choice( |
| self, prompt: str, valid_choices: List[str] = None, allow_back: bool = True |
| ) -> Optional[str]: |
| """Get user input with validation.""" |
| while True: |
| try: |
| nav_hint = " (b=back, q=quit)" if allow_back else " (q=quit)" |
| choice = input(prompt + nav_hint + ": ").strip().lower() |
| |
| if choice == "q": |
| confirm = input("Really quit? (y/n): ").strip().lower() |
| if confirm == "y": |
| print("\nWizard cancelled by user.") |
| sys.exit(0) |
| continue |
| |
| if choice == "b" and allow_back: |
| return "BACK" |
| |
| if not choice: |
| return "" |
| |
| if valid_choices: |
| if choice in valid_choices: |
| return choice |
| print(f"Please enter one of: {', '.join(valid_choices)}") |
| else: |
| return choice |
| |
| except KeyboardInterrupt: |
| print("\n\nWizard interrupted by user.") |
| sys.exit(0) |
| |
| def get_location_names(self) -> List[str]: |
| """Extract all location names from the config, excluding 'configuration'.""" |
| locations = [] |
| for key in self.config.keys(): |
| if key != "configuration": |
| locations.append(key) |
| return locations |
| |
| def get_all_opportunity_types(self) -> List[Tuple[str, str]]: |
| """Extract all unique opportunity types from the config.""" |
| opportunities = [] |
| seen = set() |
| |
| for location in self.get_location_names(): |
| for opp_name in self.config[location].keys(): |
| if opp_name not in seen: |
| seen.add(opp_name) |
| # Get description from first occurrence |
| opp_config = self.config[location][opp_name] |
| desc = "" |
| |
| # Look for a "description" property (without dot) |
| if "description" in opp_config and opp_config["description"]: |
| desc = opp_config["description"] |
| |
| opportunities.append((opp_name, desc)) |
| |
| return sorted(opportunities) |
| |
| def select_opportunities(self) -> bool: |
| """Let user select which instrumentation opportunities to enable.""" |
| while True: |
| self.clear_screen() |
| self.print_section_header("Step 1: Select Instrumentation Types") |
| |
| opportunities = self.get_all_opportunity_types() |
| |
| print("\nSelect which types of instrumentation you want to configure:") |
| print( |
| "(You can toggle individual arguments for each type in the next steps)\n" |
| ) |
| |
| for idx, (opp_name, opp_desc) in enumerate(opportunities, 1): |
| enabled = opp_name in self.enabled_opportunities |
| self.print_option(idx, opp_name, opp_desc, enabled) |
| |
| print("\nCommands:") |
| print(" - Enter numbers (space-separated) to toggle opportunities") |
| print(" - 'all' to enable all, 'none' to disable all") |
| print(" - Press Enter when done to continue") |
| |
| choice = self.get_user_choice("\nYour choice", allow_back=False) |
| |
| if choice == "BACK": |
| continue |
| elif choice == "": |
| if not self.enabled_opportunities: |
| print("\n⚠ Please enable at least one instrumentation type!") |
| input("Press Enter to continue...") |
| continue |
| return True |
| elif choice == "all": |
| self.enabled_opportunities = {opp[0] for opp in opportunities} |
| elif choice == "none": |
| self.enabled_opportunities.clear() |
| else: |
| try: |
| indices = [int(x) for x in choice.split()] |
| for idx in indices: |
| if 1 <= idx <= len(opportunities): |
| opp_name = opportunities[idx - 1][0] |
| if opp_name in self.enabled_opportunities: |
| self.enabled_opportunities.remove(opp_name) |
| else: |
| self.enabled_opportunities.add(opp_name) |
| except ValueError: |
| print( |
| "\n⚠ Invalid input. Please enter numbers separated by spaces." |
| ) |
| input("Press Enter to continue...") |
| |
| def configure_pre_post_mode(self) -> bool: |
| """Ask if PRE and POST should have the same configuration.""" |
| while True: |
| self.clear_screen() |
| self.print_section_header("Step 2: PRE vs POST Configuration") |
| |
| print("\nInstrumentation can happen at two points:") |
| print(" - PRE: Before the instrumented operation") |
| print(" - POST: After the instrumented operation") |
| print("\nFor example, for a load instruction:") |
| print(" - PRE: Can inspect/modify the pointer before the load") |
| print(" - POST: Can inspect/modify the loaded value after the load") |
| |
| print( |
| f"\nCurrent mode: {'SAME configuration for PRE and POST' if self.same_pre_post else 'DIFFERENT configurations'}" |
| ) |
| |
| choice = self.get_user_choice( |
| "\nUse same configuration for PRE and POST? (y/n/Enter to keep)", |
| valid_choices=["y", "yes", "n", "no", ""], |
| ) |
| |
| if choice == "BACK": |
| return False |
| elif choice in ["y", "yes"]: |
| self.same_pre_post = True |
| return True |
| elif choice in ["n", "no"]: |
| self.same_pre_post = False |
| return True |
| elif choice == "": |
| return True |
| |
| def configure_base_options(self) -> bool: |
| """Configure base/global options.""" |
| while True: |
| self.clear_screen() |
| self.print_section_header("Step 3: Base Configuration") |
| |
| if "configuration" not in self.config: |
| self.config["configuration"] = {} |
| |
| base_config = self.config["configuration"] |
| |
| # Collect all configuration options (excluding descriptions and runtime_stubs_file) |
| config_options = [] |
| for key in sorted(base_config.keys()): |
| if not key.endswith(".description") and key != "runtime_stubs_file": |
| config_options.append(key) |
| |
| # Display current settings |
| print("\nCurrent settings:") |
| for idx, key in enumerate(config_options, 1): |
| value = base_config.get(key) |
| # Format value display |
| if value == "": |
| display_value = "(none)" |
| else: |
| display_value = value |
| print(f" {idx}. {key:30s} {display_value}") |
| |
| print("\nEnter option number to modify, or press Enter to continue") |
| choice = self.get_user_choice("Option") |
| |
| if choice == "BACK": |
| return False |
| elif choice == "": |
| return True |
| else: |
| # Try to parse as a number |
| try: |
| idx = int(choice) |
| if 1 <= idx <= len(config_options): |
| key = config_options[idx - 1] |
| current_value = base_config[key] |
| description = base_config.get(f"{key}.description", "") |
| |
| # Show description if available |
| if description: |
| print(f"\n{description}") |
| |
| # Handle different types |
| if isinstance(current_value, bool): |
| new_val = self.get_user_choice( |
| f"Set {key}? (y/n)", |
| ["y", "n"], |
| allow_back=False, |
| ) |
| if new_val: |
| base_config[key] = new_val == "y" |
| elif isinstance(current_value, str): |
| # Special handling for filter properties |
| if "filter" in key.lower(): |
| print( |
| f"Note: '{key}' requires a valid regular expression" |
| ) |
| new_val = input( |
| f"Enter new value for {key} (current: '{current_value}'): " |
| ).strip() |
| # Allow setting to empty string |
| base_config[key] = new_val |
| else: |
| print(f"Unsupported type for {key}: {type(current_value)}") |
| input("Press Enter to continue...") |
| else: |
| print(f"\n⚠ Invalid option number: {idx}") |
| input("Press Enter to continue...") |
| except ValueError: |
| print("\n⚠ Please enter a valid number") |
| input("Press Enter to continue...") |
| |
| def configure_opportunity_args( |
| self, opp_name: str, location: str, step_prefix: str = "Step 4" |
| ) -> bool: |
| """Configure arguments for a specific opportunity at a location.""" |
| while True: |
| self.clear_screen() |
| location_desc = "PRE (before)" if "pre" in location else "POST (after)" |
| self.print_section_header( |
| f"{step_prefix}: Configure {opp_name} - {location_desc}" |
| ) |
| |
| if location not in self.config or opp_name not in self.config[location]: |
| print(f"\n⚠ {opp_name} not found in {location}") |
| input("Press Enter to continue...") |
| return True |
| |
| opp_config = self.config[location][opp_name] |
| |
| # Show enable/disable status |
| enabled = opp_config.get("enabled", False) |
| print(f"\nInstrumentation: {'ENABLED ✓' if enabled else 'DISABLED ✗'}") |
| |
| # Collect arguments |
| args = [] |
| for key, value in sorted(opp_config.items()): |
| if ( |
| key == "enabled" |
| or key.endswith(".description") |
| or key.endswith(".replace") |
| ): |
| continue |
| desc = opp_config.get(f"{key}.description", "No description") |
| can_replace = f"{key}.replace" in opp_config |
| replace_enabled = ( |
| opp_config.get(f"{key}.replace", False) if can_replace else False |
| ) |
| args.append((key, value, desc, can_replace, replace_enabled)) |
| |
| if args: |
| print("\nAvailable arguments:") |
| for idx, ( |
| arg_name, |
| arg_enabled, |
| arg_desc, |
| can_replace, |
| replace_enabled, |
| ) in enumerate(args, 1): |
| status = "[X]" if arg_enabled else "[ ]" |
| if can_replace: |
| replace_status = "REPLACE" if replace_enabled else "observe" |
| replace_mark = f" [replaceable: {replace_status}]" |
| else: |
| replace_mark = "" |
| print( |
| f" {idx:2d}. {status} {arg_name:25s} - {arg_desc}{replace_mark}" |
| ) |
| |
| print("\nCommands:") |
| print(" - 'e' to toggle enabled/disabled") |
| print(" - Enter numbers (space-separated) to toggle arguments") |
| print( |
| " - 'r <num>' to toggle replacement for replaceable argument (e.g., 'r 1')" |
| ) |
| print(" - 'all' to enable all args, 'none' to disable all args") |
| print(" - Press Enter when done") |
| |
| choice = self.get_user_choice("\nYour choice") |
| |
| if choice == "BACK": |
| return False |
| elif choice == "": |
| return True |
| elif choice == "e": |
| opp_config["enabled"] = not opp_config["enabled"] |
| elif choice == "all": |
| for arg_name, _, _, _, _ in args: |
| opp_config[arg_name] = True |
| elif choice == "none": |
| for arg_name, _, _, _, _ in args: |
| opp_config[arg_name] = False |
| elif choice.startswith("r "): |
| # Toggle replacement |
| try: |
| parts = choice.split() |
| if len(parts) == 2: |
| idx = int(parts[1]) |
| if 1 <= idx <= len(args): |
| arg_name, _, _, can_replace, _ = args[idx - 1] |
| if can_replace: |
| replace_key = f"{arg_name}.replace" |
| opp_config[replace_key] = not opp_config.get( |
| replace_key, False |
| ) |
| else: |
| print(f"\n⚠ Argument '{arg_name}' is not replaceable.") |
| input("Press Enter to continue...") |
| else: |
| print(f"\n⚠ Invalid argument number: {idx}") |
| input("Press Enter to continue...") |
| else: |
| print("\n⚠ Usage: r <number>") |
| input("Press Enter to continue...") |
| except ValueError: |
| print("\n⚠ Invalid input for replacement toggle.") |
| input("Press Enter to continue...") |
| else: |
| try: |
| indices = [int(x) for x in choice.split()] |
| for idx in indices: |
| if 1 <= idx <= len(args): |
| arg_name = args[idx - 1][0] |
| opp_config[arg_name] = not opp_config[arg_name] |
| except ValueError: |
| print("\n⚠ Invalid input.") |
| input("Press Enter to continue...") |
| |
| def configure_locations(self) -> bool: |
| """Configure all enabled opportunities for PRE and optionally POST.""" |
| # First, disable all opportunities that are not in enabled_opportunities |
| for location in self.get_location_names(): |
| for opp_name, opp_config in self.config[location].items(): |
| if opp_name not in self.enabled_opportunities: |
| opp_config["enabled"] = False |
| |
| # Get all location names and separate into PRE and POST |
| all_locations = self.get_location_names() |
| pre_locations = [loc for loc in all_locations if loc.endswith("_pre")] |
| post_locations = [loc for loc in all_locations if loc.endswith("_post")] |
| |
| # Configure PRE locations |
| step_num = 4 |
| for idx, opp_name in enumerate(sorted(self.enabled_opportunities), 1): |
| # Try to find the opportunity in any PRE location |
| location = None |
| for loc in pre_locations: |
| if opp_name in self.config[loc]: |
| location = loc |
| break |
| |
| if location: |
| if not self.configure_opportunity_args(opp_name, location): |
| return False |
| |
| # If same config, copy PRE to POST |
| if self.same_pre_post: |
| for opp_name in self.enabled_opportunities: |
| # Copy from PRE to POST for each location pair |
| for pre_loc in pre_locations: |
| if opp_name not in self.config[pre_loc]: |
| continue |
| |
| # Find corresponding POST location (e.g., function_pre -> function_post) |
| base_name = pre_loc[:-4] # Remove "_pre" |
| post_loc = base_name + "_post" |
| |
| if post_loc in self.config and opp_name in self.config[post_loc]: |
| pre_config = self.config[pre_loc][opp_name] |
| post_config = self.config[post_loc][opp_name] |
| # Copy enabled and argument settings |
| post_config["enabled"] = pre_config.get("enabled", False) |
| for key in pre_config: |
| if not key.endswith(".description") and key != "enabled": |
| if key in post_config: |
| post_config[key] = pre_config[key] |
| else: |
| # Configure POST locations separately |
| for opp_name in sorted(self.enabled_opportunities): |
| # Try to find the opportunity in any POST location |
| location = None |
| for loc in post_locations: |
| if opp_name in self.config[loc]: |
| location = loc |
| break |
| |
| if location: |
| if not self.configure_opportunity_args(opp_name, location): |
| return False |
| |
| return True |
| |
| def generate_runtime_stubs(self, config_path: str, stub_path: str) -> bool: |
| """Generate runtime stub files (both .c/.cpp and .h) using the configuration.""" |
| print(f"\nGenerating runtime stubs using: {self.opt_path}") |
| |
| # Create a minimal LLVM IR module |
| minimal_ir = """ |
| define i32 @main() { |
| %1 = alloca i32 |
| store i32 0, ptr %1 |
| %2 = load i32, ptr %1 |
| ret i32 %2 |
| } |
| """ |
| |
| # Calculate header path |
| stub_base = stub_path.rsplit(".", 1)[0] if "." in stub_path else stub_path |
| stub_header_path = stub_base + ".h" |
| |
| with tempfile.TemporaryDirectory() as tmpdir: |
| ir_file = os.path.join(tmpdir, "input.ll") |
| temp_config = os.path.join(tmpdir, "temp_config.json") |
| |
| # Write minimal IR |
| with open(ir_file, "w") as f: |
| f.write(minimal_ir) |
| |
| # Create a temporary config with stub file set |
| temp_cfg = self.config.copy() |
| if "configuration" not in temp_cfg: |
| temp_cfg["configuration"] = {} |
| temp_cfg["configuration"]["runtime_stubs_file"] = stub_path |
| |
| with open(temp_config, "w") as f: |
| json.dump(temp_cfg, f, indent=2) |
| |
| # Run opt with instrumentor to generate stubs |
| try: |
| cmd = [ |
| self.opt_path, |
| "-passes=instrumentor", |
| f"-instrumentor-read-config-files={temp_config}", |
| "-disable-output", |
| ir_file, |
| ] |
| |
| result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) |
| |
| if result.returncode != 0: |
| print( |
| f"Warning: opt returned non-zero exit code: {result.returncode}" |
| ) |
| if result.stderr: |
| print(f"stderr: {result.stderr}") |
| |
| # Check if both stub files were generated |
| stub_exists = os.path.exists(stub_path) |
| header_exists = os.path.exists(stub_header_path) |
| |
| if stub_exists and header_exists: |
| print(f"✓ Runtime stub files generated:") |
| print(f" {stub_path}") |
| print(f" {stub_header_path}") |
| return True |
| elif stub_exists: |
| print(f"✓ Stub file generated: {stub_path}") |
| print(f"⚠ Header file was not generated: {stub_header_path}") |
| return True |
| else: |
| print(f"✗ Stub files were not generated") |
| return False |
| |
| except subprocess.TimeoutExpired: |
| print("✗ opt command timed out") |
| return False |
| except Exception as e: |
| print(f"✗ Failed to generate stubs: {e}") |
| return False |
| |
| def review_and_save(self, output_path: str) -> bool: |
| """Review configuration and save.""" |
| stub_path = None |
| |
| while True: |
| self.clear_screen() |
| self.print_section_header("Step 5: Review and Save") |
| |
| print("\nEnabled instrumentation types:") |
| for opp in sorted(self.enabled_opportunities): |
| print(f" ✓ {opp}") |
| |
| print( |
| f"\nPRE/POST mode: {'Same configuration' if self.same_pre_post else 'Different configurations'}" |
| ) |
| print( |
| f"Runtime prefix: {self.config.get('configuration', {}).get('runtime_prefix', '__instrumentor_')}" |
| ) |
| |
| # Extract directory and filenames |
| output_dir = os.path.dirname(os.path.abspath(output_path)) |
| output_filename = os.path.basename(output_path) |
| |
| if stub_path: |
| stub_dir = os.path.dirname(os.path.abspath(stub_path)) |
| stub_filename = os.path.basename(stub_path) |
| # Generate header filename by replacing extension with .h |
| stub_base = ( |
| stub_path.rsplit(".", 1)[0] if "." in stub_path else stub_path |
| ) |
| stub_header_path = stub_base + ".h" |
| stub_header_filename = os.path.basename(stub_header_path) |
| else: |
| default_stub = output_path.rsplit(".", 1)[0] + "_stubs.c" |
| stub_dir = os.path.dirname(os.path.abspath(default_stub)) |
| stub_filename = os.path.basename(default_stub) |
| stub_base = default_stub.rsplit(".", 1)[0] |
| stub_header_path = stub_base + ".h" |
| stub_header_filename = os.path.basename(stub_header_path) |
| |
| print("\n" + "-" * 70) |
| print("Output Location:") |
| print("-" * 70) |
| print(f"Directory: {output_dir}") |
| print(f"Config filename: {output_filename}") |
| print( |
| f"Stub filenames: {stub_filename}{' (not generated yet)' if not stub_path else ''}" |
| ) |
| print( |
| f" {stub_header_filename}{' (not generated yet)' if not stub_path else ''}" |
| ) |
| print(f"\nFull paths:") |
| print(f" Config: {output_path}") |
| if stub_path: |
| print(f" Stubs: {stub_path}") |
| print(f" {stub_header_path}") |
| else: |
| default_stub = output_path.rsplit(".", 1)[0] + "_stubs.c" |
| print(f" Stubs: {default_stub} (when generated)") |
| print(f" {stub_header_path} (when generated)") |
| |
| print("\nCommands:") |
| print(" - 's' to save configuration and finish") |
| print(" - 'g' to generate runtime stub file") |
| print(" - 'd' to change output directory") |
| print(" - 'c' to change configuration filename") |
| print(" - 't' to change stub filename") |
| print(" - 'b' to go back and modify settings") |
| |
| choice = self.get_user_choice( |
| "\nYour choice", valid_choices=["s", "g", "d", "c", "t", "b", ""] |
| ) |
| |
| if choice == "BACK" or choice == "b": |
| return False |
| elif choice == "s": |
| try: |
| # Create output directory if it doesn't exist |
| output_dir = os.path.dirname(os.path.abspath(output_path)) |
| if output_dir and not os.path.exists(output_dir): |
| os.makedirs(output_dir) |
| print(f"✓ Created directory: {output_dir}") |
| |
| # Remove runtime_stubs_file from config before saving |
| config_to_save = json.loads(json.dumps(self.config)) |
| if "configuration" in config_to_save: |
| config_to_save["configuration"].pop("runtime_stubs_file", None) |
| config_to_save["configuration"].pop( |
| "runtime_stubs_file.description", None |
| ) |
| |
| with open(output_path, "w") as f: |
| json.dump(config_to_save, f, indent=2) |
| print(f"\n✓ Configuration saved to: {output_path}") |
| |
| return True |
| except Exception as e: |
| print(f"\n✗ Failed to save configuration: {e}") |
| input("Press Enter to continue...") |
| elif choice == "d": |
| # Change output directory |
| current_dir = os.path.dirname(os.path.abspath(output_path)) |
| print(f"\nCurrent directory: {current_dir}") |
| new_dir = input("Enter new output directory: ").strip() |
| if new_dir: |
| # Expand user home directory if needed |
| new_dir = os.path.expanduser(new_dir) |
| new_dir = os.path.abspath(new_dir) |
| |
| # Update output_path with new directory |
| output_filename = os.path.basename(output_path) |
| output_path = os.path.join(new_dir, output_filename) |
| |
| # Update stub_path with new directory if it was set |
| if stub_path: |
| stub_filename = os.path.basename(stub_path) |
| stub_path = os.path.join(new_dir, stub_filename) |
| |
| print(f"✓ Output directory updated to: {new_dir}") |
| print(f" Config will be saved to: {output_path}") |
| if stub_path: |
| print(f" Stubs will be saved to: {stub_path}") |
| else: |
| print("Directory unchanged") |
| input("Press Enter to continue...") |
| elif choice == "c": |
| # Change configuration filename only |
| current_filename = os.path.basename(output_path) |
| current_dir = os.path.dirname(os.path.abspath(output_path)) |
| print(f"\nCurrent filename: {current_filename}") |
| new_filename = input("Enter new configuration filename: ").strip() |
| if new_filename: |
| output_path = os.path.join(current_dir, new_filename) |
| print(f"✓ Configuration filename updated to: {new_filename}") |
| print(f" Full path: {output_path}") |
| else: |
| print("Filename unchanged") |
| input("Press Enter to continue...") |
| elif choice == "t": |
| # Change stub filename only |
| current_dir = os.path.dirname(os.path.abspath(output_path)) |
| if stub_path: |
| current_stub_filename = os.path.basename(stub_path) |
| else: |
| default_stub = output_path.rsplit(".", 1)[0] + "_stubs.c" |
| current_stub_filename = os.path.basename(default_stub) |
| |
| print(f"\nCurrent stub filename: {current_stub_filename}") |
| new_stub_filename = input("Enter new stub filename: ").strip() |
| if new_stub_filename: |
| stub_path = os.path.join(current_dir, new_stub_filename) |
| print(f"✓ Stub filename updated to: {new_stub_filename}") |
| print(f" Full path: {stub_path}") |
| else: |
| print("Filename unchanged") |
| input("Press Enter to continue...") |
| elif choice == "g": |
| print("\nGenerate runtime stub files") |
| print("This creates C/C++ files with stub implementations of the") |
| print( |
| "instrumentation runtime functions that you can use as a template." |
| ) |
| print( |
| "Note: Both a .c/.cpp file and a .h header file will be generated." |
| ) |
| |
| # Use current stub_path or ask for one |
| if not stub_path: |
| default_stub = output_path.rsplit(".", 1)[0] + "_stubs.c" |
| stub_input = input( |
| f"\nStub file path (default: {default_stub}): " |
| ).strip() |
| stub_path = stub_input if stub_input else default_stub |
| |
| # Calculate header path |
| stub_base = ( |
| stub_path.rsplit(".", 1)[0] if "." in stub_path else stub_path |
| ) |
| stub_header_path = stub_base + ".h" |
| |
| # Generate stubs immediately |
| print(f"\nGenerating stub files:") |
| print(f" {stub_path}") |
| print(f" {stub_header_path}") |
| |
| # First save the config to a temporary location if not already saved |
| with tempfile.TemporaryDirectory() as tmpdir: |
| temp_config_path = os.path.join(tmpdir, "temp_config.json") |
| config_to_save = json.loads(json.dumps(self.config)) |
| if "configuration" in config_to_save: |
| config_to_save["configuration"].pop("runtime_stubs_file", None) |
| config_to_save["configuration"].pop( |
| "runtime_stubs_file.description", None |
| ) |
| |
| with open(temp_config_path, "w") as f: |
| json.dump(config_to_save, f, indent=2) |
| |
| if self.generate_runtime_stubs(temp_config_path, stub_path): |
| print(f"✓ Stub files generated successfully") |
| print(f" Created: {stub_path}") |
| print(f" Created: {stub_header_path}") |
| else: |
| print(f"✗ Failed to generate stub files") |
| |
| input("Press Enter to continue...") |
| elif choice == "": |
| continue |
| |
| def run_interactive(self, output_path: str): |
| """Run the interactive configuration wizard.""" |
| self.clear_screen() |
| print("=" * 70) |
| print(" LLVM Instrumentor Configuration Wizard") |
| print("=" * 70) |
| print( |
| "\nThis wizard will help you create a custom instrumentation configuration." |
| ) |
| print("You can enable/disable instrumentation opportunities and configure") |
| print("what information is passed to the runtime functions.") |
| print("\nNavigation: Use 'b' to go back, 'q' to quit at any prompt.") |
| input("\nPress Enter to continue...") |
| |
| # Generate or load config |
| try: |
| self.config = self.generate_default_config() |
| except Exception as e: |
| print(f"Error: {e}") |
| return False |
| |
| # State machine for navigation |
| state = 0 |
| while True: |
| if state == 0: # Select opportunities |
| if self.select_opportunities(): |
| state = 1 |
| elif state == 1: # PRE/POST mode |
| if self.configure_pre_post_mode(): |
| state = 2 |
| else: |
| state = 0 |
| elif state == 2: # Base configuration |
| if self.configure_base_options(): |
| state = 3 |
| else: |
| state = 1 |
| elif state == 3: # Configure locations |
| if self.configure_locations(): |
| state = 4 |
| else: |
| state = 2 |
| elif state == 4: # Review and save |
| if self.review_and_save(output_path): |
| return True |
| else: |
| state = 3 |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Interactive wizard for configuring LLVM Instrumentor pass", |
| formatter_class=argparse.RawDescriptionHelpFormatter, |
| epilog=""" |
| Examples: |
| # Interactive mode (recommended) |
| %(prog)s |
| |
| # Specify custom output location |
| %(prog)s -o my_config.json |
| |
| # Use specific opt binary |
| %(prog)s --opt-path /path/to/opt |
| |
| # Load existing config and modify it |
| %(prog)s --input existing_config.json -o modified_config.json |
| """, |
| ) |
| |
| parser.add_argument( |
| "-o", |
| "--output", |
| default="instrumentor_config.json", |
| help="Output configuration file (default: instrumentor_config.json)", |
| ) |
| |
| parser.add_argument( |
| "--opt-path", help="Path to the opt binary (default: auto-detect)" |
| ) |
| |
| parser.add_argument( |
| "--input", help="Load and modify an existing configuration file" |
| ) |
| |
| args = parser.parse_args() |
| |
| try: |
| wizard = InstrumentorConfigWizard(opt_path=args.opt_path) |
| |
| # Load existing config if provided |
| if args.input: |
| print(f"Loading existing configuration from: {args.input}") |
| with open(args.input, "r") as f: |
| wizard.config = json.load(f) |
| print("✓ Configuration loaded\n") |
| # Extract enabled opportunities from loaded config |
| for location in wizard.get_location_names(): |
| for opp_name, opp_config in wizard.config[location].items(): |
| if opp_config.get("enabled", False): |
| wizard.enabled_opportunities.add(opp_name) |
| |
| success = wizard.run_interactive(args.output) |
| |
| if success: |
| print("\n" + "=" * 70) |
| print("Configuration complete!") |
| print("=" * 70) |
| print(f"\nTo use this configuration with opt:") |
| print(f" opt -passes=instrumentor \\") |
| print(f" -instrumentor-read-config-files={args.output} \\") |
| print(f" input.ll -S -o output.ll") |
| print(f"\nTo use with clang:") |
| print(f" clang -mllvm -enable-instrumentor \\") |
| print(f" -mllvm -instrumentor-read-config-files={args.output} \\") |
| print(f" input.c -o output") |
| return 0 |
| else: |
| return 1 |
| |
| except Exception as e: |
| print(f"\nFatal error: {e}", file=sys.stderr) |
| import traceback |
| |
| traceback.print_exc() |
| return 1 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(main()) |