#!/usr/bin/env python3 """ Solution File Manager for IoT Edge Module Scaffolding Manages adding newly created modules to solution files. Supports: - .slnx (XML-based, modern format) - auto-add capability - .sln (legacy format with GUIDs) - manual instructions only """ import argparse import json import sys import xml.etree.ElementTree as ET from pathlib import Path def find_solution_file(root_path: Path) -> dict: """ Find solution file in project root. Returns: dict with keys: - type: "slnx", "sln", or "none" - path: Path to solution file or None - name: Solution file name or None """ # Always exclude test directories def is_test_dir(path: Path) -> bool: return any(part.lower().startswith('test') for part in path.parts) # Look for .slnx first (modern format) slnx_files = [f for f in root_path.rglob("*.slnx") if not is_test_dir(f)] if slnx_files: # Use the first one found, preferring root directory slnx_files.sort(key=lambda p: (len(p.parts), p)) return { "type": "slnx", "path": slnx_files[0], "name": slnx_files[0].name } # Fall back to .sln (legacy format) sln_files = [f for f in root_path.rglob("*.sln") if not is_test_dir(f)] if sln_files: sln_files.sort(key=lambda p: (len(p.parts), p)) return { "type": "sln", "path": sln_files[0], "name": sln_files[0].name } return { "type": "none", "path": None, "name": None } def add_module_to_slnx(slnx_path: Path, module_csproj_path: str) -> dict: """ Add module to .slnx solution file. Args: slnx_path: Path to .slnx file module_csproj_path: Relative path to module .csproj (e.g., "src/IoTEdgeModules/modules/mymodule/MyModule.csproj") Returns: dict with keys: - success: bool - message: str - action: "added", "already_exists", or "error" """ try: # Parse XML tree = ET.parse(slnx_path) root = tree.getroot() # Find or create /modules/ folder modules_folder = None for folder in root.findall("Folder"): if folder.get("Name") == "/modules/": modules_folder = folder break if modules_folder is None: # Create /modules/ folder if it doesn't exist modules_folder = ET.SubElement(root, "Folder") modules_folder.set("Name", "/modules/") # Check if module already exists for project in modules_folder.findall("Project"): if project.get("Path") == module_csproj_path: return { "success": True, "message": f"Module already exists in solution: {module_csproj_path}", "action": "already_exists" } # Get all existing project paths for alphabetical insertion existing_projects = [(p.get("Path"), p) for p in modules_folder.findall("Project")] existing_paths = [path for path, _ in existing_projects] # Find insertion point (alphabetical order) insertion_index = 0 for i, existing_path in enumerate(existing_paths): if module_csproj_path.lower() < existing_path.lower(): insertion_index = i break insertion_index = i + 1 # Create new project element new_project = ET.Element("Project") new_project.set("Path", module_csproj_path) # Insert at the calculated position modules_folder.insert(insertion_index, new_project) # Write back to file with proper formatting # Add indentation indent_xml(root) tree.write(slnx_path, encoding="utf-8", xml_declaration=False) return { "success": True, "message": f"Added module to solution at position {insertion_index + 1} of {len(existing_paths) + 1}", "action": "added", "insertion_index": insertion_index, "total_modules": len(existing_paths) + 1 } except ET.ParseError as e: return { "success": False, "message": f"Failed to parse .slnx file: {e}", "action": "error" } except Exception as e: return { "success": False, "message": f"Error adding module to .slnx: {e}", "action": "error" } def indent_xml(elem, level=0): """ Recursively add indentation to an XML element and its children for pretty printing. Args: elem: xml.etree.ElementTree.Element The XML element to indent. level: int, optional The current indentation level (default is 0). Returns: None. The function modifies the XML element in place. """ indent = "\n" + " " * level if len(elem): if not elem.text or not elem.text.strip(): elem.text = indent + " " if not elem.tail or not elem.tail.strip(): elem.tail = indent for child in elem: indent_xml(child, level + 1) if not child.tail or not child.tail.strip(): child.tail = indent else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = indent def get_sln_manual_instructions(module_csproj_path: str, module_name: str) -> str: """ Generate manual instructions for adding module to .sln file. .sln files require GUIDs which are complex to generate correctly, so we provide manual instructions instead. """ instructions = f""" Manual instructions for adding module to .sln file: .sln files require project GUIDs which must be generated. The easiest way is to use Visual Studio or the dotnet CLI: Option 1: Using dotnet CLI (recommended): dotnet sln add "{module_csproj_path}" Option 2: Using Visual Studio: 1. Open the solution in Visual Studio 2. Right-click on the solution in Solution Explorer 3. Select "Add" → "Existing Project" 4. Navigate to and select: {module_csproj_path} Option 3: Manual editing (advanced): 1. Open the .sln file in a text editor 2. Generate a new GUID (use online generator or PowerShell: [guid]::NewGuid()) 3. Add project entry: Project("{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}") = "{module_name}", "{module_csproj_path}", "{{YOUR-NEW-GUID}}" EndProject 4. Add to solution configuration platforms section (match existing patterns) Note: The dotnet CLI approach is recommended as it handles all GUID generation automatically. """ return instructions def main(): parser = argparse.ArgumentParser( description="Manage solution file updates for IoT Edge modules" ) parser.add_argument( "--root", type=str, default=".", help="Root directory to search for solution file (default: current directory)" ) parser.add_argument( "--detect", action="store_true", help="Detect solution file type and location" ) parser.add_argument( "--add-module", type=str, help="Relative path to module .csproj to add to solution" ) parser.add_argument( "--module-name", type=str, help="Module name (for manual instructions)" ) args = parser.parse_args() root_path = Path(args.root).resolve() # Detect solution file solution_info = find_solution_file(root_path) if args.detect: # Just output detection results print(json.dumps(solution_info, indent=2, default=str)) return 0 if args.add_module: if solution_info["type"] == "none": result = { "success": False, "message": "No solution file found", "action": "error" } print(json.dumps(result, indent=2)) return 1 if solution_info["type"] == "slnx": # Auto-add to .slnx result = add_module_to_slnx( solution_info["path"], args.add_module ) print(json.dumps(result, indent=2)) return 0 if result["success"] else 1 elif solution_info["type"] == "sln": # Provide manual instructions for .sln module_name = args.module_name or Path(args.add_module).stem instructions = get_sln_manual_instructions(args.add_module, module_name) result = { "success": True, "message": "Manual instructions generated for .sln file", "action": "manual_instructions", "instructions": instructions, "solution_path": str(solution_info["path"]) } print(json.dumps(result, indent=2)) return 0 # No action specified parser.print_help() return 1 if __name__ == "__main__": sys.exit(main())