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..8ae6e7fab 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\tDefineConstant\tDEBUG', '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,29 +695,21 @@ describe('dotnet records → per-framework sidecar', () => { langVersion: undefined, nullable: 'enable', assemblyName: undefined, - buildError: undefined, frameworkReferences: ['Microsoft.NETCore.App'], - defineConstants: ['NET8_0_OR_GREATER'], + defineConstants: ['DEBUG'], usings: [ { namespace: 'System.Linq' }, { 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..2994cd4ef 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(); } @@ -156,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; } } @@ -212,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() { @@ -282,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 d7236c73d..ce38350b9 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; @@ -164,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; } @@ -229,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; @@ -241,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); @@ -333,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 @@ -456,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); @@ -530,16 +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(ProjectInstance 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; } @@ -552,104 +541,69 @@ 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. An - // excluded project is only evaluated, never built. - // `frameworks`: the inner builds to read, when restore names them. - private void EmitPaths(ProjectInfo info, Project project, List? frameworks = null) { + // no Compile items or TargetPath, or of the project's single framework. + 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, null, config); + if (_configs.Matches(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 multi.Where(_configs.Matches)) { + 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: {ErrorText(e)}", info.Key); + continue; } + EmitBuild(info, build, tfm); } - var language = instance.GetPropertyValue("Language"); + } + + // 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); 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 +613,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..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 a design-time build failure 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 3cbc6b566..52dc89251 100644 --- a/src/commands/manifest/scripts/resolution-report-nuget.test.mts +++ b/src/commands/manifest/scripts/resolution-report-nuget.test.mts @@ -10,14 +10,15 @@ 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.', + '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 92c585fe1..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): @@ -140,18 +141,13 @@ 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 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 failures(rows): + return [r for r in rows if r[0] == 'failure'] 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 +157,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 +175,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') + 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(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'