diff --git a/eng/restore-toolset.ps1 b/eng/restore-toolset.ps1
index 8d493c0690f..bf64b047733 100644
--- a/eng/restore-toolset.ps1
+++ b/eng/restore-toolset.ps1
@@ -3,18 +3,138 @@
Set-StrictMode -Off
+function Get-FileUri {
+ param([string]$path)
+
+ $fullPath = [System.IO.Path]::GetFullPath($path)
+ $builder = New-Object System.UriBuilder
+ $builder.Scheme = 'file'
+ $builder.Host = ''
+ $builder.Path = $fullPath
+ return $builder.Uri
+}
+
+function Get-RelativeSolutionPath {
+ param(
+ [string]$pathValue,
+ [string]$sourceDir,
+ [string]$targetDir
+ )
+
+ if ([string]::IsNullOrWhiteSpace($pathValue)) {
+ return $pathValue
+ }
+
+ $normalized = $pathValue.Replace('/', [System.IO.Path]::DirectorySeparatorChar)
+ $absolute = if ([System.IO.Path]::IsPathRooted($normalized)) {
+ [System.IO.Path]::GetFullPath($normalized)
+ }
+ else {
+ [System.IO.Path]::GetFullPath((Join-Path $sourceDir $normalized))
+ }
+
+ $targetDirWithSep = if ($targetDir.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
+ $targetDir
+ }
+ else {
+ $targetDir + [System.IO.Path]::DirectorySeparatorChar
+ }
+
+ $targetUri = Get-FileUri -path $targetDirWithSep
+ $absoluteUri = Get-FileUri -path $absolute
+ $relative = $targetUri.MakeRelativeUri($absoluteUri).ToString()
+ $relative = [System.Uri]::UnescapeDataString($relative)
+ return $relative.Replace('\', '/')
+}
+
+function Update-PathAttributes {
+ param(
+ [xml]$xml,
+ [string]$sourceDir,
+ [string]$targetDir
+ )
+
+ $nodes = $xml.SelectNodes("//Project[@Path] | //File[@Path]")
+ foreach ($node in $nodes) {
+ $attribute = $node.Attributes["Path"]
+ if ($null -ne $attribute) {
+ $attribute.Value = Get-RelativeSolutionPath -pathValue $attribute.Value -sourceDir $sourceDir -targetDir $targetDir
+ }
+ }
+}
+
+function Ensure-MauiTestsProject {
+ param([xml]$xml)
+
+ $testsFolder = $xml.SelectSingleNode("//Folder[@Name='/tests/Hosting/']")
+ if ($null -eq $testsFolder) {
+ return
+ }
+
+ $desiredPath = "tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj"
+ if ($null -ne $testsFolder.SelectSingleNode("Project[@Path='$desiredPath']")) {
+ return
+ }
+
+ $projectNode = $xml.CreateElement("Project")
+ $pathAttribute = $xml.CreateAttribute("Path")
+ $pathAttribute.Value = $desiredPath
+ $projectNode.Attributes.Append($pathAttribute) | Out-Null
+
+ $inserted = $false
+ $projectNodes = $testsFolder.SelectNodes("Project")
+ foreach ($existing in $projectNodes) {
+ if ([string]::Compare($desiredPath, $existing.Attributes["Path"].Value, $true) -lt 0) {
+ $testsFolder.InsertBefore($projectNode, $existing) | Out-Null
+ $inserted = $true
+ break
+ }
+ }
+
+ if (-not $inserted) {
+ $testsFolder.AppendChild($projectNode) | Out-Null
+ }
+}
+
+function Add-MauiFolder {
+ param(
+ [xml]$xml,
+ [System.Xml.XmlElement]$solutionElement
+ )
+
+ if ($null -ne $solutionElement.SelectSingleNode("Folder[@Name='/playground/AspireWithMaui/']")) {
+ return
+ }
+
+ $mauiFolderXml = @"
+
+
+
+
+
+
+
+"@
+
+ $tempDoc = [xml]"$mauiFolderXml"
+ $mauiNode = $tempDoc.DocumentElement.FirstChild
+ $importedNode = $xml.ImportNode($mauiNode, $true)
+ $solutionElement.AppendChild($importedNode) | Out-Null
+}
+
if ($restoreMaui) {
$isWindowsOrMac = ($IsWindows -or $IsMacOS -or (-not (Get-Variable -Name IsWindows -ErrorAction SilentlyContinue)))
-
+
if ($isWindowsOrMac) {
Write-Host "Installing MAUI workload..."
-
+
$dotnetCmd = if ($IsWindows -or (-not (Get-Variable -Name IsWindows -ErrorAction SilentlyContinue))) {
Join-Path $RepoRoot "dotnet.cmd"
- } else {
+ }
+ else {
Join-Path $RepoRoot "dotnet.sh"
}
-
+
& $dotnetCmd workload install maui 2>&1 | Out-Host
if ($LASTEXITCODE -ne 0) {
Write-Host ""
@@ -32,45 +152,39 @@ if ($restoreMaui) {
else {
Write-Host "Skipping MAUI workload installation on Linux (not supported)."
}
-
+
# Generate AspireWithMaui.slnx from the base Aspire.slnx
Write-Host "Generating AspireWithMaui.slnx..."
$sourceSlnx = Join-Path $RepoRoot "Aspire.slnx"
$outputPath = Join-Path $RepoRoot "playground/AspireWithMaui"
$outputSlnx = Join-Path $outputPath "AspireWithMaui.slnx"
-
+
if (-not (Test-Path $sourceSlnx)) {
Write-Warning "Source solution file not found: $sourceSlnx"
- } else {
- # Read and parse the source XML
+ }
+ else {
+ if (-not (Test-Path $outputPath)) {
+ New-Item -ItemType Directory -Force -Path $outputPath | Out-Null
+ }
+
[xml]$xml = Get-Content $sourceSlnx
$solutionElement = $xml.DocumentElement
-
- # Create the Maui folder element
- $mauiFolderXml = @"
-
-
-
-
-
-
-
-"@
-
- # Parse the Maui folder element
- $tempDoc = [xml]"$mauiFolderXml"
- $mauiNode = $tempDoc.DocumentElement.FirstChild
- $importedNode = $xml.ImportNode($mauiNode, $true)
- $solutionElement.AppendChild($importedNode) | Out-Null
-
- # Write the XML with proper formatting
+
+ Add-MauiFolder -xml $xml -solutionElement $solutionElement
+ Ensure-MauiTestsProject -xml $xml
+
+ $sourceDir = Split-Path $sourceSlnx -Parent
+ $targetDir = Split-Path $outputSlnx -Parent
+ Update-PathAttributes -xml $xml -sourceDir $sourceDir -targetDir $targetDir
+
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Indent = $true
$settings.IndentChars = " "
$settings.NewLineChars = [System.Environment]::NewLine
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
$settings.OmitXmlDeclaration = $true
-
+ $settings.Encoding = New-Object System.Text.UTF8Encoding($true)
+
$writer = [System.Xml.XmlWriter]::Create($outputSlnx, $settings)
try {
$xml.WriteTo($writer)
@@ -78,7 +192,7 @@ if ($restoreMaui) {
finally {
$writer.Dispose()
}
-
+
Write-Host "Generated AspireWithMaui.slnx at: $outputSlnx"
}
}
diff --git a/eng/restore-toolset.sh b/eng/restore-toolset.sh
index 9b856b2334c..8a7bb526c06 100644
--- a/eng/restore-toolset.sh
+++ b/eng/restore-toolset.sh
@@ -8,9 +8,9 @@ if [[ "$restore_maui" == true ]]; then
if [[ "$(uname -s)" == "Darwin" ]]; then
echo ""
echo "Installing MAUI workload..."
-
+
dotnet_sh="$repo_root/dotnet.sh"
-
+
if "$dotnet_sh" workload install maui; then
echo "MAUI workload installed successfully."
echo ""
@@ -25,41 +25,98 @@ if [[ "$restore_maui" == true ]]; then
else
echo "Skipping MAUI workload installation on Linux (not supported)."
fi
-
+
+ if ! command -v python3 >/dev/null 2>&1; then
+ echo "python3 is required to generate AspireWithMaui.slnx"
+ exit 1
+ fi
+
# Generate AspireWithMaui.slnx from the base Aspire.slnx
echo ""
echo "Generating AspireWithMaui.slnx..."
-
+
source_slnx="$repo_root/Aspire.slnx"
output_path="$repo_root/playground/AspireWithMaui"
output_slnx="$output_path/AspireWithMaui.slnx"
-
+
if [ ! -f "$source_slnx" ]; then
echo "WARNING: Source solution file not found: $source_slnx"
else
- # Create a temporary file
- temp_file=$(mktemp)
- trap "rm -f $temp_file" EXIT
-
- # Copy the source file
- cp "$source_slnx" "$temp_file"
-
- # Insert the Maui folder before the closing tag
- # Using a here-doc approach for BSD sed compatibility
- sed -i.bak '/<\/Solution>/i\
- \
- \
- \
- \
- \
- \
-
-' "$temp_file"
- rm -f "$temp_file.bak"
- # Write UTF-8 BOM and append temp file to output location
- printf '\xEF\xBB\xBF' > "$output_slnx"
- cat "$temp_file" >> "$output_slnx"
-
+ mkdir -p "$output_path"
+
+ python3 <<'PY' "$source_slnx" "$output_slnx"
+import codecs
+import os
+import re
+import sys
+
+source = os.path.abspath(sys.argv[1])
+target = os.path.abspath(sys.argv[2])
+source_dir = os.path.dirname(source)
+target_dir = os.path.dirname(target)
+
+with open(source, 'rb') as handle:
+ text = handle.read().decode('utf-8-sig')
+
+maui_folder_marker = 'playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj'
+if maui_folder_marker not in text:
+ folder_block = (
+ '\r\n \r\n'
+ ' \r\n'
+ ' \r\n'
+ ' \r\n'
+ ' \r\n'
+ ' \r\n'
+ ' \r\n'
+ )
+ text = text.replace('\r\n', f'{folder_block}', 1)
+
+tests_folder_pattern = re.compile(r'(\r?\n)(.*?)( )', re.DOTALL)
+match = tests_folder_pattern.search(text)
+desired_line = ' \r\n'
+desired_path = 'tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj'
+if match:
+ body = match.group(2)
+ if desired_path not in body:
+ lines = body.splitlines(keepends=True)
+
+ def extract_path(line: str) -> str:
+ hit = re.search(r'Path="([^"]+)"', line)
+ return hit.group(1) if hit else ''
+
+ inserted = False
+ for index, line in enumerate(lines):
+ existing_path = extract_path(line)
+ if existing_path and desired_path.lower() < existing_path.lower():
+ lines.insert(index, desired_line)
+ inserted = True
+ break
+ if not inserted:
+ lines.append(desired_line)
+
+ new_body = ''.join(lines)
+ text = text[:match.start(2)] + new_body + text[match.end(2):]
+
+def resolve_relative(value: str) -> str:
+ normalized = value.replace('\\', '/').replace('/', os.sep)
+ if os.path.isabs(normalized):
+ absolute = os.path.normpath(normalized)
+ else:
+ absolute = os.path.normpath(os.path.join(source_dir, normalized))
+ relative = os.path.relpath(absolute, target_dir)
+ return relative.replace(os.sep, '/')
+
+def substitute(match: re.Match) -> str:
+ original = match.group(1)
+ return f'Path="{resolve_relative(original)}"'
+
+text = re.sub(r'Path="([^"]+)"', substitute, text)
+
+with open(target, 'wb') as handle:
+ handle.write(codecs.BOM_UTF8)
+ handle.write(text.encode('utf-8'))
+PY
+
echo "Generated AspireWithMaui.slnx at: $output_slnx"
fi
fi
diff --git a/playground/AspireWithMaui/AspireWithMaui.slnx b/playground/AspireWithMaui/AspireWithMaui.slnx
index 34b188bdbf1..da2aad210a6 100644
--- a/playground/AspireWithMaui/AspireWithMaui.slnx
+++ b/playground/AspireWithMaui/AspireWithMaui.slnx
@@ -5,471 +5,472 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
-
-
+
+
+
+
-
+
-
-
-
+
+
+
-
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
+
-
-
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
-
-
+
+
+
+
+
-
+
\ No newline at end of file