From 00d157be3bc0854bf0f2c5d9269b5366c984b228 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Tue, 3 Nov 2026 14:34:33 +0100 Subject: [PATCH 1/2] fix(manifest): read .NET projects without running their targets The dotnet facts tool ran a design-time Compile build of every project for --with-files (what --reach uses). That executes the repository's own MSBuild targets, writes generated files into obj/, and fails outright on projects without a compiler, such as Microsoft.Build.Traversal aggregators (MSB4057: CoreCompile does not exist). Read every project by evaluation alone instead. The sidecar loses references, analyzers and buildError, which Coana does not read; sources no longer include target-generated files, and define constants no longer include the SDK's implicit framework symbols. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 + src/commands/manifest/scripts/assemble.mts | 2 - .../manifest/scripts/assemble.test.mts | 16 +- .../scripts/dotnet-tool/FactsRunner.cs | 1 - .../manifest/scripts/dotnet-tool/Reader.cs | 181 +++--------------- src/commands/manifest/scripts/facts.mts | 11 +- src/commands/manifest/scripts/records.mts | 8 - .../scripts/resolution-report-nuget.mts | 4 +- .../scripts/resolution-report-nuget.test.mts | 3 +- .../test/dotnet-compat/assert-facts.py | 39 ++-- .../dotnet-compat/evaluation/Build.csproj | 9 + .../dotnet-compat/evaluation/Lib/Class.cs | 3 + .../dotnet-compat/evaluation/Lib/Lib.csproj | 8 + .../failures/designtime/DesignTime.csproj | 8 - .../test/dotnet-compat/langs/Fs/First.fs | 3 + .../test/dotnet-compat/langs/Fs/Fs.fsproj | 5 +- .../scripts/test/dotnet-compat/smoke-test.sh | 2 +- 17 files changed, 89 insertions(+), 219 deletions(-) create mode 100644 src/commands/manifest/scripts/test/dotnet-compat/evaluation/Build.csproj create mode 100644 src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Class.cs create mode 100644 src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Lib.csproj delete mode 100644 src/commands/manifest/scripts/test/dotnet-compat/failures/designtime/DesignTime.csproj create mode 100644 src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/First.fs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee4c197c1..60d156648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [Unreleased] + +### Fixed +- Reachability scans of .NET projects now read your projects without running any of their build targets, so builds with compiler-less projects such as `Microsoft.Build.Traversal` aggregators no longer fail. + ## [1.13.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.13.0) - 2026-11-03 ### Added diff --git a/src/commands/manifest/scripts/assemble.mts b/src/commands/manifest/scripts/assemble.mts index 73520aeff..b2ca641c8 100644 --- a/src/commands/manifest/scripts/assemble.mts +++ b/src/commands/manifest/scripts/assemble.mts @@ -546,8 +546,6 @@ function buildFrameworks( })), // In compiler order, which F# depends on. sources: [...new Set(build?.sources)].filter(exists), - references: [...new Set(build?.references)].filter(exists), - analyzers: [...new Set(build?.analyzers)].filter(exists), targets: sortedUnique(build?.targets ?? []).filter(exists), dependencies: [], classpath: [], diff --git a/src/commands/manifest/scripts/assemble.test.mts b/src/commands/manifest/scripts/assemble.test.mts index 53a717c23..a43125640 100644 --- a/src/commands/manifest/scripts/assemble.test.mts +++ b/src/commands/manifest/scripts/assemble.test.mts @@ -654,17 +654,13 @@ describe('dotnet records → per-framework sidecar', () => { 'meta\tdotnet\t8.0.414\t', 'project\tLib.csproj\t\tLib\t1.0.0\t.', 'projectSrc\tLib.csproj\t/abs/L.cs\tnet8.0', - 'projectSrc\tLib.csproj\t/abs/obj/Lib.AssemblyInfo.cs\tnet8.0', + 'projectSrc\tLib.csproj\t/abs/A.cs\tnet8.0', 'projectSrc\tLib.csproj\t/abs/Legacy.cs\tnetstandard2.0', - 'projectInfo\tLib.csproj\tnetstandard2.0\tBuildError\tMSB3644: reference assemblies not found', 'projectTgt\tLib.csproj\t/abs/bin/net8.0/Lib.dll\tnet8.0', 'projectInfo\tLib.csproj\tnet8.0\tTargetFrameworkMoniker\t.NETCoreApp,Version=v8.0', 'projectInfo\tLib.csproj\tnet8.0\tDefineConstant\tNET8_0_OR_GREATER', 'projectInfo\tLib.csproj\tnet8.0\tFrameworkReference\tMicrosoft.NETCore.App', 'projectInfo\tLib.csproj\tnet8.0\tNullable\tenable', - 'projectInfo\tLib.csproj\tnet8.0\tReference\t/packs/ref/net8.0/System.Runtime.dll', - 'projectInfo\tLib.csproj\tnet8.0\tReference\t/pkgs/x/ref/net8.0/X.dll', - 'projectInfo\tLib.csproj\tnet8.0\tAnalyzer\t/packs/analyzers/Gen.dll', 'projectUsing\tLib.csproj\tnet8.0\tSystem.Linq\t\t0', 'projectUsing\tLib.csproj\tnet8.0\tSystem.Math\t\t1', 'root\ta\tLib.csproj\tnet8.0\t0\tnet8.0\t\tnet8.0', @@ -699,7 +695,6 @@ describe('dotnet records → per-framework sidecar', () => { langVersion: undefined, nullable: 'enable', assemblyName: undefined, - buildError: undefined, frameworkReferences: ['Microsoft.NETCore.App'], defineConstants: ['NET8_0_OR_GREATER'], usings: [ @@ -707,21 +702,14 @@ describe('dotnet records → per-framework sidecar', () => { { namespace: 'System.Math', static: true }, ], // Compiler order, not sorted. - sources: ['/abs/L.cs', '/abs/obj/Lib.AssemblyInfo.cs'], - references: [ - '/packs/ref/net8.0/System.Runtime.dll', - '/pkgs/x/ref/net8.0/X.dll', - ], - analyzers: ['/packs/analyzers/Gen.dll'], + sources: ['/abs/L.cs', '/abs/A.cs'], targets: ['/abs/bin/net8.0/Lib.dll'], dependencies: ['gone:1.0.0', 'x:1.0.0'], classpath: ['gone:1.0.0', 'x:1.0.0'], }, expect.objectContaining({ alias: 'netstandard2.0', - buildError: 'MSB3644: reference assemblies not found', sources: ['/abs/Legacy.cs'], - references: [], targets: [], classpath: ['x:1.0.0'], }), diff --git a/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs b/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs index c0cfbf2fa..e2d708a91 100644 --- a/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs +++ b/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs @@ -124,7 +124,6 @@ public void Execute() { } foreach (var loaded in postRestore.GetLoadedProjects(path).ToList()) postRestore.UnloadProject(loaded); } - EndDesignTimeBuilds(); ReportRestoreErrors(); } diff --git a/src/commands/manifest/scripts/dotnet-tool/Reader.cs b/src/commands/manifest/scripts/dotnet-tool/Reader.cs index d7236c73d..0af89e861 100644 --- a/src/commands/manifest/scripts/dotnet-tool/Reader.cs +++ b/src/commands/manifest/scripts/dotnet-tool/Reader.cs @@ -1,9 +1,7 @@ -using System.Collections.Concurrent; using System.Xml.Linq; using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; -using Microsoft.Build.Execution; using NuGet.Common; using NuGet.Frameworks; using NuGet.LibraryModel; @@ -533,9 +531,6 @@ private static string NormalizeSlashes(string path) { private static string? ShortFrameworkName(Project build) => ShortFrameworkName(build.GetPropertyValue("TargetFrameworkMoniker"), build.GetPropertyValue("TargetPlatformMoniker")); - private static string? ShortFrameworkName(ProjectInstance build) => - ShortFrameworkName(build.GetPropertyValue("TargetFrameworkMoniker"), build.GetPropertyValue("TargetPlatformMoniker")); - private static string? ShortFrameworkName(string moniker, string platformMoniker) { if (string.IsNullOrEmpty(moniker)) return null; try { @@ -563,93 +558,60 @@ private static List Frameworks(Project project) { } // Per inner build of a multi-targeting project, whose outer evaluation has - // no Compile items or TargetPath, or of the project's single framework. An - // excluded project is only evaluated, never built. + // no Compile items or TargetPath, or of the project's single framework. // `frameworks`: the inner builds to read, when restore names them. private void EmitPaths(ProjectInfo info, Project project, List? frameworks = null) { var multi = Frameworks(project); if (multi.Count == 0) { var single = project.GetPropertyValue("TargetFramework"); var config = string.IsNullOrEmpty(single) ? LegacyFrameworkName(project) : single; - if (_configs.Matches(new[] { config })) EmitBuild(info, project, null, config); + if (_configs.Matches(new[] { config })) EmitBuild(info, project, config); return; } - foreach (var tfm in (frameworks ?? multi).Where(f => _configs.Matches(new[] { f }))) EmitBuild(info, project, tfm, tfm); - } - - // Roslyn MSBuildWorkspace's design-time build, minus its command-line parsing: - // https://github.com/dotnet/roslyn/blob/d53e65ceaf037efb4f0c43c5a2ae4a3e19f5a59f/src/Workspaces/MSBuild/BuildHost/Build/ProjectBuildManager.cs#L28-L58 - private static readonly Dictionary DesignTimeProperties = new(StringComparer.OrdinalIgnoreCase) { - ["BuildingProject"] = "false", - ["BuildProjectReferences"] = "false", - ["DesignTimeBuild"] = "true", - ["ShouldUnsetParentConfigurationAndPlatform"] = "false", - ["SkipCompilerExecution"] = "true", - }; - - // WPF's, which generates the XAML code-behind sources. - private const string MarkupCompilationTarget = "DesignTimeMarkupCompilation"; - - // As the compiler task receives it; a failed design-time build fails the - // run, and leaves the evaluated values for --ignore-unresolved. - private void EmitBuild(ProjectInfo info, Project project, string? innerFramework, string config) { - var props = new Dictionary(project.GlobalProperties, StringComparer.OrdinalIgnoreCase); - if (innerFramework != null) props["TargetFramework"] = innerFramework; - ProjectInstance? compiled = null; - if (!info.Excluded) { - var error = DesignTimeBuild(project, props, out compiled); - if (error != null) { - _records.ProjectInfo(info.Key, config, "BuildError", error); - _records.Failure(info.Key, $"the design-time build failed: {error}", config, info.Key); - } - } - var instance = compiled; - if (instance == null) { + foreach (var tfm in (frameworks ?? multi).Where(f => _configs.Matches(new[] { f }))) { + Project build; try { - instance = EvaluatedWithImplicitDefines(innerFramework == null ? project : LoadInnerBuild(project, innerFramework)); + build = LoadInnerBuild(project, tfm); } catch (InvalidProjectFileException e) { - _records.Unscannable(config, $"could not evaluate the {config} build: {Text.FirstLine(e.Message)}", info.Key); - return; + _records.Unscannable(tfm, $"could not evaluate the {tfm} build: {Text.FirstLine(e.Message)}", info.Key); + continue; } + EmitBuild(info, build, tfm); } - var language = instance.GetPropertyValue("Language"); + } + + // As evaluation leaves it: no target runs, so neither the sources targets + // generate nor the references and analyzers they resolve are known. + private void EmitBuild(ProjectInfo info, Project build, string config) { + var language = build.GetPropertyValue("Language"); var isFSharp = string.Equals(language, "F#", StringComparison.OrdinalIgnoreCase); var isVb = string.Equals(language, "VB", StringComparison.OrdinalIgnoreCase); - var sources = isFSharp ? FSharpCompileOrder(instance) : instance.GetItems("Compile"); + var sources = isFSharp ? FSharpCompileOrder(build) : build.GetItems("Compile"); foreach (var source in FullPaths(sources)) _records.ProjectSrc(info.Key, source, config); - var targetPath = NormalizeSlashes(instance.GetPropertyValue("TargetPath")); + var targetPath = NormalizeSlashes(build.GetPropertyValue("TargetPath")); if (!string.IsNullOrEmpty(targetPath)) { - _records.ProjectTgt(info.Key, Path.GetFullPath(targetPath, project.DirectoryPath), config); + _records.ProjectTgt(info.Key, Path.GetFullPath(targetPath, build.DirectoryPath), config); } foreach (var property in new[] { "TargetFrameworkMoniker", "Language", "LangVersion", "AssemblyName", "Nullable" }) { - var value = instance.GetPropertyValue(property); + var value = build.GetPropertyValue(property); if (!string.IsNullOrEmpty(value)) _records.ProjectInfo(info.Key, config, property, value); } // Unrecognized, the alias stands in. - if (ShortFrameworkName(instance) is { } framework) { + if (ShortFrameworkName(build) is { } framework) { _records.ProjectInfo(info.Key, config, "Framework", framework); } - foreach (var reference in instance.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).Distinct()) { + foreach (var reference in build.GetItems("FrameworkReference").Select(i => i.EvaluatedInclude).Distinct()) { _records.ProjectInfo(info.Key, config, "FrameworkReference", reference); } - foreach (var item in instance.GetItems("Using")) { + foreach (var item in build.GetItems("Using")) { _records.ProjectUsing( info.Key, config, item.EvaluatedInclude, item.GetMetadataValue("Alias"), string.Equals(item.GetMetadataValue("Static"), "true", StringComparison.OrdinalIgnoreCase) ); } - // Csc, Vbc and Fsc all take ReferencePathWithRefAssemblies; Fsc takes no - // Analyzer; Vbc takes FinalDefineConstants, the others DefineConstants. - if (compiled != null) { - foreach (var path in FullPaths(compiled.GetItems("ReferencePathWithRefAssemblies"))) { - _records.ProjectInfo(info.Key, config, "Reference", path); - } - if (!isFSharp) { - foreach (var path in FullPaths(compiled.GetItems("Analyzer"))) _records.ProjectInfo(info.Key, config, "Analyzer", path); - } - } - var finalDefines = isVb ? instance.GetPropertyValue("FinalDefineConstants") : ""; - var defines = finalDefines.Length > 0 ? finalDefines : instance.GetPropertyValue("DefineConstants"); + // Vbc takes FinalDefineConstants, Csc and Fsc DefineConstants. + var finalDefines = isVb ? build.GetPropertyValue("FinalDefineConstants") : ""; + var defines = finalDefines.Length > 0 ? finalDefines : build.GetPropertyValue("DefineConstants"); foreach (var symbol in defines .Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(s => s.Trim()) @@ -659,106 +621,25 @@ private void EmitBuild(ProjectInfo info, Project project, string? innerFramework } } - // One build session for every build of the run, so a project that several - // others reference is evaluated and asked for its output once. - private BuildManager? _designTimeBuilds; - private readonly FirstErrorLogger _designTimeErrors = new(); - - private static IEnumerable FullPaths(IEnumerable items) => items + private static IEnumerable FullPaths(IEnumerable items) => items .Select(item => item.GetMetadataValue("FullPath")) .Where(full => !string.IsNullOrEmpty(full)) .Select(full => Path.GetFullPath(full)) .Distinct(StringComparer.Ordinal); - // As the F# SDK's FSharpSourceCodeCompileOrder target orders them for Fsc; - // idempotent once that target has run. - private static IEnumerable FSharpCompileOrder(ProjectInstance instance) { - IEnumerable Ordered(string order) => - instance.GetItems("Compile").Where(item => item.GetMetadataValue("CompileOrder") == order); + // As the F# SDK's FSharpSourceCodeCompileOrder target orders them for Fsc. + private static IEnumerable FSharpCompileOrder(Project build) { + IEnumerable Ordered(string order) => + build.GetItems("Compile").Where(item => item.GetMetadataValue("CompileOrder") == order); return Ordered("CompileFirst") - .Concat(instance.GetItems("CompileBefore")) + .Concat(build.GetItems("CompileBefore")) .Concat(Ordered("CompileBefore")) .Concat(Ordered("")) .Concat(Ordered("CompileAfter")) - .Concat(instance.GetItems("CompileAfter")) + .Concat(build.GetItems("CompileAfter")) .Concat(Ordered("CompileLast")); } - // Null on success, else the build's first error. Any exception is one - // build's failure, never the run's. - private string? DesignTimeBuild(Project project, Dictionary buildProperties, out ProjectInstance? instance) { - instance = null; - var props = new Dictionary(buildProperties, StringComparer.OrdinalIgnoreCase); - foreach (var (key, value) in DesignTimeProperties) props[key] = value; - try { - var result = BuildSession(project.ProjectCollection).BuildRequest(new BuildRequestData( - project.FullPath, - props, - null, - project.Targets.ContainsKey(MarkupCompilationTarget) ? new[] { "Compile", MarkupCompilationTarget } : new[] { "Compile" }, - null, - BuildRequestDataFlags.ProvideProjectStateAfterBuild - )); - if (result.OverallResult == BuildResultCode.Success && result.ProjectStateAfterBuild is { } state) { - instance = state; - return null; - } - return _designTimeErrors.FirstOf(result.SubmissionId) - ?? (result.Exception is { } failure ? Text.FirstLine(failure.Message) : "the Compile target failed"); - } catch (Exception e) { - return Text.FirstLine(e.Message); - } - } - - // The SDK's implicit framework symbols (NET8_0_OR_GREATER, ...) exist only - // once its AddImplicitDefineConstants target has run. - private ProjectInstance EvaluatedWithImplicitDefines(Project build) { - var instance = build.CreateProjectInstance(); - if (!instance.Targets.ContainsKey("AddImplicitDefineConstants")) return instance; - try { - BuildSession(build.ProjectCollection).BuildRequest(new BuildRequestData(instance, new[] { "AddImplicitDefineConstants" })); - } catch (Exception e) { - Log($"{build.FullPath}: implicit preprocessor symbols unavailable: {Text.FirstLine(e.Message)}"); - } - return instance; - } - - private BuildManager BuildSession(ProjectCollection collection) { - if (_designTimeBuilds == null) { - _designTimeBuilds = BuildManager.DefaultBuildManager; - _designTimeBuilds.BeginBuild(new BuildParameters(collection) { - Loggers = new Microsoft.Build.Framework.ILogger[] { _designTimeErrors }, - // Only errors are read; the engine then skips its other messages and - // drops the tasks' before any logger sees them. - OnlyLogCriticalEvents = true, - ProjectLoadSettings = RestoreLoadSettings, - }); - } - return _designTimeBuilds; - } - - private void EndDesignTimeBuilds() { - _designTimeBuilds?.EndBuild(); - _designTimeBuilds = null; - } - - private sealed class FirstErrorLogger : Microsoft.Build.Framework.ILogger { - private readonly ConcurrentDictionary _first = new(); - public Microsoft.Build.Framework.LoggerVerbosity Verbosity { get; set; } = Microsoft.Build.Framework.LoggerVerbosity.Quiet; - public string? Parameters { get; set; } - - public string? FirstOf(int submissionId) => _first.TryGetValue(submissionId, out var e) ? e : null; - - public void Initialize(Microsoft.Build.Framework.IEventSource eventSource) { - eventSource.ErrorRaised += (_, e) => _first.TryAdd( - e.BuildEventContext?.SubmissionId ?? -1, - string.IsNullOrEmpty(e.Code) ? e.Message : $"{e.Code}: {e.Message}" - ); - } - - public void Shutdown() { } - } - // The repository files declaring the project's dependencies; SDK- and // package-provided definitions are not the user's to change. private void EmitBuildFiles(ProjectInfo info, Project project) { diff --git a/src/commands/manifest/scripts/facts.mts b/src/commands/manifest/scripts/facts.mts index 97557df27..e3fb6831a 100644 --- a/src/commands/manifest/scripts/facts.mts +++ b/src/commands/manifest/scripts/facts.mts @@ -71,17 +71,14 @@ export type ProjectFramework = { langVersion?: string | undefined nullable?: string | undefined assemblyName?: string | undefined - // Set when the design-time build failed; the other fields then hold the - // evaluated values, without generated sources, references or analyzers. - buildError?: string | undefined frameworkReferences: string[] + // As evaluated: the SDK's implicit framework symbols (NET8_0_OR_GREATER, + // ...) are absent. defineConstants: string[] usings: Array<{ namespace: string; alias?: string; static?: true }> - // What the compiler receives: sources in order, generated ones included, - // and every reference and analyzer assembly. + // In compiler order, as evaluated: sources that build targets generate + // are absent. sources: string[] - references: string[] - analyzers: string[] targets: string[] dependencies: string[] classpath: string[] diff --git a/src/commands/manifest/scripts/records.mts b/src/commands/manifest/scripts/records.mts index 897f482eb..48cb3b6a7 100644 --- a/src/commands/manifest/scripts/records.mts +++ b/src/commands/manifest/scripts/records.mts @@ -74,16 +74,13 @@ export type RawUsing = { namespace: string; alias: string; static: boolean } // One dotnet target framework build of a project, by its alias. export type RawBuild = { - analyzers: string[] assemblyName?: string | undefined - buildError?: string | undefined defineConstants: string[] framework?: string | undefined frameworkReferences: string[] langVersion?: string | undefined language?: string | undefined nullable?: string | undefined - references: string[] sources: string[] targetFrameworkMoniker?: string | undefined targets: string[] @@ -101,16 +98,13 @@ type RawBuildValue = { // projectInfo names, as the dotnet tool's Reader.cs writes them. const PROJECT_INFO_LISTS = { __proto__: null, - Analyzer: 'analyzers', DefineConstant: 'defineConstants', FrameworkReference: 'frameworkReferences', - Reference: 'references', } as unknown as Record const PROJECT_INFO_VALUES = { __proto__: null, AssemblyName: 'assemblyName', - BuildError: 'buildError', Framework: 'framework', LangVersion: 'langVersion', Language: 'language', @@ -228,10 +222,8 @@ export function parseRecords(text: string): ParsedRecords { let b = builds.get(config) if (!b) { b = { - analyzers: [], defineConstants: [], frameworkReferences: [], - references: [], sources: [], targets: [], usings: [], diff --git a/src/commands/manifest/scripts/resolution-report-nuget.mts b/src/commands/manifest/scripts/resolution-report-nuget.mts index 1df27f6e3..fb18ee97a 100644 --- a/src/commands/manifest/scripts/resolution-report-nuget.mts +++ b/src/commands/manifest/scripts/resolution-report-nuget.mts @@ -11,8 +11,8 @@ const NOT_FOUND_CODES = new Set(['NU1100', 'NU1101', 'NU1102', 'NU1103']) const SOURCE_CODES = new Set(['NU1301', 'NU1302']) // The socket-facts-dotnet tool reports restore errors as `: `, -// and a design-time build failure as `: : `; its -// findings without a code count as other failures. +// and some findings as `: : `; its findings without a +// code count as other failures. export function classifyNugetFailure(detail: string): FailureCategory { const code = /(?:^|: )(NU\d{4}|NETSDK\d{4}|MSB\d{4}):/.exec(detail || '')?.[1] if (!code) { diff --git a/src/commands/manifest/scripts/resolution-report-nuget.test.mts b/src/commands/manifest/scripts/resolution-report-nuget.test.mts index 3cbc6b566..ad3d2fa54 100644 --- a/src/commands/manifest/scripts/resolution-report-nuget.test.mts +++ b/src/commands/manifest/scripts/resolution-report-nuget.test.mts @@ -10,7 +10,8 @@ const DETAILS_BY_CATEGORY = { 'NU1109: Detected package downgrade: Polly from 8.4.2 to centrally defined 7.2.4.', 'NU1201: Project Fs is not compatible with netstandard2.0.', 'NETSDK1147: To build this project, the following workloads must be installed: maui', - 'the design-time build failed: MSB3644: The reference assemblies for .NETFramework,Version=v4.8 were not found.', + "MSB4236: The SDK 'Microsoft.Build.Traversal/2.0.24' specified could not be found.", + 'could not read the solution: MSB5009: Error parsing the nested project section in solution file.', ], 'not-found': [ 'NU1101: Unable to find package Foo. No packages exist with this id in source(s): nuget.org', diff --git a/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py b/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py index 92c585fe1..a196e307f 100644 --- a/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py +++ b/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py @@ -140,18 +140,17 @@ def records(name): return rows -# Some SDKs' MSBuild finds no .NET Framework reference assemblies off Windows, -# which fails the legacy project's design-time build there, as its compile would. -def package_failures(rows): - return [r for r in rows if r[0] == 'failure' and not r[2].startswith('the design-time build failed: MSB3644')] +def failures(rows): + return [r for r in rows if r[0] == 'failure'] -def info_rows(rows, project, name, config=None): - return [r for r in rows if r[0] == 'projectInfo' and r[1] == project and r[3] == name and config in (None, r[2])] +def info_rows(rows, project, name): + return [r for r in rows if r[0] == 'projectInfo' and r[1] == project and r[3] == name] def records_phase(): basic = records('basic.tsv') + check(not failures(basic), f'basic failures {failures(basic)}') files = {r[2]: r[3] for r in basic if r[0] == 'file'} check(any(k.startswith('demo.ext.tool:') for k in files), 'Tool assembly recorded') check(not any(k.startswith('demo.ext.analyzer:') for k in files), 'analyzer has no runtime assembly') @@ -161,21 +160,11 @@ def records_phase(): check(all(r[7] == r[3] for r in lib_roots), f'root aliases {[(r[3], r[7]) for r in lib_roots]}') info = {(r[2], r[3], r[4]) for r in basic if r[0] == 'projectInfo' and r[1] == 'src/Lib/Lib.csproj'} check(('net6.0', 'TargetFrameworkMoniker', '.NETCoreApp,Version=v6.0') in info, 'net6.0 moniker recorded') - check(('net6.0', 'DefineConstant', 'NET6_0_OR_GREATER') in info, 'implicit framework symbols recorded') + check(('net6.0', 'DefineConstant', 'DEBUG') in info, 'evaluated symbols recorded') check(('net6.0', 'FrameworkReference', 'Microsoft.NETCore.App') in info, 'framework reference recorded') check(any(r[0] == 'compileFile' and r[2].startswith('demo.ext.tool:') for r in basic), 'compile assemblies recorded') sources = [r[2] for r in basic if r[0] == 'projectSrc'] check(any(s.endswith('/shared/Linked.cs') for s in sources), 'linked source recorded') - check(any(s.endswith('.AssemblyInfo.cs') for s in sources), 'generated source recorded') - errors = [r for r in basic if r[0] == 'projectInfo' and r[3] == 'BuildError'] - check(not errors, f'design-time build errors {errors}') - refs = {r[4] for r in info_rows(basic, 'src/Lib/Lib.csproj', 'Reference', 'net6.0')} - check(any(p.endswith('/System.Runtime.dll') for p in refs), 'framework reference assemblies recorded') - # The stub packages' empty assemblies are not references the compiler - # would take; a project reference's output is, built or not. - check(any(p.endswith('/Extra.dll') for p in refs), 'project reference recorded') - analyzers = {r[4] for r in info_rows(basic, 'src/Lib/Lib.csproj', 'Analyzer', 'net6.0')} - check(any('demo.ext.analyzer' in p.lower() for p in analyzers), f'analyzers {analyzers}') netstandard = records('basic-netstandard.tsv') built = {(r[1], r[3]) for r in netstandard if r[0] == 'projectSrc'} @@ -189,24 +178,26 @@ def records_phase(): unknown = {r[3] for r in stale if r[0] == 'unscannable' and 'did not restore this project' in r[2]} check({'src/App/App.csproj', 'src/Extra/Extra.csproj', 'src/Lib/Lib.csproj'} <= unknown, f'unrestored projects reported {sorted(unknown)}') - designtime = records('designtime.tsv') - failed = [r for r in designtime if r[0] == 'failure' and 'the design-time build failed' in r[2]] - check(len(failed) == 1 and 'generated sources unavailable' in failed[0][2], f'design-time failure {failed}') + evaluation = records('evaluation.tsv') + problems = [r for r in evaluation if r[0] in ('failure', 'unscannable')] + check(not problems, f'evaluation problems {problems}') + lib = [r[2] for r in evaluation if r[0] == 'projectSrc' and r[1] == 'Lib/Lib.csproj'] + check(len(lib) == 1 and lib[0].endswith('/Lib/Class.cs'), f'sources of a project failing any target run {lib}') + check(any(r[0] == 'project' and r[1] == 'Build.csproj' for r in evaluation), 'compiler-less aggregator recorded') langs = records('langs.tsv') vb = {r[4] for r in info_rows(langs, 'Vb.vbproj', 'DefineConstant')} check({'DEBUG=-1', 'TRACE=-1'} <= vb, f'VB compiler constants {vb}') fs = [r[2] for r in langs if r[0] == 'projectSrc' and r[1] == '../Fs/Fs.fsproj'] - check(len(fs) >= 2 and fs[-1].endswith('/Library.fs'), f'F# sources in compile order {fs}') - check(not any(r[0] == 'failure' for r in basic), f'basic failures {[r for r in basic if r[0] == "failure"]}') + check([os.path.basename(f) for f in fs] == ['First.fs', 'Library.fs'], f'F# sources in compile order {fs}') missing = records('pc-missing.tsv') unfetched = {r[2] for r in missing if r[0] == 'unfetched'} check(unfetched == {'demo.ext.tool:1.0.0', 'demo.ext.shared:1.0.0'}, f'unfetched {unfetched}') - check(len(package_failures(missing)) == 2, 'missing packages.config assemblies fail') + check(len(failures(missing)) == 2, 'missing packages.config assemblies fail') present = records('pc-present.tsv') - check(not package_failures(present) and not any(r[0] == 'unfetched' for r in present), 'installed packages.config resolves') + check(not failures(present) and not any(r[0] == 'unfetched' for r in present), 'installed packages.config resolves') legacy_roots = {(r[3], r[5], r[7]) for r in present if r[0] == 'root' and r[2] == 'Legacy/Legacy.csproj'} check(legacy_roots == {('net48', 'net48', 'net48')}, f'Legacy roots (config, framework, alias) {legacy_roots}') present_files = {r[2] for r in present if r[0] == 'file'} diff --git a/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Build.csproj b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Build.csproj new file mode 100644 index 000000000..a504a68d3 --- /dev/null +++ b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Build.csproj @@ -0,0 +1,9 @@ + + + + PackageReference + + + + + diff --git a/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Class.cs b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Class.cs new file mode 100644 index 000000000..a7220561e --- /dev/null +++ b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Class.cs @@ -0,0 +1,3 @@ +namespace Lib; + +public static class Class { } diff --git a/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Lib.csproj b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Lib.csproj new file mode 100644 index 000000000..03cdb68c1 --- /dev/null +++ b/src/commands/manifest/scripts/test/dotnet-compat/evaluation/Lib/Lib.csproj @@ -0,0 +1,8 @@ + + net6.0 + + + + + + diff --git a/src/commands/manifest/scripts/test/dotnet-compat/failures/designtime/DesignTime.csproj b/src/commands/manifest/scripts/test/dotnet-compat/failures/designtime/DesignTime.csproj deleted file mode 100644 index 4e958ccb8..000000000 --- a/src/commands/manifest/scripts/test/dotnet-compat/failures/designtime/DesignTime.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - net6.0 - - - - - - diff --git a/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/First.fs b/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/First.fs new file mode 100644 index 000000000..597f374f2 --- /dev/null +++ b/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/First.fs @@ -0,0 +1,3 @@ +module First + +let value = 1 diff --git a/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Fs.fsproj b/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Fs.fsproj index b38c3d8bb..15ac4e7ba 100644 --- a/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Fs.fsproj +++ b/src/commands/manifest/scripts/test/dotnet-compat/langs/Fs/Fs.fsproj @@ -1,5 +1,8 @@ net6.0 - + + + + diff --git a/src/commands/manifest/scripts/test/dotnet-compat/smoke-test.sh b/src/commands/manifest/scripts/test/dotnet-compat/smoke-test.sh index 0448412d1..fb6cd0934 100755 --- a/src/commands/manifest/scripts/test/dotnet-compat/smoke-test.sh +++ b/src/commands/manifest/scripts/test/dotnet-compat/smoke-test.sh @@ -102,7 +102,7 @@ records() { records "$WORK/basic/Basic.sln" "$WORK/basic-netstandard.tsv" --include-configs netstandard2.0 records "$WORK/basic/Basic.sln" "$WORK/basic.tsv" records "$WORK/langs/Vb/Vb.vbproj" "$WORK/langs.tsv" -records "$WORK/failures/designtime/DesignTime.csproj" "$WORK/designtime.tsv" +records "$WORK/evaluation/Build.csproj" "$WORK/evaluation.tsv" records "$WORK/packages-config/Mixed.sln" "$WORK/pc-missing.tsv" # What `nuget restore` lays out for packages.config: .. python3 - "$WORK" <<'PY' From 452e8e80211b3fb8f647cf7f85a5fc892bfc90ce Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Tue, 3 Nov 2026 16:07:43 +0100 Subject: [PATCH 2/2] fix(manifest): keep MSBuild error codes in .NET evaluation failures MSBuild keeps an evaluation error's code out of the exception message, so evaluation and solution-parse failures reached the resolution report without it and were grouped as other failures. Prefix the code so they group as project configuration problems. Inner builds now come from TargetFrameworks, split as the cross-targeting targets do, rather than from restore's list, which only mattered while the tool built projects. Also tidies helpers and tests the evaluation-only change left with one caller. --- .../manifest/scripts/assemble.test.mts | 4 +- .../scripts/dotnet-tool/FactsRunner.cs | 18 ++++---- .../manifest/scripts/dotnet-tool/Patterns.cs | 2 + .../manifest/scripts/dotnet-tool/Reader.cs | 42 ++++++++----------- .../scripts/resolution-report-nuget.mts | 4 +- .../scripts/resolution-report-nuget.test.mts | 2 +- .../test/dotnet-compat/assert-facts.py | 13 +++--- 7 files changed, 38 insertions(+), 47 deletions(-) diff --git a/src/commands/manifest/scripts/assemble.test.mts b/src/commands/manifest/scripts/assemble.test.mts index a43125640..8ae6e7fab 100644 --- a/src/commands/manifest/scripts/assemble.test.mts +++ b/src/commands/manifest/scripts/assemble.test.mts @@ -658,7 +658,7 @@ describe('dotnet records → per-framework sidecar', () => { 'projectSrc\tLib.csproj\t/abs/Legacy.cs\tnetstandard2.0', 'projectTgt\tLib.csproj\t/abs/bin/net8.0/Lib.dll\tnet8.0', 'projectInfo\tLib.csproj\tnet8.0\tTargetFrameworkMoniker\t.NETCoreApp,Version=v8.0', - 'projectInfo\tLib.csproj\tnet8.0\tDefineConstant\tNET8_0_OR_GREATER', + 'projectInfo\tLib.csproj\tnet8.0\tDefineConstant\tDEBUG', 'projectInfo\tLib.csproj\tnet8.0\tFrameworkReference\tMicrosoft.NETCore.App', 'projectInfo\tLib.csproj\tnet8.0\tNullable\tenable', 'projectUsing\tLib.csproj\tnet8.0\tSystem.Linq\t\t0', @@ -696,7 +696,7 @@ describe('dotnet records → per-framework sidecar', () => { nullable: 'enable', assemblyName: undefined, frameworkReferences: ['Microsoft.NETCore.App'], - defineConstants: ['NET8_0_OR_GREATER'], + defineConstants: ['DEBUG'], usings: [ { namespace: 'System.Linq' }, { namespace: 'System.Math', static: true }, diff --git a/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs b/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs index e2d708a91..2994cd4ef 100644 --- a/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs +++ b/src/commands/manifest/scripts/dotnet-tool/FactsRunner.cs @@ -155,7 +155,7 @@ private bool IsExcludedProject(string path) { _solution = new SolutionContext(entry, _opts.GlobalProperties); return _solution.Members.Keys.OrderBy(p => p, StringComparer.Ordinal).ToList(); } catch (Exception e) when (e is InvalidProjectFileException or IOException or JsonException) { - _records.Failure(Rel(entry), $"could not read the solution: {Text.FirstLine(e.Message)}", ""); + _records.Failure(Rel(entry), $"could not read the solution: {ErrorText(e)}", ""); return null; } } @@ -211,13 +211,7 @@ private IEnumerable ProjectReferences(Project project, bool innerBuilds) } } } - return builds - .SelectMany(b => b.GetItems("ProjectReference")) - .Select(item => item.GetMetadataValue("FullPath")) - .Where(full => !string.IsNullOrEmpty(full) && File.Exists(full)) - .Select(full => Path.GetFullPath(full)) - .Distinct(StringComparer.Ordinal) - .ToList(); + return FullPaths(builds.SelectMany(b => b.GetItems("ProjectReference"))).Where(File.Exists).ToList(); } private void RunRestore() { @@ -281,12 +275,18 @@ private Project LoadInnerBuild(Project project, string targetFramework) { return Load(collection, info.FullPath, info.GlobalProperties); } catch (InvalidProjectFileException e) { if (!info.Excluded) { - _records.Unscannable("", $"could not evaluate the project: {Text.FirstLine(e.Message)}", info.Key); + _records.Unscannable("", $"could not evaluate the project: {ErrorText(e)}", info.Key); } return null; } } + // MSBuild keeps an error's code out of its message. + private static string ErrorText(Exception e) { + var line = Text.FirstLine(e.Message); + return e is InvalidProjectFileException { ErrorCode: { Length: > 0 } code } ? $"{code}: {line}" : line; + } + // Restore errors not already reported from an assets file, then the projects // left without current assets and without errors of their own. private void ReportRestoreErrors() { diff --git a/src/commands/manifest/scripts/dotnet-tool/Patterns.cs b/src/commands/manifest/scripts/dotnet-tool/Patterns.cs index 78b26c7f1..294b5e41a 100644 --- a/src/commands/manifest/scripts/dotnet-tool/Patterns.cs +++ b/src/commands/manifest/scripts/dotnet-tool/Patterns.cs @@ -22,6 +22,8 @@ public bool Matches(IReadOnlyCollection names) { return _includes.Count == 0 || _includes.Any(p => names.Any(p.IsMatch)); } + public bool Matches(string name) => Matches(new[] { name }); + private static List Parse(string csv) { return csv .Split(',') diff --git a/src/commands/manifest/scripts/dotnet-tool/Reader.cs b/src/commands/manifest/scripts/dotnet-tool/Reader.cs index 0af89e861..ce38350b9 100644 --- a/src/commands/manifest/scripts/dotnet-tool/Reader.cs +++ b/src/commands/manifest/scripts/dotnet-tool/Reader.cs @@ -162,12 +162,10 @@ string[] Unscannable(string? detail) { var assets = new Assets(PackageResolver(lockFile), libraries, projectKeys, projectNames); var isTest = IsTestProject(project); - var frameworks = new List(); foreach (var target in NuGetShim.Seq(lockFile, "Targets")) { - var alias = EmitTarget(info, spec, target, assets, isTest); - if (alias != null && string.IsNullOrEmpty(target.RuntimeIdentifier)) frameworks.Add(alias); + EmitTarget(info, spec, target, assets, isTest); } - if (_opts.WithFiles) EmitPaths(info, project, frameworks); + if (_opts.WithFiles) EmitPaths(info, project); return references; } @@ -227,9 +225,8 @@ private static bool IsTestProject(Project project) { || project.GetPropertyValue("ProjectTypeGuids").Contains(TestProjectTypeGuid, StringComparison.OrdinalIgnoreCase); } - // One resolution root per (target framework [/ runtime], prod|dev). Returns - // the target framework alias when the target was read. - private string? EmitTarget(ProjectInfo info, PackageSpec spec, LockFileTarget target, Assets assets, bool isTest) { + // One resolution root per (target framework [/ runtime], prod|dev). + private void EmitTarget(ProjectInfo info, PackageSpec spec, LockFileTarget target, Assets assets, bool isTest) { var framework = NuGetShim.Seq(spec, "TargetFrameworks") .FirstOrDefault(f => f.FrameworkName.Equals(target.TargetFramework)); var alias = framework?.TargetAlias; @@ -239,7 +236,7 @@ private static bool IsTestProject(Project project) { // A runtime-specific target also answers to its framework's name, so a // filter on `net8.0` covers `net8.0/win-x64`. var names = string.IsNullOrEmpty(rid) ? new[] { config } : new[] { config, alias }; - if (!_configs.Matches(names)) return null; + if (!_configs.Matches(names)) return; if (_scanned.Add(config)) _records.Scanned(config); var libraries = new Dictionary(StringComparer.Ordinal); @@ -331,7 +328,6 @@ void EmitRoot(string kind, bool prod, HashSet ids, bool withFiles) { // project resolves is dev. EmitRoot("all", false, reached, _opts.WithFiles); if (!isTest) EmitRoot("prod", true, shipped, false); - return alias; } // Runtime assemblies (lib/, runtimes//lib/) carry the method bodies an @@ -454,7 +450,7 @@ private void ReadPackagesConfig(ProjectInfo info, Project project, string packag } if (packages.Count == 0) return; var config = LegacyFrameworkName(project); - if (!_configs.Matches(new[] { config })) return; + if (!_configs.Matches(config)) return; if (_scanned.Add(config)) _records.Scanned(config); var isTest = IsTestProject(project); @@ -528,13 +524,11 @@ private static string NormalizeSlashes(string path) { // NuGet's short folder name of a build's framework, as restore derives it // from the two monikers; null when unrecognized. - private static string? ShortFrameworkName(Project build) => - ShortFrameworkName(build.GetPropertyValue("TargetFrameworkMoniker"), build.GetPropertyValue("TargetPlatformMoniker")); - - private static string? ShortFrameworkName(string moniker, string platformMoniker) { + private static string? ShortFrameworkName(Project build) { + var moniker = build.GetPropertyValue("TargetFrameworkMoniker"); if (string.IsNullOrEmpty(moniker)) return null; try { - return NuGetFramework.ParseComponents(moniker, platformMoniker).GetShortFolderName(); + return NuGetFramework.ParseComponents(moniker, build.GetPropertyValue("TargetPlatformMoniker")).GetShortFolderName(); } catch (ArgumentException) { return null; } @@ -547,41 +541,39 @@ private static string LegacyFrameworkName(Project project) { return string.IsNullOrEmpty(version) ? "unknown" : $"net{version.TrimStart('v').Replace(".", "")}"; } - // The inner builds of a multi-targeting project, by target framework; none - // for a project with a single framework. + // Split as Microsoft.Common.CrossTargeting.targets does: trimmed, distinct + // ignoring case. Empty for a single-framework project. private static List Frameworks(Project project) { var multi = project.GetPropertyValue("TargetFrameworks"); if (string.IsNullOrEmpty(project.GetPropertyValue("TargetFramework")) && !string.IsNullOrEmpty(multi)) { - return multi.Split(';').Select(f => f.Trim()).Where(f => f.Length > 0).Distinct().ToList(); + return multi.Split(';').Select(f => f.Trim()).Where(f => f.Length > 0).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } return new List(); } // Per inner build of a multi-targeting project, whose outer evaluation has // no Compile items or TargetPath, or of the project's single framework. - // `frameworks`: the inner builds to read, when restore names them. - private void EmitPaths(ProjectInfo info, Project project, List? frameworks = null) { + private void EmitPaths(ProjectInfo info, Project project) { var multi = Frameworks(project); if (multi.Count == 0) { var single = project.GetPropertyValue("TargetFramework"); var config = string.IsNullOrEmpty(single) ? LegacyFrameworkName(project) : single; - if (_configs.Matches(new[] { config })) EmitBuild(info, project, config); + if (_configs.Matches(config)) EmitBuild(info, project, config); return; } - foreach (var tfm in (frameworks ?? multi).Where(f => _configs.Matches(new[] { f }))) { + foreach (var tfm in multi.Where(_configs.Matches)) { Project build; try { build = LoadInnerBuild(project, tfm); } catch (InvalidProjectFileException e) { - _records.Unscannable(tfm, $"could not evaluate the {tfm} build: {Text.FirstLine(e.Message)}", info.Key); + _records.Unscannable(tfm, $"could not evaluate the {tfm} build: {ErrorText(e)}", info.Key); continue; } EmitBuild(info, build, tfm); } } - // As evaluation leaves it: no target runs, so neither the sources targets - // generate nor the references and analyzers they resolve are known. + // As evaluation leaves it: sources that targets generate are absent. private void EmitBuild(ProjectInfo info, Project build, string config) { var language = build.GetPropertyValue("Language"); var isFSharp = string.Equals(language, "F#", StringComparison.OrdinalIgnoreCase); diff --git a/src/commands/manifest/scripts/resolution-report-nuget.mts b/src/commands/manifest/scripts/resolution-report-nuget.mts index fb18ee97a..fb924414e 100644 --- a/src/commands/manifest/scripts/resolution-report-nuget.mts +++ b/src/commands/manifest/scripts/resolution-report-nuget.mts @@ -11,8 +11,8 @@ const NOT_FOUND_CODES = new Set(['NU1100', 'NU1101', 'NU1102', 'NU1103']) const SOURCE_CODES = new Set(['NU1301', 'NU1302']) // The socket-facts-dotnet tool reports restore errors as `: `, -// and some findings as `: : `; its findings without a -// code count as other failures. +// and evaluation errors as `: : `; its findings +// without a code count as other failures. export function classifyNugetFailure(detail: string): FailureCategory { const code = /(?:^|: )(NU\d{4}|NETSDK\d{4}|MSB\d{4}):/.exec(detail || '')?.[1] if (!code) { diff --git a/src/commands/manifest/scripts/resolution-report-nuget.test.mts b/src/commands/manifest/scripts/resolution-report-nuget.test.mts index ad3d2fa54..52dc89251 100644 --- a/src/commands/manifest/scripts/resolution-report-nuget.test.mts +++ b/src/commands/manifest/scripts/resolution-report-nuget.test.mts @@ -12,13 +12,13 @@ const DETAILS_BY_CATEGORY = { 'NETSDK1147: To build this project, the following workloads must be installed: maui', "MSB4236: The SDK 'Microsoft.Build.Traversal/2.0.24' specified could not be found.", 'could not read the solution: MSB5009: Error parsing the nested project section in solution file.', + 'could not evaluate the project: MSB4019: The imported project "/repo/x.props" was not found. /repo/App.csproj', ], 'not-found': [ 'NU1101: Unable to find package Foo. No packages exist with this id in source(s): nuget.org', 'NU1102: Unable to find package Foo with version (>= 9.0.0)', ], other: [ - 'could not evaluate the project: The imported project "x.props" was not found.', 'obj/project.assets.json belongs to B.csproj: the projects share an intermediate output directory, so restore output cannot be attributed', 'runtime assemblies restore listed are missing from the package folders (lib/net8.0/Foo.dll); re-run `dotnet restore`', ], diff --git a/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py b/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py index a196e307f..f0da72f41 100644 --- a/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py +++ b/src/commands/manifest/scripts/test/dotnet-compat/assert-facts.py @@ -7,6 +7,7 @@ """ import json import os +import re import sys WORK, SDK, CPM_ENTRY = sys.argv[1], sys.argv[2], sys.argv[3] @@ -129,7 +130,7 @@ def default_phase(): check('Demo.Ext.DoesNotExist' in log and 'NU1101' in log, 'missing package reported') with open(os.path.join(WORK, 'broken.log')) as f: log = f.read() - check('could not evaluate the project' in log, 'evaluation failure reported') + check(re.search(r'could not evaluate the project: MSB\d{4}: ', log), 'evaluation failure reported with its code') def records(name): @@ -144,10 +145,6 @@ def failures(rows): return [r for r in rows if r[0] == 'failure'] -def info_rows(rows, project, name): - return [r for r in rows if r[0] == 'projectInfo' and r[1] == project and r[3] == name] - - def records_phase(): basic = records('basic.tsv') check(not failures(basic), f'basic failures {failures(basic)}') @@ -179,14 +176,14 @@ def records_phase(): check({'src/App/App.csproj', 'src/Extra/Extra.csproj', 'src/Lib/Lib.csproj'} <= unknown, f'unrestored projects reported {sorted(unknown)}') evaluation = records('evaluation.tsv') - problems = [r for r in evaluation if r[0] in ('failure', 'unscannable')] - check(not problems, f'evaluation problems {problems}') + faults = [r for r in evaluation if r[0] in ('failure', 'unscannable')] + check(not faults, f'evaluation faults {faults}') lib = [r[2] for r in evaluation if r[0] == 'projectSrc' and r[1] == 'Lib/Lib.csproj'] check(len(lib) == 1 and lib[0].endswith('/Lib/Class.cs'), f'sources of a project failing any target run {lib}') check(any(r[0] == 'project' and r[1] == 'Build.csproj' for r in evaluation), 'compiler-less aggregator recorded') langs = records('langs.tsv') - vb = {r[4] for r in info_rows(langs, 'Vb.vbproj', 'DefineConstant')} + vb = {r[4] for r in langs if r[0] == 'projectInfo' and r[1] == 'Vb.vbproj' and r[3] == 'DefineConstant'} check({'DEBUG=-1', 'TRACE=-1'} <= vb, f'VB compiler constants {vb}') fs = [r[2] for r in langs if r[0] == 'projectSrc' and r[1] == '../Fs/Fs.fsproj'] check([os.path.basename(f) for f in fs] == ['First.fs', 'Library.fs'], f'F# sources in compile order {fs}')