Skip to content

Commit ebfa1ed

Browse files
authored
Harden SDK tool resolution and dependency preflight (#735)
* centralize .NET Framework SDK tool and assembly resolution in BuildUtils for reuse * simplify ProjectLocalizer to use shared resolver methods for resgen, al, and framework assembly references * add dependency preflight integration to build.ps1 and test.ps1 (with -SkipDependencyCheck escape hatch) * extend Verify-FwDependencies.ps1 to validate ResGen.exe and al.exe, and accept .NET Framework targeting pack 4.8 or 4.8.1
1 parent fce6918 commit ebfa1ed

5 files changed

Lines changed: 248 additions & 14 deletions

File tree

Build/Agent/Verify-FwDependencies.ps1

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,33 @@ function Test-Dependency {
7171
}
7272
}
7373

74+
function Find-DotNetFrameworkSdkTool {
75+
param([Parameter(Mandatory)][string]$ToolName)
76+
77+
$programFilesX86 = ${env:ProgramFiles(x86)}
78+
if (-not $programFilesX86) { return $null }
79+
80+
$sdkBase = Join-Path $programFilesX86 'Microsoft SDKs\Windows\v10.0A\bin'
81+
if (-not (Test-Path $sdkBase)) { return $null }
82+
83+
$toolCandidates = @()
84+
$netfxDirs = Get-ChildItem -Path $sdkBase -Directory -Filter 'NETFX*' -ErrorAction SilentlyContinue |
85+
Sort-Object Name -Descending
86+
87+
foreach ($dir in $netfxDirs) {
88+
$toolCandidates += (Join-Path $dir.FullName (Join-Path 'x64' $ToolName))
89+
$toolCandidates += (Join-Path $dir.FullName $ToolName)
90+
}
91+
92+
foreach ($candidate in $toolCandidates) {
93+
if (Test-Path $candidate) {
94+
return $candidate
95+
}
96+
}
97+
98+
return $null
99+
}
100+
74101
# ============================================================================
75102
# MAIN SCRIPT
76103
# ============================================================================
@@ -85,11 +112,17 @@ $results = @()
85112
# ----------------------------------------------------------------------------
86113
Write-Host "--- Required Dependencies ---" -ForegroundColor Cyan
87114

