Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 143 additions & 29 deletions eng/restore-toolset.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"
<Folder Name="/playground/AspireWithMaui/">
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />
</Folder>
"@

$tempDoc = [xml]"<root>$mauiFolderXml</root>"
$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 ""
Expand All @@ -32,53 +152,47 @@ 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 = @"
<Folder Name="/playground/AspireWithMaui/">
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />
</Folder>
"@

# Parse the Maui folder element
$tempDoc = [xml]"<root>$mauiFolderXml</root>"
$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)
}
finally {
$writer.Dispose()
}

Write-Host "Generated AspireWithMaui.slnx at: $outputSlnx"
}
}
113 changes: 85 additions & 28 deletions eng/restore-toolset.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand All @@ -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 </Solution> tag
# Using a here-doc approach for BSD sed compatibility
sed -i.bak '/<\/Solution>/i\
<Folder Name="/playground/AspireWithMaui/">\
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />\
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />\
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />\
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />\
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />\
</Folder>
' "$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 <Folder Name="/playground/AspireWithMaui/">\r\n'
' <Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />\r\n'
' <Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />\r\n'
' <Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />\r\n'
' <Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />\r\n'
' <Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />\r\n'
' </Folder>\r\n'
)
text = text.replace('\r\n</Solution>', f'{folder_block}</Solution>', 1)

tests_folder_pattern = re.compile(r'(<Folder Name="/tests/Hosting/">\r?\n)(.*?)( </Folder>)', re.DOTALL)
match = tests_folder_pattern.search(text)
desired_line = ' <Project Path="tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj" />\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
Loading