287 lines
8.9 KiB
Python
287 lines
8.9 KiB
Python
#!/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())
|