88-
# .NET Framework 4.8.1 Targeting Pack
89-
$results += Test-Dependency -Name ".NET Framework 4.8.1 Targeting Pack" -Check {
90-
$path = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8.1"
91-
if (Test-Path $path) { return $path }
92-
throw "Not found at $path"
115+
# .NET Framework targeting pack (4.8+)
116+
$results += Test-Dependency -Name ".NET Framework Targeting Pack (4.8+)" -Check {
117+
$base = "${env:ProgramFiles(x86)}\Reference Assemblies\Microsoft\Framework\.NETFramework"
118+
$candidates = @('v4.8.1', 'v4.8')
119+
foreach ($version in $candidates) {
120+
$path = Join-Path $base $version
121+
if (Test-Path $path) {
122+
return "$version at $path"
123+
}
124+
}
125+
throw "Neither v4.8.1 nor v4.8 targeting pack was found under $base"
93126
}
94127

95128
# Windows SDK
@@ -131,6 +164,19 @@ $results += Test-Dependency -Name "MSBuild" -Check {
131164
throw "MSBuild not found in PATH or VS installation"
132165
}
133166

167+
# .NET Framework SDK tools used by localization tasks
168+
$results += Test-Dependency -Name "ResGen.exe (.NET Framework SDK)" -Check {
169+
$resgen = Find-DotNetFrameworkSdkTool -ToolName 'ResGen.exe'
170+
if ($resgen) { return $resgen }
171+
throw "ResGen.exe not found in Windows SDK NETFX tool folders"
172+
}
173+
174+
$results += Test-Dependency -Name "al.exe (.NET Framework SDK)" -Check {
175+
$al = Find-DotNetFrameworkSdkTool -ToolName 'al.exe'
176+
if ($al) { return $al }
177+
throw "al.exe not found in Windows SDK NETFX tool folders"
178+
}
179+
134180
# NuGet CLI (legacy — build uses dotnet restore since CPM migration)
135181
$results += Test-Dependency -Name "NuGet CLI (legacy)" -Required "Optional" -Check {
136182
$nuget = Get-Command nuget.exe -ErrorAction SilentlyContinue

Build/Src/FwBuildTasks/BuildUtils.cs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Diagnostics;
88
using System.IO;
9+
using System.Linq;
910
using System.Reflection;
1011
using System.Runtime.InteropServices;
1112
using System.Text.RegularExpressions;
@@ -22,6 +23,8 @@ namespace FwBuildTasks
2223
public static class BuildUtils
2324
{
2425
public static bool IsUnix => Environment.OSVersion.Platform == PlatformID.Unix;
26+
private const string NetFxLegacyVersionFolder = "v4.0.30319";
27+
private static readonly string[] NetFxFrameworkFolders = { "Framework64", "Framework" };
2528

2629
/// <summary>
2730
/// Return the executing assembly's location as a directory path.
@@ -204,5 +207,167 @@ public static bool ParseSymbolFile(string symbolFile, TaskLoggingHelper log, out
204207
}
205208
return true;
206209
}
210+
211+
public static string ResolveDotNetFrameworkSdkTool(string toolName)
212+
{
213+
if (IsUnix)
214+
return Path.GetFileNameWithoutExtension(toolName);
215+
216+
var probeLog = new List<string>();
217+
foreach (var sdkPath in GetToolLocationHelperCandidates(toolName, probeLog))
218+
{
219+
if (File.Exists(sdkPath))
220+
return sdkPath;
221+
}
222+
223+
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
224+
if (!string.IsNullOrEmpty(programFilesX86))
225+
{
226+
var sdkBase = Path.Combine(programFilesX86, "Microsoft SDKs", "Windows", "v10.0A", "bin");
227+
if (Directory.Exists(sdkBase))
228+
{
229+
var netfxDirs = Directory.GetDirectories(sdkBase, "NETFX*")
230+
.OrderByDescending(d => d)
231+
.ToList();
232+
foreach (var dir in netfxDirs)
233+
{
234+
var x64Path = Path.Combine(dir, "x64", toolName);
235+
probeLog.Add(x64Path);
236+
if (File.Exists(x64Path))
237+
return x64Path;
238+
239+
var anyPath = Path.Combine(dir, toolName);
240+
probeLog.Add(anyPath);
241+
if (File.Exists(anyPath))
242+
return anyPath;
243+
}
244+
}
245+
}
246+
247+
if (IsToolOnPath(toolName))
248+
return toolName;
249+
250+
throw new FileNotFoundException(
251+
$"Unable to locate required SDK tool '{toolName}'. " +
252+
"Install Visual Studio Build Tools with .NET Framework 4.8 SDK/targeting pack or run build.ps1/test.ps1 from this repo. " +
253+
$"Probed: {string.Join("; ", probeLog.Distinct())}");
254+
}
255+
256+
public static string ResolveDotNetFrameworkAssemblyPath(string assemblyFileName)
257+
{
258+
var probeLog = new List<string>();
259+
foreach (var candidate in GetFrameworkAssemblyCandidates(assemblyFileName, probeLog))
260+
{
261+
if (File.Exists(candidate))
262+
return candidate;
263+
}
264+
265+
throw new FileNotFoundException(
266+
$"Unable to locate required .NET Framework assembly '{assemblyFileName}' for resgen. " +
267+
$"Probed: {string.Join("; ", probeLog.Distinct())}");
268+
}
269+
270+
private static IEnumerable<string> GetToolLocationHelperCandidates(string toolName, ICollection<string> probeLog)
271+
{
272+
var candidates = new List<string>();
273+
try
274+
{
275+
candidates.Add(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(
276+
toolName,
277+
TargetDotNetFrameworkVersion.Version48,
278+
VisualStudioVersion.Version170,
279+
DotNetFrameworkArchitecture.Bitness64));
280+
}
281+
catch
282+
{
283+
}
284+
285+
try
286+
{
287+
candidates.Add(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(
288+
toolName,
289+
TargetDotNetFrameworkVersion.Version48,
290+
DotNetFrameworkArchitecture.Bitness64));
291+
}
292+
catch
293+
{
294+
}
295+
296+
try
297+
{
298+
candidates.Add(ToolLocationHelper.GetPathToDotNetFrameworkSdkFile(
299+
toolName,
300+
TargetDotNetFrameworkVersion.Version48));
301+
}
302+
catch
303+
{
304+
}
305+
306+
foreach (var candidate in candidates.Where(c => !string.IsNullOrEmpty(c)))
307+
{
308+
probeLog.Add(candidate);
309+
yield return candidate;
310+
}
311+
}
312+
313+
private static IEnumerable<string> GetFrameworkAssemblyCandidates(string assemblyFileName, ICollection<string> probeLog)
314+
{
315+
var candidates = new List<string>();
316+
try
317+
{
318+
var sdkPath = ToolLocationHelper.GetPathToDotNetFrameworkFile(
319+
assemblyFileName,
320+
TargetDotNetFrameworkVersion.Version48);
321+
if (!string.IsNullOrEmpty(sdkPath))
322+
candidates.Add(sdkPath);
323+
}
324+
catch
325+
{
326+
}
327+
328+
var runtimeDirectory = RuntimeEnvironment.GetRuntimeDirectory();
329+
if (!string.IsNullOrEmpty(runtimeDirectory))
330+
{
331+
var runtimeCandidate = Path.Combine(runtimeDirectory, assemblyFileName);
332+
candidates.Add(runtimeCandidate);
333+
}
334+
335+
var windowsFolder = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
336+
if (!string.IsNullOrEmpty(windowsFolder))
337+
{
338+
foreach (var frameworkFolder in NetFxFrameworkFolders)
339+
{
340+
var candidate = Path.Combine(
341+
windowsFolder,
342+
"Microsoft.NET",
343+
frameworkFolder,
344+
NetFxLegacyVersionFolder,
345+
assemblyFileName);
346+
candidates.Add(candidate);
347+
}
348+
}
349+
350+
foreach (var candidate in candidates)
351+
{
352+
probeLog.Add(candidate);
353+
yield return candidate;
354+
}
355+
}
356+
357+
private static bool IsToolOnPath(string toolName)
358+
{
359+
var path = Environment.GetEnvironmentVariable("PATH");
360+
if (string.IsNullOrEmpty(path))
361+
return false;
362+
363+
foreach (var entry in path.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
364+
{
365+
var candidate = Path.Combine(entry.Trim(), toolName);
366+
if (File.Exists(candidate))
367+
return true;
368+
}
369+
370+
return false;
371+
}
207372
}
208373
}

Build/Src/FwBuildTasks/Localization/ProjectLocalizer.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
using System.Globalization;
1010
using System.IO;
1111
using System.Linq;
12-
using System.Runtime.InteropServices;
1312
using System.Text;
1413
using System.Threading;
1514
using System.Xml.Linq;
1615
using System.Xml.XPath;
16+
using FwBuildTasks;
1717
using Microsoft.Build.Framework;
1818

1919
// ReSharper disable AssignNullToNotNullAttribute - System.IO is hypocritical in its null handling
@@ -294,7 +294,7 @@ protected virtual void RunAssemblyLinker(string outputDllPath, string culture,
294294
{
295295
// Run assembly linker with the specified arguments
296296
Directory.CreateDirectory(Path.GetDirectoryName(outputDllPath)); // make sure the directory in which we want to make it exists.
297-
var fileName = IsUnix ? "al" : "al.exe";
297+
var fileName = BuildUtils.ResolveDotNetFrameworkSdkTool(IsUnix ? "al" : "al.exe");
298298
var arguments = BuildLinkerArgs(outputDllPath, culture, fileversion,
299299
productVersion, version, resources);
300300
var exitCode = RunProcess(fileName, arguments, out var stdOutput);
@@ -380,14 +380,13 @@ protected static string BuildLinkerArgs(string outputDllPath, string culture, st
380380
protected virtual void RunResGen(string outputResourcePath, string localizedResxPath,
381381
string originalResxFolder)
382382
{
383-
var fileName = IsUnix ? "resgen" : "resgen.exe";
383+
var fileName = BuildUtils.ResolveDotNetFrameworkSdkTool(IsUnix ? "resgen" : "resgen.exe");
384384
var arguments = $"\"{localizedResxPath}\" \"{outputResourcePath}\"";
385385
if (!IsUnix)
386386
{
387-
// It needs to be able to reference the appropriate System.Drawing.dll and System.Windows.Forms.dll to make the conversion.
388-
var clrFolder = RuntimeEnvironment.GetRuntimeDirectory();
389-
var drawingPath = Path.Combine(clrFolder, "System.Drawing.dll");
390-
var formsPath = Path.Combine(clrFolder, "System.Windows.Forms.dll");
387+
// resgen needs System.Drawing.dll and System.Windows.Forms.dll to process designer resx files.
388+
var drawingPath = BuildUtils.ResolveDotNetFrameworkAssemblyPath("System.Drawing.dll");
389+
var formsPath = BuildUtils.ResolveDotNetFrameworkAssemblyPath("System.Windows.Forms.dll");
391390

392391
arguments += $" /r:\"{drawingPath}\" /r:\"{formsPath}\"";
393392
}

build.ps1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@ param(
134134
[switch]$SignInstaller,
135135
[switch]$TraceCrashes,
136136
[switch]$UseLocalLcm,
137-
[string]$LocalLcmPath
137+
[string]$LocalLcmPath,
138+
[switch]$SkipDependencyCheck
138139
)
139140

140141
$ErrorActionPreference = "Stop"
@@ -273,6 +274,17 @@ try {
273274
Initialize-VsDevEnvironment
274275
Test-CvtresCompatibility
275276

277+
if (-not $SkipDependencyCheck) {
278+
$verifyScript = Join-Path $PSScriptRoot "Build/Agent/Verify-FwDependencies.ps1"
279+
if (Test-Path $verifyScript) {
280+
Write-Host "Running dependency preflight..." -ForegroundColor Cyan
281+
& $verifyScript -FailOnMissing
282+
if ($LASTEXITCODE -ne 0) {
283+
throw "Dependency preflight failed. Re-run with -SkipDependencyCheck only if you are actively debugging environment setup."
284+
}
285+
}
286+
}
287+
276288
# Set architecture environment variable (x64-only)
277289
$env:arch = 'x64'
278290
Write-Host "Set arch environment variable to: $env:arch" -ForegroundColor Green

test.ps1

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ param(
5454
[switch]$ListTests,
5555
[ValidateSet('quiet', 'minimal', 'normal', 'detailed', 'q', 'm', 'n', 'd')]
5656
[string]$Verbosity = "normal",
57-
[switch]$Native
57+
[switch]$Native,
58+
[switch]$SkipDependencyCheck
5859
)
5960

6061
$ErrorActionPreference = 'Stop'
@@ -89,6 +90,17 @@ try {
8990
Initialize-VsDevEnvironment
9091
Test-CvtresCompatibility
9192

93+
if (-not $SkipDependencyCheck) {
94+
$verifyScript = Join-Path $PSScriptRoot "Build/Agent/Verify-FwDependencies.ps1"
95+
if (Test-Path $verifyScript) {
96+
Write-Host "Running dependency preflight..." -ForegroundColor Cyan
97+
& $verifyScript -FailOnMissing
98+
if ($LASTEXITCODE -ne 0) {
99+
throw "Dependency preflight failed. Re-run with -SkipDependencyCheck only if you are actively debugging environment setup."
100+
}
101+
}
102+
}
103+
92104
# Set architecture (x64-only)
93105
$env:arch = 'x64'
94106

0 commit comments

Comments
 (0)