396 lines
11 KiB
Python
396 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
StellaOps Solution Generator.
|
|
|
|
Generates consistent .sln files for:
|
|
- Main solution (src/StellaOps.sln) with all projects
|
|
- Module solutions (src/<Module>/StellaOps.<Module>.sln) with external deps in __External/
|
|
|
|
Usage:
|
|
python sln_generator.py [OPTIONS]
|
|
|
|
Options:
|
|
--src-root PATH Root of src/ directory (default: ./src)
|
|
--main-only Only regenerate main solution
|
|
--module NAME Regenerate specific module solution only
|
|
--all Regenerate all solutions (default)
|
|
--dry-run Show changes without writing
|
|
--check CI mode: exit 1 if solutions need updating
|
|
-v, --verbose Verbose output
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from lib.csproj_parser import find_all_csproj, parse_csproj
|
|
from lib.dependency_graph import (
|
|
collect_all_external_dependencies,
|
|
get_module_projects,
|
|
)
|
|
from lib.models import CsprojProject
|
|
from lib.sln_writer import (
|
|
build_external_folder_hierarchy,
|
|
build_folder_hierarchy,
|
|
generate_solution_content,
|
|
has_bypass_marker,
|
|
write_solution_file,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Directories under src/ that are modules (have their own solutions)
|
|
# Excludes special directories like __Libraries, __Tests, __Analyzers, Web, etc.
|
|
EXCLUDED_FROM_MODULE_SOLUTIONS = {
|
|
"__Libraries",
|
|
"__Tests",
|
|
"__Analyzers",
|
|
".nuget",
|
|
".cache",
|
|
".vs",
|
|
"Web", # Angular project, not .NET
|
|
"plugins",
|
|
"app",
|
|
"Api",
|
|
"Sdk",
|
|
"DevPortal",
|
|
"Mirror",
|
|
"Provenance",
|
|
"Symbols",
|
|
"Unknowns",
|
|
}
|
|
|
|
|
|
def setup_logging(verbose: bool) -> None:
|
|
"""Configure logging based on verbosity."""
|
|
level = logging.DEBUG if verbose else logging.INFO
|
|
logging.basicConfig(
|
|
level=level,
|
|
format="%(levelname)s: %(message)s",
|
|
)
|
|
|
|
|
|
def discover_modules(src_root: Path) -> list[Path]:
|
|
"""
|
|
Discover all module directories under src/.
|
|
|
|
A module is a directory that:
|
|
- Is a direct child of src/
|
|
- Is not in EXCLUDED_FROM_MODULE_SOLUTIONS
|
|
- Contains at least one .csproj file
|
|
|
|
Returns:
|
|
List of absolute paths to module directories
|
|
"""
|
|
modules: list[Path] = []
|
|
|
|
for item in src_root.iterdir():
|
|
if not item.is_dir():
|
|
continue
|
|
|
|
if item.name in EXCLUDED_FROM_MODULE_SOLUTIONS:
|
|
continue
|
|
|
|
if item.name.startswith("."):
|
|
continue
|
|
|
|
# Check if it contains any csproj files
|
|
csproj_files = list(item.rglob("*.csproj"))
|
|
if csproj_files:
|
|
modules.append(item.resolve())
|
|
|
|
return sorted(modules)
|
|
|
|
|
|
def load_all_projects(src_root: Path) -> tuple[list[CsprojProject], dict[Path, CsprojProject]]:
|
|
"""
|
|
Load and parse all projects under src/.
|
|
|
|
Returns:
|
|
Tuple of (list of all projects, map from path to project)
|
|
"""
|
|
csproj_files = find_all_csproj(src_root)
|
|
logger.info(f"Found {len(csproj_files)} .csproj files")
|
|
|
|
projects: list[CsprojProject] = []
|
|
project_map: dict[Path, CsprojProject] = {}
|
|
|
|
for csproj_path in csproj_files:
|
|
project = parse_csproj(csproj_path, src_root)
|
|
if project:
|
|
projects.append(project)
|
|
project_map[project.path] = project
|
|
else:
|
|
logger.warning(f"Failed to parse: {csproj_path}")
|
|
|
|
logger.info(f"Successfully parsed {len(projects)} projects")
|
|
return projects, project_map
|
|
|
|
|
|
def generate_main_solution(
|
|
src_root: Path,
|
|
projects: list[CsprojProject],
|
|
dry_run: bool = False,
|
|
) -> bool:
|
|
"""
|
|
Generate the main StellaOps.sln with all projects.
|
|
|
|
Args:
|
|
src_root: Root of src/ directory
|
|
projects: All parsed projects
|
|
dry_run: If True, don't write files
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
sln_path = src_root / "StellaOps.sln"
|
|
|
|
# Check for bypass marker
|
|
if has_bypass_marker(sln_path):
|
|
logger.info(f"Skipping {sln_path} (has bypass marker)")
|
|
return True
|
|
|
|
logger.info(f"Generating main solution: {sln_path}")
|
|
|
|
# Build folder hierarchy matching physical structure
|
|
folders = build_folder_hierarchy(projects, src_root)
|
|
|
|
# Generate solution content
|
|
content = generate_solution_content(
|
|
sln_path=sln_path,
|
|
projects=[], # Projects are in folders
|
|
folders=folders,
|
|
external_folders=None,
|
|
add_bypass_marker=False,
|
|
)
|
|
|
|
return write_solution_file(sln_path, content, dry_run)
|
|
|
|
|
|
def generate_module_solution(
|
|
module_dir: Path,
|
|
src_root: Path,
|
|
all_projects: list[CsprojProject],
|
|
project_map: dict[Path, CsprojProject],
|
|
dry_run: bool = False,
|
|
) -> bool:
|
|
"""
|
|
Generate a module-specific solution.
|
|
|
|
Args:
|
|
module_dir: Root directory of the module
|
|
src_root: Root of src/ directory
|
|
all_projects: All parsed projects
|
|
project_map: Map from path to project
|
|
dry_run: If True, don't write files
|
|
|
|
Returns:
|
|
True if successful
|
|
"""
|
|
module_name = module_dir.name
|
|
sln_path = module_dir / f"StellaOps.{module_name}.sln"
|
|
|
|
# Check for bypass marker
|
|
if has_bypass_marker(sln_path):
|
|
logger.info(f"Skipping {sln_path} (has bypass marker)")
|
|
return True
|
|
|
|
logger.info(f"Generating module solution: {sln_path}")
|
|
|
|
# Get projects within this module
|
|
module_projects = get_module_projects(module_dir, all_projects)
|
|
|
|
if not module_projects:
|
|
logger.warning(f"No projects found in module: {module_name}")
|
|
return True
|
|
|
|
logger.debug(f" Found {len(module_projects)} projects in module")
|
|
|
|
# Build internal folder hierarchy
|
|
internal_folders = build_folder_hierarchy(module_projects, module_dir)
|
|
|
|
# Collect external dependencies
|
|
external_groups = collect_all_external_dependencies(
|
|
projects=module_projects,
|
|
module_dir=module_dir,
|
|
src_root=src_root,
|
|
project_map=project_map,
|
|
)
|
|
|
|
# Build external folder hierarchy
|
|
external_folders = {}
|
|
if external_groups:
|
|
external_folders = build_external_folder_hierarchy(external_groups, src_root)
|
|
ext_count = sum(len(v) for v in external_groups.values())
|
|
logger.debug(f" Found {ext_count} external dependencies")
|
|
|
|
# Generate solution content
|
|
content = generate_solution_content(
|
|
sln_path=sln_path,
|
|
projects=[], # Projects are in folders
|
|
folders=internal_folders,
|
|
external_folders=external_folders,
|
|
add_bypass_marker=False,
|
|
)
|
|
|
|
return write_solution_file(sln_path, content, dry_run)
|
|
|
|
|
|
def check_solutions_up_to_date(
|
|
src_root: Path,
|
|
modules: list[Path],
|
|
all_projects: list[CsprojProject],
|
|
project_map: dict[Path, CsprojProject],
|
|
) -> bool:
|
|
"""
|
|
Check if solutions need updating (for --check mode).
|
|
|
|
Args:
|
|
src_root: Root of src/ directory
|
|
modules: List of module directories
|
|
all_projects: All parsed projects
|
|
project_map: Map from path to project
|
|
|
|
Returns:
|
|
True if all solutions are up to date
|
|
"""
|
|
# This is a simplified check - in a real implementation,
|
|
# you would compare generated content with existing files
|
|
logger.info("Checking if solutions are up to date...")
|
|
|
|
# For now, just check if files exist
|
|
main_sln = src_root / "StellaOps.sln"
|
|
if not main_sln.exists():
|
|
logger.error(f"Main solution missing: {main_sln}")
|
|
return False
|
|
|
|
for module_dir in modules:
|
|
module_name = module_dir.name
|
|
module_sln = module_dir / f"StellaOps.{module_name}.sln"
|
|
|
|
if has_bypass_marker(module_sln):
|
|
continue
|
|
|
|
if not module_sln.exists():
|
|
logger.error(f"Module solution missing: {module_sln}")
|
|
return False
|
|
|
|
logger.info("All solutions appear to be up to date")
|
|
return True
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate StellaOps solution files",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog=__doc__,
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--src-root",
|
|
type=Path,
|
|
default=Path("src"),
|
|
help="Root of src/ directory (default: ./src)",
|
|
)
|
|
parser.add_argument(
|
|
"--main-only",
|
|
action="store_true",
|
|
help="Only regenerate main solution",
|
|
)
|
|
parser.add_argument(
|
|
"--module",
|
|
type=str,
|
|
help="Regenerate specific module solution only",
|
|
)
|
|
parser.add_argument(
|
|
"--all",
|
|
action="store_true",
|
|
dest="regenerate_all",
|
|
help="Regenerate all solutions (default)",
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show changes without writing",
|
|
)
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="CI mode: exit 1 if solutions need updating",
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--verbose",
|
|
action="store_true",
|
|
help="Verbose output",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
setup_logging(args.verbose)
|
|
|
|
# Resolve src root
|
|
src_root = args.src_root.resolve()
|
|
if not src_root.exists():
|
|
logger.error(f"Source root does not exist: {src_root}")
|
|
return 1
|
|
|
|
logger.info(f"Source root: {src_root}")
|
|
|
|
# Load all projects
|
|
all_projects, project_map = load_all_projects(src_root)
|
|
|
|
if not all_projects:
|
|
logger.error("No projects found")
|
|
return 1
|
|
|
|
# Discover modules
|
|
modules = discover_modules(src_root)
|
|
logger.info(f"Discovered {len(modules)} modules")
|
|
|
|
# Check mode
|
|
if args.check:
|
|
if check_solutions_up_to_date(src_root, modules, all_projects, project_map):
|
|
return 0
|
|
return 1
|
|
|
|
# Determine what to generate
|
|
success = True
|
|
|
|
if args.module:
|
|
# Specific module only
|
|
module_dir = src_root / args.module
|
|
if not module_dir.exists():
|
|
logger.error(f"Module directory does not exist: {module_dir}")
|
|
return 1
|
|
|
|
success = generate_module_solution(
|
|
module_dir, src_root, all_projects, project_map, args.dry_run
|
|
)
|
|
|
|
elif args.main_only:
|
|
# Main solution only
|
|
success = generate_main_solution(src_root, all_projects, args.dry_run)
|
|
|
|
else:
|
|
# Generate all (default)
|
|
# Main solution
|
|
if not generate_main_solution(src_root, all_projects, args.dry_run):
|
|
success = False
|
|
|
|
# Module solutions
|
|
for module_dir in modules:
|
|
if not generate_module_solution(
|
|
module_dir, src_root, all_projects, project_map, args.dry_run
|
|
):
|
|
success = False
|
|
|
|
if args.dry_run:
|
|
logger.info("Dry run complete - no files were modified")
|
|
|
|
return 0 if success else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|