save progress

This commit is contained in:
StellaOps Bot
2025-12-28 01:40:35 +02:00
parent 3bfbbae115
commit cec4265a40
694 changed files with 88052 additions and 24718 deletions

View File

@@ -0,0 +1,381 @@
"""
Solution file (.sln) generation utilities.
Provides functions to:
- Build solution folder hierarchy from projects
- Generate complete .sln file content
"""
import logging
from pathlib import Path
from typing import Optional
from .csproj_parser import get_deterministic_guid
from .models import (
BYPASS_MARKER,
CSHARP_PROJECT_TYPE_GUID,
SOLUTION_FOLDER_TYPE_GUID,
CsprojProject,
SolutionFolder,
)
logger = logging.getLogger(__name__)
# Solution file header
SOLUTION_HEADER = """\
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
"""
def build_folder_hierarchy(
projects: list[CsprojProject],
base_dir: Path,
prefix: str = "",
) -> dict[str, SolutionFolder]:
"""
Build solution folder hierarchy from project paths.
Creates nested folders matching the physical directory structure.
Args:
projects: List of projects to organize
base_dir: Base directory for calculating relative paths
prefix: Optional prefix for folder paths (e.g., "__External")
Returns:
Dictionary mapping folder path to SolutionFolder object
"""
folders: dict[str, SolutionFolder] = {}
base_dir = base_dir.resolve()
for project in projects:
try:
rel_path = project.path.parent.relative_to(base_dir)
except ValueError:
# Project outside base_dir - skip folder creation
continue
parts = list(rel_path.parts)
if not parts:
continue
# Add prefix if specified
if prefix:
parts = [prefix] + list(parts)
# Create folders for each level
current_path = ""
parent_guid: Optional[str] = None
for part in parts:
if current_path:
current_path = f"{current_path}/{part}"
else:
current_path = part
if current_path not in folders:
folder_guid = get_deterministic_guid(
Path(current_path), Path("")
)
folders[current_path] = SolutionFolder(
name=part,
guid=folder_guid,
path=current_path,
parent_guid=parent_guid,
)
parent_guid = folders[current_path].guid
# Assign project to its folder
if current_path in folders:
folders[current_path].projects.append(project)
return folders
def build_external_folder_hierarchy(
external_groups: dict[str, list[CsprojProject]],
src_root: Path,
) -> dict[str, SolutionFolder]:
"""
Build folder hierarchy for external dependencies.
Organizes external projects under __External/<Source>/<Path>.
Args:
external_groups: Dictionary mapping source category to projects
src_root: Root of the src/ directory
Returns:
Dictionary mapping folder path to SolutionFolder object
"""
folders: dict[str, SolutionFolder] = {}
src_root = src_root.resolve()
# Create __External root folder
external_root_path = "__External"
external_root_guid = get_deterministic_guid(Path(external_root_path), Path(""))
folders[external_root_path] = SolutionFolder(
name="__External",
guid=external_root_guid,
path=external_root_path,
parent_guid=None,
)
for category, projects in sorted(external_groups.items()):
if not projects:
continue
# Create category folder (e.g., __External/__Libraries, __External/Authority)
category_path = f"{external_root_path}/{category}"
category_guid = get_deterministic_guid(Path(category_path), Path(""))
if category_path not in folders:
folders[category_path] = SolutionFolder(
name=category,
guid=category_guid,
path=category_path,
parent_guid=external_root_guid,
)
# For each project, create intermediate folders based on path within source
for project in projects:
try:
if category == "__Libraries":
# Path relative to src/__Libraries/
lib_root = src_root / "__Libraries"
rel_path = project.path.parent.relative_to(lib_root)
else:
# Path relative to src/<Module>/
module_root = src_root / category
rel_path = project.path.parent.relative_to(module_root)
except ValueError:
# Just put directly in category folder
folders[category_path].projects.append(project)
continue
parts = list(rel_path.parts)
if not parts:
folders[category_path].projects.append(project)
continue
# Create intermediate folders
current_path = category_path
parent_guid = category_guid
for part in parts:
current_path = f"{current_path}/{part}"
if current_path not in folders:
folder_guid = get_deterministic_guid(Path(current_path), Path(""))
folders[current_path] = SolutionFolder(
name=part,
guid=folder_guid,
path=current_path,
parent_guid=parent_guid,
)
parent_guid = folders[current_path].guid
# Assign project to deepest folder
folders[current_path].projects.append(project)
return folders
def generate_solution_content(
sln_path: Path,
projects: list[CsprojProject],
folders: dict[str, SolutionFolder],
external_folders: Optional[dict[str, SolutionFolder]] = None,
add_bypass_marker: bool = False,
) -> str:
"""
Generate complete .sln file content.
Args:
sln_path: Path where the solution will be written (for relative paths)
projects: List of internal projects
folders: Internal folder hierarchy
external_folders: External dependency folders (optional)
add_bypass_marker: Whether to add the bypass marker comment
Returns:
Complete .sln file content as string
"""
lines: list[str] = []
sln_dir = sln_path.parent.resolve()
# Add header
if add_bypass_marker:
lines.append(BYPASS_MARKER)
lines.append("")
lines.append(SOLUTION_HEADER.rstrip())
# Merge folders
all_folders = dict(folders)
if external_folders:
all_folders.update(external_folders)
# Collect all projects (internal + external from folders)
all_projects: list[CsprojProject] = list(projects)
project_to_folder: dict[Path, str] = {}
for folder_path, folder in all_folders.items():
for proj in folder.projects:
if proj not in all_projects:
all_projects.append(proj)
project_to_folder[proj.path] = folder_path
# Write solution folder entries
for folder_path in sorted(all_folders.keys()):
folder = all_folders[folder_path]
lines.append(
f'Project("{{{SOLUTION_FOLDER_TYPE_GUID}}}") = "{folder.name}", "{folder.name}", "{{{folder.guid}}}"'
)
lines.append("EndProject")
# Write project entries
for project in sorted(all_projects, key=lambda p: p.name):
rel_path = _get_relative_path(sln_dir, project.path)
lines.append(
f'Project("{{{CSHARP_PROJECT_TYPE_GUID}}}") = "{project.name}", "{rel_path}", "{{{project.guid}}}"'
)
lines.append("EndProject")
# Write Global section
lines.append("Global")
# SolutionConfigurationPlatforms
lines.append("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution")
lines.append("\t\tDebug|Any CPU = Debug|Any CPU")
lines.append("\t\tRelease|Any CPU = Release|Any CPU")
lines.append("\tEndGlobalSection")
# ProjectConfigurationPlatforms
lines.append("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution")
for project in sorted(all_projects, key=lambda p: p.name):
guid = project.guid
lines.append(f"\t\t{{{guid}}}.Debug|Any CPU.ActiveCfg = Debug|Any CPU")
lines.append(f"\t\t{{{guid}}}.Debug|Any CPU.Build.0 = Debug|Any CPU")
lines.append(f"\t\t{{{guid}}}.Release|Any CPU.ActiveCfg = Release|Any CPU")
lines.append(f"\t\t{{{guid}}}.Release|Any CPU.Build.0 = Release|Any CPU")
lines.append("\tEndGlobalSection")
# SolutionProperties
lines.append("\tGlobalSection(SolutionProperties) = preSolution")
lines.append("\t\tHideSolutionNode = FALSE")
lines.append("\tEndGlobalSection")
# NestedProjects - assign folders and projects to parent folders
lines.append("\tGlobalSection(NestedProjects) = preSolution")
# Nest folders under their parents
for folder_path in sorted(all_folders.keys()):
folder = all_folders[folder_path]
if folder.parent_guid:
lines.append(f"\t\t{{{folder.guid}}} = {{{folder.parent_guid}}}")
# Nest projects under their folders
for project in sorted(all_projects, key=lambda p: p.name):
if project.path in project_to_folder:
folder_path = project_to_folder[project.path]
folder = all_folders[folder_path]
lines.append(f"\t\t{{{project.guid}}} = {{{folder.guid}}}")
lines.append("\tEndGlobalSection")
# ExtensibilityGlobals (required by VS)
lines.append("\tGlobalSection(ExtensibilityGlobals) = postSolution")
# Generate a solution GUID
sln_guid = get_deterministic_guid(sln_path, sln_path.parent.parent)
lines.append(f"\t\tSolutionGuid = {{{sln_guid}}}")
lines.append("\tEndGlobalSection")
lines.append("EndGlobal")
lines.append("") # Trailing newline
return "\r\n".join(lines)
def _get_relative_path(from_dir: Path, to_path: Path) -> str:
"""
Get relative path from directory to file, using backslashes.
Args:
from_dir: Directory to calculate from
to_path: Target path
Returns:
Relative path with backslashes (Windows format for .sln)
"""
try:
rel = to_path.relative_to(from_dir)
return str(rel).replace("/", "\\")
except ValueError:
# Different drive or not relative - use absolute with backslashes
return str(to_path).replace("/", "\\")
def has_bypass_marker(sln_path: Path) -> bool:
"""
Check if a solution file has the bypass marker.
Args:
sln_path: Path to the solution file
Returns:
True if the bypass marker is found in the first 10 lines
"""
if not sln_path.exists():
return False
try:
with open(sln_path, "r", encoding="utf-8-sig") as f:
for i, line in enumerate(f):
if i >= 10:
break
if BYPASS_MARKER in line:
return True
except Exception as e:
logger.warning(f"Failed to read solution file {sln_path}: {e}")
return False
def write_solution_file(
sln_path: Path,
content: str,
dry_run: bool = False,
) -> bool:
"""
Write solution content to file.
Args:
sln_path: Path to write to
content: Solution file content
dry_run: If True, don't actually write
Returns:
True if successful (or would be successful in dry run)
"""
if dry_run:
logger.info(f"Would write solution to: {sln_path}")
return True
try:
# Ensure parent directory exists
sln_path.parent.mkdir(parents=True, exist_ok=True)
# Write with UTF-8 BOM and CRLF line endings
with open(sln_path, "w", encoding="utf-8-sig", newline="\r\n") as f:
f.write(content)
logger.info(f"Wrote solution to: {sln_path}")
return True
except Exception as e:
logger.error(f"Failed to write solution {sln_path}: {e}")
return False