#!/usr/bin/env python3 """ Generate Visual Studio solution files for StellaOps Organizes all .csproj files into: 1. Main StellaOps.sln (all projects) 2. Module-specific .sln files 3. StellaOps.Infrastructure.sln (shared libraries) 4. StellaOps.Tests.sln (global tests) """ import os import uuid import re from pathlib import Path from typing import Dict, List, Set, Tuple from collections import defaultdict # Base directory BASE_DIR = Path(r"E:\dev\git.stella-ops.org") SRC_DIR = BASE_DIR / "src" # Module names based on directory structure MODULES = [ "AdvisoryAI", "AirGap", "Aoc", "Attestor", "Authority", "Bench", "BinaryIndex", "Cartographer", "Cli", "Concelier", "Cryptography", "EvidenceLocker", "Excititor", "ExportCenter", "Gateway", "Graph", "IssuerDirectory", "Notify", "Orchestrator", "Policy", "Replay", "SbomService", "Scanner", "Scheduler", "Signer", "Signals", "TaskRunner", "Telemetry", "VexHub", "VexLens", "VulnExplorer", "Web", "Zastava" ] # Project type GUIDs FAE04EC0_301F_11D3_BF4B_00C04F79EFBC = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}" # C# project SLN_FOLDER_GUID = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}" # Solution folder def generate_project_guid(project_path: str) -> str: """Generate deterministic GUID based on project path""" # Use namespace UUID for deterministic generation namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') return str(uuid.uuid5(namespace, project_path)).upper() def get_module_from_path(project_path: Path) -> str: """Determine module name from project path""" relative = project_path.relative_to(SRC_DIR) parts = relative.parts # Check direct module directory if len(parts) > 0 and parts[0] in MODULES: return parts[0] # Check __Libraries/StellaOps..* if parts[0] == "__Libraries": project_name = parts[-1].replace(".csproj", "") for module in MODULES: if f"StellaOps.{module}" in project_name: return module # Check __Tests/StellaOps..*.Tests if parts[0] == "__Tests": project_name = parts[-1].replace(".csproj", "") for module in MODULES: if f"StellaOps.{module}" in project_name: return module # Global tests return "Tests" # Check Integration tests if len(parts) > 1 and parts[0] == "__Tests" and parts[1] == "Integration": project_name = parts[-1].replace(".csproj", "") for module in MODULES: if f"StellaOps.{module}" in project_name: return module return "Tests" # Default to Infrastructure for shared libraries if parts[0] == "__Libraries": return "Infrastructure" return "Infrastructure" def find_all_projects() -> List[Path]: """Find all .csproj files in src directory""" projects = [] for root, dirs, files in os.walk(SRC_DIR): for file in files: if file.endswith(".csproj"): projects.append(Path(root) / file) return sorted(projects) def categorize_project(project_path: Path, module: str) -> str: """Determine category for solution folder organization""" relative = project_path.relative_to(SRC_DIR) parts = relative.parts # Test projects if "__Tests" in parts or project_path.name.endswith(".Tests.csproj"): return "Tests" # Benchmark projects if "Bench" in parts or "Benchmark" in project_path.name: return "Benchmarks" # Plugin projects if "Plugin" in project_path.name or "Connector" in project_path.name: return "Plugins" # Library projects if "__Libraries" in parts: return "Libraries" # Analyzer projects if "__Analyzers" in parts or "Analyzer" in project_path.name: return "Analyzers" # Web services if "WebService" in project_path.name: return "WebServices" # Workers if "Worker" in project_path.name: return "Workers" # Core module projects return "Core" def generate_sln_header() -> str: """Generate Visual Studio 2022 solution header""" return """Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 """ def generate_project_entry(project_path: Path, project_guid: str) -> str: """Generate project entry for .sln file""" project_name = project_path.stem relative_path = project_path.relative_to(BASE_DIR) return f'Project("{FAE04EC0_301F_11D3_BF4B_00C04F79EFBC}") = "{project_name}", "{relative_path}", "{{{project_guid}}}"\nEndProject' def generate_folder_entry(folder_name: str, folder_guid: str) -> str: """Generate solution folder entry""" return f'Project("{SLN_FOLDER_GUID}") = "{folder_name}", "{folder_name}", "{{{folder_guid}}}"\nEndProject' def generate_nested_projects(folder_mappings: Dict[str, List[str]]) -> str: """Generate NestedProjects section""" lines = ["\tGlobalSection(NestedProjects) = preSolution"] for folder_guid, project_guids in folder_mappings.items(): for project_guid in project_guids: lines.append(f"\t\t{{{project_guid}}} = {{{folder_guid}}}") lines.append("\tEndGlobalSection") return "\n".join(lines) def generate_main_solution(projects: List[Path], module_assignments: Dict[str, List[Path]]) -> str: """Generate main StellaOps.sln with all projects""" content = [generate_sln_header()] # Track GUIDs project_guids: Dict[str, str] = {} folder_guids: Dict[str, str] = {} folder_mappings: Dict[str, List[str]] = defaultdict(list) # Create folder structure: Module -> Category -> Projects for module in sorted(module_assignments.keys()): module_folder_guid = generate_project_guid(f"folder_{module}") folder_guids[module] = module_folder_guid content.append(generate_folder_entry(module, module_folder_guid)) # Group projects by category within module category_projects: Dict[str, List[Path]] = defaultdict(list) for project in module_assignments[module]: category = categorize_project(project, module) category_projects[category].append(project) # Create category folders for category in sorted(category_projects.keys()): category_folder_name = f"{module}.{category}" category_folder_guid = generate_project_guid(f"folder_{category_folder_name}") folder_guids[category_folder_name] = category_folder_guid content.append(generate_folder_entry(category, category_folder_guid)) folder_mappings[module_folder_guid].append(category_folder_guid) # Add projects to category for project in sorted(category_projects[category]): project_guid = generate_project_guid(str(project)) project_guids[str(project)] = project_guid content.append(generate_project_entry(project, project_guid)) folder_mappings[category_folder_guid].append(project_guid) # Add Global section content.append("Global") content.append("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution") content.append("\t\tDebug|Any CPU = Debug|Any CPU") content.append("\t\tRelease|Any CPU = Release|Any CPU") content.append("\tEndGlobalSection") # Project configurations content.append("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution") for project_guid in project_guids.values(): content.append(f"\t\t{{{project_guid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU") content.append(f"\t\t{{{project_guid}}}.Debug|Any CPU.Build.0 = Debug|Any CPU") content.append(f"\t\t{{{project_guid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU") content.append(f"\t\t{{{project_guid}}}.Release|Any CPU.Build.0 = Release|Any CPU") content.append("\tEndGlobalSection") # Nested projects content.append(generate_nested_projects(folder_mappings)) content.append("EndGlobal") return "\n".join(content) def generate_module_solution(module: str, projects: List[Path]) -> str: """Generate module-specific .sln file""" content = [generate_sln_header()] project_guids: Dict[str, str] = {} folder_guids: Dict[str, str] = {} folder_mappings: Dict[str, List[str]] = defaultdict(list) # Group projects by category category_projects: Dict[str, List[Path]] = defaultdict(list) for project in projects: category = categorize_project(project, module) category_projects[category].append(project) # Create category folders and add projects for category in sorted(category_projects.keys()): category_folder_guid = generate_project_guid(f"folder_{module}_{category}") folder_guids[category] = category_folder_guid content.append(generate_folder_entry(category, category_folder_guid)) for project in sorted(category_projects[category]): project_guid = generate_project_guid(str(project)) project_guids[str(project)] = project_guid content.append(generate_project_entry(project, project_guid)) folder_mappings[category_folder_guid].append(project_guid) # Add Global section content.append("Global") content.append("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution") content.append("\t\tDebug|Any CPU = Debug|Any CPU") content.append("\t\tRelease|Any CPU = Release|Any CPU") content.append("\tEndGlobalSection") # Project configurations content.append("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution") for project_guid in project_guids.values(): content.append(f"\t\t{{{project_guid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU") content.append(f"\t\t{{{project_guid}}}.Debug|Any CPU.Build.0 = Debug|Any CPU") content.append(f"\t\t{{{project_guid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU") content.append(f"\t\t{{{project_guid}}}.Release|Any CPU.Build.0 = Release|Any CPU") content.append("\tEndGlobalSection") # Nested projects content.append(generate_nested_projects(folder_mappings)) content.append("EndGlobal") return "\n".join(content) def main(): print("Finding all .csproj files...") all_projects = find_all_projects() print(f"Found {len(all_projects)} projects") # Assign projects to modules module_assignments: Dict[str, List[Path]] = defaultdict(list) for project in all_projects: module = get_module_from_path(project) module_assignments[module].append(project) # Print summary print("\nModule assignment summary:") for module in sorted(module_assignments.keys()): print(f" {module}: {len(module_assignments[module])} projects") # Generate main solution print("\nGenerating main StellaOps.sln...") main_sln = generate_main_solution(all_projects, module_assignments) main_sln_path = SRC_DIR / "StellaOps.sln" with open(main_sln_path, 'w', encoding='utf-8-sig') as f: f.write(main_sln) print(f" Written: {main_sln_path}") print(f" Projects: {len(all_projects)}") # Generate module-specific solutions print("\nGenerating module-specific solutions...") for module in sorted(module_assignments.keys()): if module in ["Infrastructure", "Tests"]: # These get special handling below continue projects = module_assignments[module] if len(projects) == 0: continue module_sln = generate_module_solution(module, projects) module_sln_path = SRC_DIR / f"StellaOps.{module}.sln" with open(module_sln_path, 'w', encoding='utf-8-sig') as f: f.write(module_sln) print(f" Written: {module_sln_path}") print(f" Projects: {len(projects)}") # Generate Infrastructure solution if "Infrastructure" in module_assignments: print("\nGenerating StellaOps.Infrastructure.sln...") infra_projects = module_assignments["Infrastructure"] infra_sln = generate_module_solution("Infrastructure", infra_projects) infra_sln_path = SRC_DIR / "StellaOps.Infrastructure.sln" with open(infra_sln_path, 'w', encoding='utf-8-sig') as f: f.write(infra_sln) print(f" Written: {infra_sln_path}") print(f" Projects: {len(infra_projects)}") # Generate Tests solution if "Tests" in module_assignments: print("\nGenerating StellaOps.Tests.sln...") test_projects = module_assignments["Tests"] test_sln = generate_module_solution("Tests", test_projects) test_sln_path = SRC_DIR / "StellaOps.Tests.sln" with open(test_sln_path, 'w', encoding='utf-8-sig') as f: f.write(test_sln) print(f" Written: {test_sln_path}") print(f" Projects: {len(test_projects)}") # Verify each project is in exactly 2 solutions print("\n\nVerifying project membership...") project_solution_count: Dict[str, Set[str]] = defaultdict(set) # Count main solution for project in all_projects: project_solution_count[str(project)].add("StellaOps.sln") # Count module solutions for module, projects in module_assignments.items(): if module == "Infrastructure": sln_name = "StellaOps.Infrastructure.sln" elif module == "Tests": sln_name = "StellaOps.Tests.sln" else: sln_name = f"StellaOps.{module}.sln" for project in projects: project_solution_count[str(project)].add(sln_name) # Check for violations violations = [] for project, solutions in project_solution_count.items(): if len(solutions) != 2: violations.append((project, solutions)) if violations: print(f"\nāŒ ERROR: {len(violations)} projects are not in exactly 2 solutions:") for project, solutions in violations[:10]: # Show first 10 print(f" {Path(project).name}: in {len(solutions)} solutions - {solutions}") if len(violations) > 10: print(f" ... and {len(violations) - 10} more") else: print("āœ… All projects are in exactly 2 solutions!") print("\nāœ… Solution generation complete!") print(f" Total projects: {len(all_projects)}") print(f" Solutions created: {len(module_assignments) + 1}") if __name__ == "__main__": main()