From 26c8cb246159ab4f476d4926e9f1902d7b7e237d Mon Sep 17 00:00:00 2001 From: Stuart Axelbrooke Date: Thu, 17 Jul 2025 23:28:51 -0700 Subject: [PATCH] Add cargo.toml generator so intellij isn't useless for rust --- .gitignore | 1 + tools/build_rules/generate_cargo_toml.py | 275 +++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 tools/build_rules/generate_cargo_toml.py diff --git a/.gitignore b/.gitignore index fb697d3..a06ef4e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ examples/podcast_reviews/data .venv node_modules **/node_modules +Cargo.toml diff --git a/tools/build_rules/generate_cargo_toml.py b/tools/build_rules/generate_cargo_toml.py new file mode 100644 index 0000000..61ed75c --- /dev/null +++ b/tools/build_rules/generate_cargo_toml.py @@ -0,0 +1,275 @@ +#!/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() \ No newline at end of file