#!/usr/bin/env python3 """ Generate a Cargo.toml file from MODULE.bazel crate specifications. This is purely for IDE support - Bazel still handles the actual builds. """ import re import sys from pathlib import Path import fnmatch def load_gitignore_patterns(): """Load .gitignore patterns if available.""" gitignore_file = Path(".gitignore") patterns = [ # Default patterns to ignore "target/", ".git/", "bazel-*", "*.pyc", "__pycache__/", "node_modules/", ".DS_Store" ] if gitignore_file.exists(): content = gitignore_file.read_text() for line in content.splitlines(): line = line.strip() if line and not line.startswith('#'): patterns.append(line) return patterns def should_ignore(path, ignore_patterns): """Check if a path should be ignored based on gitignore-style patterns.""" path_str = str(path) for pattern in ignore_patterns: if fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(path.name, pattern): return True # Handle directory patterns if pattern.endswith('/') and pattern[:-1] in path.parts: return True return False def find_rust_sources(ignore_patterns): """Find Rust source files, respecting gitignore patterns.""" rust_files = [] for rs_file in Path('.').rglob('*.rs'): if not should_ignore(rs_file, ignore_patterns): rust_files.append(rs_file) return rust_files def detect_project_structure(rust_files): """Detect if this is a library, binary, or mixed project.""" structure = { 'lib': None, 'bins': [], 'examples': [], 'tests': [] } for rs_file in rust_files: parts = rs_file.parts # Check for lib.rs if rs_file.name == 'lib.rs': structure['lib'] = rs_file # Check for main.rs (could be src/main.rs or in subdirs) elif rs_file.name == 'main.rs': # Determine the binary name from the path if len(parts) >= 2 and parts[-2] == 'src': # src/main.rs - default binary structure['bins'].append(('main', rs_file)) elif len(parts) >= 3 and parts[-3] == 'src' and parts[-2] == 'bin': # src/bin/name/main.rs or similar bin_name = parts[-2] if parts[-2] != 'bin' else rs_file.stem structure['bins'].append((bin_name, rs_file)) else: # Other main.rs files, use directory name bin_name = parts[-2] if len(parts) > 1 else 'main' structure['bins'].append((bin_name, rs_file)) # Check for other binaries (src/bin/*.rs) elif len(parts) >= 3 and parts[-2] == 'bin' and parts[-3] == 'src': bin_name = rs_file.stem structure['bins'].append((bin_name, rs_file)) # Check for examples elif 'examples' in parts: structure['examples'].append(rs_file) # Check for tests elif 'tests' in parts or rs_file.name.startswith('test_'): structure['tests'].append(rs_file) return structure def parse_crate_specs(module_content): """Extract crate specifications from MODULE.bazel content.""" crates = {} # Find all crate.spec() calls spec_pattern = r'crate\.spec\(\s*(.*?)\s*\)' specs = re.findall(spec_pattern, module_content, re.DOTALL) for spec in specs: # Parse the spec parameters package_match = re.search(r'package\s*=\s*"([^"]+)"', spec) version_match = re.search(r'version\s*=\s*"([^"]+)"', spec) features_match = re.search(r'features\s*=\s*\[(.*?)\]', spec, re.DOTALL) default_features_match = re.search(r'default_features\s*=\s*False', spec) if package_match and version_match: package = package_match.group(1) version = version_match.group(1) crate_info = {"version": version} # Handle features if features_match: features_str = features_match.group(1) features = [f.strip().strip('"') for f in features_str.split(',') if f.strip()] if features: crate_info["features"] = features # Handle default-features = false if default_features_match: crate_info["default-features"] = False crates[package] = crate_info return crates """Extract crate specifications from MODULE.bazel content.""" crates = {} # Find all crate.spec() calls spec_pattern = r'crate\.spec\(\s*(.*?)\s*\)' specs = re.findall(spec_pattern, module_content, re.DOTALL) for spec in specs: # Parse the spec parameters package_match = re.search(r'package\s*=\s*"([^"]+)"', spec) version_match = re.search(r'version\s*=\s*"([^"]+)"', spec) features_match = re.search(r'features\s*=\s*\[(.*?)\]', spec, re.DOTALL) default_features_match = re.search(r'default_features\s*=\s*False', spec) if package_match and version_match: package = package_match.group(1) version = version_match.group(1) crate_info = {"version": version} # Handle features if features_match: features_str = features_match.group(1) features = [f.strip().strip('"') for f in features_str.split(',') if f.strip()] if features: crate_info["features"] = features # Handle default-features = false if default_features_match: crate_info["default-features"] = False crates[package] = crate_info return crates def generate_cargo_toml(crates, structure, project_name="databuild"): """Generate Cargo.toml content from parsed crates and project structure.""" lines = [ f'[package]', f'name = "{project_name}"', f'version = "0.1.0"', f'edition = "2021"', f'', f'# Generated from MODULE.bazel for IDE support only', f'# Actual dependencies are managed by Bazel', f'' ] # Add library section if lib.rs exists if structure['lib']: lines.extend([ f'[lib]', f'path = "{structure["lib"]}"', f'' ]) # Add binary sections for bin_name, bin_path in structure['bins']: lines.extend([ f'[[bin]]', f'name = "{bin_name}"', f'path = "{bin_path}"', f'' ]) # Add example sections for example_path in structure['examples']: example_name = example_path.stem lines.extend([ f'[[example]]', f'name = "{example_name}"', f'path = "{example_path}"', f'' ]) lines.append('[dependencies]') # Simple dependencies first for package, info in sorted(crates.items()): if not isinstance(info, dict): lines.append(f'{package} = "{info}"') # Complex dependencies as tables for package, info in sorted(crates.items()): if isinstance(info, dict): lines.append(f'') lines.append(f'[dependencies.{package}]') lines.append(f'version = "{info["version"]}"') if "features" in info: features_str = ', '.join(f'"{f}"' for f in info["features"]) lines.append(f'features = [{features_str}]') if "default-features" in info: lines.append(f'default-features = {str(info["default-features"]).lower()}') return '\n'.join(lines) def main(): module_file = Path("MODULE.bazel") if not module_file.exists(): print("MODULE.bazel not found in current directory") sys.exit(1) # Load gitignore patterns ignore_patterns = load_gitignore_patterns() # Find Rust source files rust_files = find_rust_sources(ignore_patterns) if not rust_files: print("No Rust source files found") sys.exit(1) print(f"Found {len(rust_files)} Rust source files") # Detect project structure structure = detect_project_structure(rust_files) # Parse MODULE.bazel for dependencies content = module_file.read_text() crates = parse_crate_specs(content) if not crates: print("No crate specifications found in MODULE.bazel") sys.exit(1) # Generate Cargo.toml cargo_toml = generate_cargo_toml(crates, structure) output_file = Path("Cargo.toml") output_file.write_text(cargo_toml) print(f"Generated Cargo.toml with:") print(f" - {len(crates)} dependencies") if structure['lib']: print(f" - Library: {structure['lib']}") if structure['bins']: print(f" - {len(structure['bins'])} binaries: {[name for name, _ in structure['bins']]}") if structure['examples']: print(f" - {len(structure['examples'])} examples") print("This file is for IDE support only - Bazel manages the actual build") if __name__ == "__main__": main()