Skip to content

Commit 600f309

Browse files
authored
Fix relative path in AspireWithMaui.slnx (#13020)
1 parent ba535d7 commit 600f309

File tree

3 files changed

+562
-390
lines changed

3 files changed

+562
-390
lines changed

eng/restore-toolset.ps1

Lines changed: 143 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,138 @@
33

44
Set-StrictMode -Off
55

6+
function Get-FileUri {
7+
param([string]$path)
8+
9+
$fullPath = [System.IO.Path]::GetFullPath($path)
10+
$builder = New-Object System.UriBuilder
11+
$builder.Scheme = 'file'
12+
$builder.Host = ''
13+
$builder.Path = $fullPath
14+
return $builder.Uri
15+
}
16+
17+
function Get-RelativeSolutionPath {
18+
param(
19+
[string]$pathValue,
20+
[string]$sourceDir,
21+
[string]$targetDir
22+
)
23+
24+
if ([string]::IsNullOrWhiteSpace($pathValue)) {
25+
return $pathValue
26+
}
27+
28+
$normalized = $pathValue.Replace('/', [System.IO.Path]::DirectorySeparatorChar)
29+
$absolute = if ([System.IO.Path]::IsPathRooted($normalized)) {
30+
[System.IO.Path]::GetFullPath($normalized)
31+
}
32+
else {
33+
[System.IO.Path]::GetFullPath((Join-Path $sourceDir $normalized))
34+
}
35+
36+
$targetDirWithSep = if ($targetDir.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
37+
$targetDir
38+
}
39+
else {
40+
$targetDir + [System.IO.Path]::DirectorySeparatorChar
41+
}
42+
43+
$targetUri = Get-FileUri -path $targetDirWithSep
44+
$absoluteUri = Get-FileUri -path $absolute
45+
$relative = $targetUri.MakeRelativeUri($absoluteUri).ToString()
46+
$relative = [System.Uri]::UnescapeDataString($relative)
47+
return $relative.Replace('\', '/')
48+
}
49+
50+
function Update-PathAttributes {
51+
param(
52+
[xml]$xml,
53+
[string]$sourceDir,
54+
[string]$targetDir
55+
)
56+
57+
$nodes = $xml.SelectNodes("//Project[@Path] | //File[@Path]")
58+
foreach ($node in $nodes) {
59+
$attribute = $node.Attributes["Path"]
60+
if ($null -ne $attribute) {
61+
$attribute.Value = Get-RelativeSolutionPath -pathValue $attribute.Value -sourceDir $sourceDir -targetDir $targetDir
62+
}
63+
}
64+
}
65+
66+
function Ensure-MauiTestsProject {
67+
param([xml]$xml)
68+
69+
$testsFolder = $xml.SelectSingleNode("//Folder[@Name='/tests/Hosting/']")
70+
if ($null -eq $testsFolder) {
71+
return
72+
}
73+
74+
$desiredPath = "tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj"
75+
if ($null -ne $testsFolder.SelectSingleNode("Project[@Path='$desiredPath']")) {
76+
return
77+
}
78+
79+
$projectNode = $xml.CreateElement("Project")
80+
$pathAttribute = $xml.CreateAttribute("Path")
81+
$pathAttribute.Value = $desiredPath
82+
$projectNode.Attributes.Append($pathAttribute) | Out-Null
83+
84+
$inserted = $false
85+
$projectNodes = $testsFolder.SelectNodes("Project")
86+
foreach ($existing in $projectNodes) {
87+
if ([string]::Compare($desiredPath, $existing.Attributes["Path"].Value, $true) -lt 0) {
88+
$testsFolder.InsertBefore($projectNode, $existing) | Out-Null
89+
$inserted = $true
90+
break
91+
}
92+
}
93+
94+
if (-not $inserted) {
95+
$testsFolder.AppendChild($projectNode) | Out-Null
96+
}
97+
}
98+
99+
function Add-MauiFolder {
100+
param(
101+
[xml]$xml,
102+
[System.Xml.XmlElement]$solutionElement
103+
)
104+
105+
if ($null -ne $solutionElement.SelectSingleNode("Folder[@Name='/playground/AspireWithMaui/']")) {
106+
return
107+
}
108+
109+
$mauiFolderXml = @"
110+
<Folder Name="/playground/AspireWithMaui/">
111+
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />
112+
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />
113+
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />
114+
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />
115+
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />
116+
</Folder>
117+
"@
118+
119+
$tempDoc = [xml]"<root>$mauiFolderXml</root>"
120+
$mauiNode = $tempDoc.DocumentElement.FirstChild
121+
$importedNode = $xml.ImportNode($mauiNode, $true)
122+
$solutionElement.AppendChild($importedNode) | Out-Null
123+
}
124+
6125
if ($restoreMaui) {
7126
$isWindowsOrMac = ($IsWindows -or $IsMacOS -or (-not (Get-Variable -Name IsWindows -ErrorAction SilentlyContinue)))
8-
127+
9128
if ($isWindowsOrMac) {
10129
Write-Host "Installing MAUI workload..."
11-
130+
12131
$dotnetCmd = if ($IsWindows -or (-not (Get-Variable -Name IsWindows -ErrorAction SilentlyContinue))) {
13132
Join-Path $RepoRoot "dotnet.cmd"
14-
} else {
133+
}
134+
else {
15135
Join-Path $RepoRoot "dotnet.sh"
16136
}
17-
137+
18138
& $dotnetCmd workload install maui 2>&1 | Out-Host
19139
if ($LASTEXITCODE -ne 0) {
20140
Write-Host ""
@@ -32,53 +152,47 @@ if ($restoreMaui) {
32152
else {
33153
Write-Host "Skipping MAUI workload installation on Linux (not supported)."
34154
}
35-
155+
36156
# Generate AspireWithMaui.slnx from the base Aspire.slnx
37157
Write-Host "Generating AspireWithMaui.slnx..."
38158
$sourceSlnx = Join-Path $RepoRoot "Aspire.slnx"
39159
$outputPath = Join-Path $RepoRoot "playground/AspireWithMaui"
40160
$outputSlnx = Join-Path $outputPath "AspireWithMaui.slnx"
41-
161+
42162
if (-not (Test-Path $sourceSlnx)) {
43163
Write-Warning "Source solution file not found: $sourceSlnx"
44-
} else {
45-
# Read and parse the source XML
164+
}
165+
else {
166+
if (-not (Test-Path $outputPath)) {
167+
New-Item -ItemType Directory -Force -Path $outputPath | Out-Null
168+
}
169+
46170
[xml]$xml = Get-Content $sourceSlnx
47171
$solutionElement = $xml.DocumentElement
48-
49-
# Create the Maui folder element
50-
$mauiFolderXml = @"
51-
<Folder Name="/playground/AspireWithMaui/">
52-
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />
53-
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />
54-
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />
55-
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />
56-
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />
57-
</Folder>
58-
"@
59-
60-
# Parse the Maui folder element
61-
$tempDoc = [xml]"<root>$mauiFolderXml</root>"
62-
$mauiNode = $tempDoc.DocumentElement.FirstChild
63-
$importedNode = $xml.ImportNode($mauiNode, $true)
64-
$solutionElement.AppendChild($importedNode) | Out-Null
65-
66-
# Write the XML with proper formatting
172+
173+
Add-MauiFolder -xml $xml -solutionElement $solutionElement
174+
Ensure-MauiTestsProject -xml $xml
175+
176+
$sourceDir = Split-Path $sourceSlnx -Parent
177+
$targetDir = Split-Path $outputSlnx -Parent
178+
Update-PathAttributes -xml $xml -sourceDir $sourceDir -targetDir $targetDir
179+
67180
$settings = New-Object System.Xml.XmlWriterSettings
68181
$settings.Indent = $true
69182
$settings.IndentChars = " "
70183
$settings.NewLineChars = [System.Environment]::NewLine
71184
$settings.NewLineHandling = [System.Xml.NewLineHandling]::Replace
72185
$settings.OmitXmlDeclaration = $true
73-
186+
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
187+
74188
$writer = [System.Xml.XmlWriter]::Create($outputSlnx, $settings)
75189
try {
76190
$xml.WriteTo($writer)
77191
}
78192
finally {
79193
$writer.Dispose()
80194
}
81-
195+
82196
Write-Host "Generated AspireWithMaui.slnx at: $outputSlnx"
83197
}
84198
}

eng/restore-toolset.sh

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ if [[ "$restore_maui" == true ]]; then
88
if [[ "$(uname -s)" == "Darwin" ]]; then
99
echo ""
1010
echo "Installing MAUI workload..."
11-
11+
1212
dotnet_sh="$repo_root/dotnet.sh"
13-
13+
1414
if "$dotnet_sh" workload install maui; then
1515
echo "MAUI workload installed successfully."
1616
echo ""
@@ -25,41 +25,98 @@ if [[ "$restore_maui" == true ]]; then
2525
else
2626
echo "Skipping MAUI workload installation on Linux (not supported)."
2727
fi
28-
28+
29+
if ! command -v python3 >/dev/null 2>&1; then
30+
echo "python3 is required to generate AspireWithMaui.slnx"
31+
exit 1
32+
fi
33+
2934
# Generate AspireWithMaui.slnx from the base Aspire.slnx
3035
echo ""
3136
echo "Generating AspireWithMaui.slnx..."
32-
37+
3338
source_slnx="$repo_root/Aspire.slnx"
3439
output_path="$repo_root/playground/AspireWithMaui"
3540
output_slnx="$output_path/AspireWithMaui.slnx"
36-
41+
3742
if [ ! -f "$source_slnx" ]; then
3843
echo "WARNING: Source solution file not found: $source_slnx"
3944
else
40-
# Create a temporary file
41-
temp_file=$(mktemp)
42-
trap "rm -f $temp_file" EXIT
43-
44-
# Copy the source file
45-
cp "$source_slnx" "$temp_file"
46-
47-
# Insert the Maui folder before the closing </Solution> tag
48-
# Using a here-doc approach for BSD sed compatibility
49-
sed -i.bak '/<\/Solution>/i\
50-
<Folder Name="/playground/AspireWithMaui/">\
51-
<Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />\
52-
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />\
53-
<Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />\
54-
<Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />\
55-
<Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />\
56-
</Folder>
57-
' "$temp_file"
58-
rm -f "$temp_file.bak"
59-
# Write UTF-8 BOM and append temp file to output location
60-
printf '\xEF\xBB\xBF' > "$output_slnx"
61-
cat "$temp_file" >> "$output_slnx"
62-
45+
mkdir -p "$output_path"
46+
47+
python3 <<'PY' "$source_slnx" "$output_slnx"
48+
import codecs
49+
import os
50+
import re
51+
import sys
52+
53+
source = os.path.abspath(sys.argv[1])
54+
target = os.path.abspath(sys.argv[2])
55+
source_dir = os.path.dirname(source)
56+
target_dir = os.path.dirname(target)
57+
58+
with open(source, 'rb') as handle:
59+
text = handle.read().decode('utf-8-sig')
60+
61+
maui_folder_marker = 'playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj'
62+
if maui_folder_marker not in text:
63+
folder_block = (
64+
'\r\n <Folder Name="/playground/AspireWithMaui/">\r\n'
65+
' <Project Path="playground/AspireWithMaui/AspireWithMaui.AppHost/AspireWithMaui.AppHost.csproj" />\r\n'
66+
' <Project Path="playground/AspireWithMaui/AspireWithMaui.MauiClient/AspireWithMaui.MauiClient.csproj" />\r\n'
67+
' <Project Path="playground/AspireWithMaui/AspireWithMaui.MauiServiceDefaults/AspireWithMaui.MauiServiceDefaults.csproj" />\r\n'
68+
' <Project Path="playground/AspireWithMaui/AspireWithMaui.ServiceDefaults/AspireWithMaui.ServiceDefaults.csproj" />\r\n'
69+
' <Project Path="playground/AspireWithMaui/AspireWithMaui.WeatherApi/AspireWithMaui.WeatherApi.csproj" />\r\n'
70+
' </Folder>\r\n'
71+
)
72+
text = text.replace('\r\n</Solution>', f'{folder_block}</Solution>', 1)
73+
74+
tests_folder_pattern = re.compile(r'(<Folder Name="/tests/Hosting/">\r?\n)(.*?)( </Folder>)', re.DOTALL)
75+
match = tests_folder_pattern.search(text)
76+
desired_line = ' <Project Path="tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj" />\r\n'
77+
desired_path = 'tests/Aspire.Hosting.Maui.Tests/Aspire.Hosting.Maui.Tests.csproj'
78+
if match:
79+
body = match.group(2)
80+
if desired_path not in body:
81+
lines = body.splitlines(keepends=True)
82+
83+
def extract_path(line: str) -> str:
84+
hit = re.search(r'Path="([^"]+)"', line)
85+
return hit.group(1) if hit else ''
86+
87+
inserted = False
88+
for index, line in enumerate(lines):
89+
existing_path = extract_path(line)
90+
if existing_path and desired_path.lower() < existing_path.lower():
91+
lines.insert(index, desired_line)
92+
inserted = True
93+
break
94+
if not inserted:
95+
lines.append(desired_line)
96+
97+
new_body = ''.join(lines)
98+
text = text[:match.start(2)] + new_body + text[match.end(2):]
99+
100+
def resolve_relative(value: str) -> str:
101+
normalized = value.replace('\\', '/').replace('/', os.sep)
102+
if os.path.isabs(normalized):
103+
absolute = os.path.normpath(normalized)
104+
else:
105+
absolute = os.path.normpath(os.path.join(source_dir, normalized))
106+
relative = os.path.relpath(absolute, target_dir)
107+
return relative.replace(os.sep, '/')
108+
109+
def substitute(match: re.Match) -> str:
110+
original = match.group(1)
111+
return f'Path="{resolve_relative(original)}"'
112+
113+
text = re.sub(r'Path="([^"]+)"', substitute, text)
114+
115+
with open(target, 'wb') as handle:
116+
handle.write(codecs.BOM_UTF8)
117+
handle.write(text.encode('utf-8'))
118+
PY
119+
63120
echo "Generated AspireWithMaui.slnx at: $output_slnx"
64121
fi
65122
fi

0 commit comments

Comments
 (0)