Add cargo.toml generator so intellij isn't useless for rust
This commit is contained in:
parent
a358e7a091
commit
26c8cb2461
2 changed files with 276 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,3 +9,4 @@ examples/podcast_reviews/data
|
||||||
.venv
|
.venv
|
||||||
node_modules
|
node_modules
|
||||||
**/node_modules
|
**/node_modules
|
||||||
|
Cargo.toml
|
||||||
|
|
|
||||||
275
tools/build_rules/generate_cargo_toml.py
Normal file
275
tools/build_rules/generate_cargo_toml.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue