diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs index 21c9dee..bfda9cd 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogFixture.cs @@ -2,6 +2,7 @@ using Microsoft.Build.Framework; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -15,29 +16,36 @@ namespace Basic.CompilerLog.UnitTests; public sealed class CompilerLogFixture : IDisposable { + private readonly ImmutableArray> _allCompLogs; + + /// + /// Storage directory for all the generated artifacts and scatch directories + /// internal string StorageDirectory { get; } - + /// /// Directory that holds the log files /// internal string ComplogDirectory { get; } - internal string ConsoleComplogPath { get; } + internal Lazy ConsoleComplogPath { get; } + + internal Lazy ConsoleNoGeneratorComplogPath { get; } + + internal Lazy ConsoleWithLineComplogPath { get; } - internal string ConsoleNoGeneratorComplogPath { get; } + internal Lazy ConsoleWithLineAndEmbedComplogPath { get; } - internal string ClassLibComplogPath { get; } + internal Lazy ClassLibComplogPath { get; } - internal string ClassLibSignedComplogPath { get; } + internal Lazy ClassLibSignedComplogPath { get; } /// /// A multi-targeted class library /// - internal string ClassLibMultiComplogPath { get; } + internal Lazy ClassLibMultiComplogPath { get; } - internal string? WpfAppComplogPath { get; } - - internal IEnumerable AllComplogs { get; } + internal Lazy? WpfAppComplogPath { get; } /// /// Constructor for the primary fixture. To get actual diagnostic messages into the output @@ -60,8 +68,8 @@ void RunDotnetCommand(string args, string workingDirectory) Assert.True(result.Succeeded); } - var allCompLogs = new List(); - ConsoleComplogPath = WithBuild("console.complog", string (string scratchPath) => + var builder = ImmutableArray.CreateBuilder>(); + ConsoleComplogPath = WithBuild("console.complog", void (string scratchPath) => { RunDotnetCommand($"new console --name console --output .", scratchPath); var projectFileContent = """ @@ -89,29 +97,49 @@ partial class Util { """; File.WriteAllText(Path.Combine(scratchPath, "Program.cs"), program, TestBase.DefaultEncoding); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); - ConsoleNoGeneratorComplogPath = WithBuild("console-no-generator.complog", string (string scratchPath) => + ConsoleNoGeneratorComplogPath = WithBuild("console-no-generator.complog", void (string scratchPath) => { RunDotnetCommand($"new console --name example-no-generator --output .", scratchPath); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibComplogPath = WithBuild("classlib.complog", string (string scratchPath) => + ConsoleWithLineComplogPath = WithBuild("console-with-line.complog", void (string scratchPath) => { - RunDotnetCommand($"new classlib --name classlib --output .", scratchPath); - var projectFileContent = """ - - - net7.0 - enable - enable - - + RunDotnetCommand($"new console --name console --output .", scratchPath); + var extra = """ + using System; + using System.Text.RegularExpressions; + + // File that does not exsit + #line 42 "blah.txt" + class C { } + """; + File.WriteAllText(Path.Combine(scratchPath, "Extra.cs"), extra, TestBase.DefaultEncoding); + RunDotnetCommand("build -bl", scratchPath); + }); + + ConsoleWithLineAndEmbedComplogPath = WithBuild("console-with-line-and-embed.complog", void (string scratchPath) => + { + RunDotnetCommand($"new console --name console --output .", scratchPath); + DotnetUtil.AddProjectProperty("true", scratchPath); + var extra = """ + using System; + using System.Text.RegularExpressions; + + // File that does not exsit + #line 42 "line.txt" + class C { } """; - File.WriteAllText(Path.Combine(scratchPath, "classlib.csproj"), projectFileContent, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "Extra.cs"), extra, TestBase.DefaultEncoding); + File.WriteAllText(Path.Combine(scratchPath, "line.txt"), "this is content", TestBase.DefaultEncoding); + RunDotnetCommand("build -bl", scratchPath); + }); + + ClassLibComplogPath = WithBuild("classlib.complog", void (string scratchPath) => + { + RunDotnetCommand($"new classlib --name classlib --output . --framework net7.0", scratchPath); var program = """ using System; using System.Text.RegularExpressions; @@ -123,24 +151,13 @@ partial class Util { """; File.WriteAllText(Path.Combine(scratchPath, "Class1.cs"), program, TestBase.DefaultEncoding); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibSignedComplogPath = WithBuild("classlibsigned.complog", string (string scratchPath) => + ClassLibSignedComplogPath = WithBuild("classlibsigned.complog", void (string scratchPath) => { RunDotnetCommand($"new classlib --name classlibsigned --output .", scratchPath); var keyFilePath = Path.Combine(scratchPath, "Key.snk"); - var projectFileContent = $""" - - - net7.0 - enable - enable - {keyFilePath} - - - """; - File.WriteAllText(Path.Combine(scratchPath, "classlibsigned.csproj"), projectFileContent, TestBase.DefaultEncoding); + DotnetUtil.AddProjectProperty($"{keyFilePath}", scratchPath); File.WriteAllBytes(keyFilePath, ResourceLoader.GetResourceBlob("Key.snk")); var program = """ using System; @@ -153,10 +170,9 @@ partial class Util { """; File.WriteAllText(Path.Combine(scratchPath, "Class1.cs"), program, TestBase.DefaultEncoding); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); - ClassLibMultiComplogPath = WithBuild("classlibmulti.complog", string (string scratchPath) => + ClassLibMultiComplogPath = WithBuild("classlibmulti.complog", void (string scratchPath) => { RunDotnetCommand($"new classlib --name classlibmulti --output .", scratchPath); var projectFileContent = """ @@ -179,44 +195,50 @@ partial class Util { """; File.WriteAllText(Path.Combine(scratchPath, "Class 1.cs"), program, TestBase.DefaultEncoding); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - WpfAppComplogPath = WithBuild("wpfapp.complog", string (string scratchPath) => + WpfAppComplogPath = WithBuild("wpfapp.complog", void (string scratchPath) => { RunDotnetCommand("new wpf --name wpfapp --output .", scratchPath); RunDotnetCommand("build -bl", scratchPath); - return Path.Combine(scratchPath, "msbuild.binlog"); }); } - AllComplogs = allCompLogs; - string WithBuild(string name, Func action) + _allCompLogs = builder.ToImmutable(); + Lazy WithBuild(string name, Action action) { - try - { - var scratchPath = Path.Combine(StorageDirectory, "scratch dir", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(scratchPath); - RunDotnetCommand("new globaljson --sdk-version 7.0.400", scratchPath); - var binlogFilePath = action(scratchPath); - Assert.True(File.Exists(binlogFilePath)); - var complogFilePath = Path.Combine(ComplogDirectory, name); - var diagnostics = CompilerLogUtil.ConvertBinaryLog(binlogFilePath, complogFilePath); - Assert.Empty(diagnostics); - Directory.Delete(scratchPath, recursive: true); - allCompLogs.Add(complogFilePath); - return complogFilePath; - } - catch (Exception ex) + var lazy = new Lazy(() => { - messageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString())); - throw new Exception($"Cannot generate compiler log {name}", ex); - } + try + { + var scratchPath = Path.Combine(StorageDirectory, "scratch dir", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(scratchPath); + RunDotnetCommand("new globaljson --sdk-version 7.0.400", scratchPath); + action(scratchPath); + var binlogFilePath = Path.Combine(scratchPath, "msbuild.binlog"); + Assert.True(File.Exists(binlogFilePath)); + var complogFilePath = Path.Combine(ComplogDirectory, name); + var diagnostics = CompilerLogUtil.ConvertBinaryLog(binlogFilePath, complogFilePath); + Assert.Empty(diagnostics); + Directory.Delete(scratchPath, recursive: true); + return complogFilePath; + } + catch (Exception ex) + { + messageSink.OnMessage(new DiagnosticMessage(diagnosticBuilder.ToString())); + throw new Exception($"Cannot generate compiler log {name}", ex); + } + }); + + builder.Add(lazy); + return lazy; } } + public IEnumerable GetAllCompLogs() => _allCompLogs.Select(x => x.Value); + public void Dispose() { Directory.Delete(StorageDirectory, recursive: true); diff --git a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs index 6120931..d0cdd39 100644 --- a/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/CompilerLogReaderTests.cs @@ -104,7 +104,7 @@ public void ResourceSimpleEmbedded() public void KeyFileDefault() { var keyBytes = ResourceLoader.GetResourceBlob("Key.snk"); - using var reader = CompilerLogReader.Create(Fixture.ClassLibSignedComplogPath); + using var reader = CompilerLogReader.Create(Fixture.ClassLibSignedComplogPath.Value); var data = reader.ReadCompilationData(0); Assert.NotNull(data.CompilationOptions.CryptoKeyFile); @@ -121,7 +121,7 @@ public void KeyFileCustomState() using var state = new CompilerLogState(tempDir.DirectoryPath); var keyBytes = ResourceLoader.GetResourceBlob("Key.snk"); - using var reader = CompilerLogReader.Create(Fixture.ClassLibSignedComplogPath, state: state); + using var reader = CompilerLogReader.Create(Fixture.ClassLibSignedComplogPath.Value, state: state); var data = reader.ReadCompilationData(0); Assert.NotNull(data.CompilationOptions.CryptoKeyFile); @@ -150,7 +150,7 @@ public void AnalyzerLoadOptions() any = true; var options = new BasicAnalyzerHostOptions(kind); - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, options: options); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, options: options); var data = reader.ReadCompilationData(0); var compilation = data.GetCompilationAfterGenerators(out var diagnostics); Assert.Empty(diagnostics); @@ -181,7 +181,7 @@ public void AnalyzerLoadCaching(BasicAnalyzerKind kind) } var options = new BasicAnalyzerHostOptions(kind, cacheable: true); - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, options: options); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, options: options); var data = reader.ReadRawCompilationData(0).Item2; var host1 = reader.ReadAnalyzers(data); @@ -203,7 +203,7 @@ public void AnalyzerLoadDispose(BasicAnalyzerKind kind) } var options = new BasicAnalyzerHostOptions(kind, cacheable: true); - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, options: options); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, options: options); var data = reader.ReadCompilationData(0); Assert.False(data.BasicAnalyzerHost.IsDisposed); reader.Dispose(); @@ -213,7 +213,7 @@ public void AnalyzerLoadDispose(BasicAnalyzerKind kind) [Fact] public void ProjectSingleTarget() { - using var reader = CompilerLogReader.Create(Fixture.ClassLibComplogPath); + using var reader = CompilerLogReader.Create(Fixture.ClassLibComplogPath.Value); var list = reader.ReadAllCompilationData(); Assert.Single(list); Assert.NotNull(list.Single(x => x.CompilerCall.TargetFramework == "net7.0")); @@ -222,7 +222,7 @@ public void ProjectSingleTarget() [Fact] public void ProjectMultiTarget() { - using var reader = CompilerLogReader.Create(Fixture.ClassLibMultiComplogPath); + using var reader = CompilerLogReader.Create(Fixture.ClassLibMultiComplogPath.Value); var list = reader.ReadAllCompilationData(); Assert.Equal(2, list.Count); Assert.NotNull(list.Single(x => x.CompilerCall.TargetFramework == "net6.0")); @@ -232,8 +232,9 @@ public void ProjectMultiTarget() [Fact] public void EmitToDisk() { - Assert.NotEmpty(Fixture.AllComplogs); - foreach (var complogPath in Fixture.AllComplogs) + var all = Fixture.GetAllCompLogs(); + Assert.NotEmpty(all); + foreach (var complogPath in all) { TestOutputHelper.WriteLine(complogPath); using var reader = CompilerLogReader.Create(complogPath); @@ -250,8 +251,9 @@ public void EmitToDisk() [Fact] public void EmitToMemory() { - Assert.NotEmpty(Fixture.AllComplogs); - foreach (var complogPath in Fixture.AllComplogs) + var all = Fixture.GetAllCompLogs(); + Assert.NotEmpty(all); + foreach (var complogPath in all) { TestOutputHelper.WriteLine(complogPath); using var reader = CompilerLogReader.Create(complogPath); @@ -267,7 +269,7 @@ public void EmitToMemory() [Fact] public void NoneHostGeneratedFilesInRaw() { - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, BasicAnalyzerHostOptions.None); var (_, data) = reader.ReadRawCompilationData(0); Assert.Equal(1, data.Contents.Count(x => x.Kind == RawContentKind.GeneratedText)); } @@ -275,7 +277,7 @@ public void NoneHostGeneratedFilesInRaw() [Fact] public void NoneHostGeneratedFilesShouldBeLast() { - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, BasicAnalyzerHostOptions.None); var data = reader.ReadCompilationData(0); var tree = data.GetCompilationAfterGenerators().SyntaxTrees.Last(); var decls = tree.GetRoot().DescendantNodes().OfType().ToList(); @@ -287,7 +289,7 @@ public void NoneHostGeneratedFilesShouldBeLast() [Fact] public void NoneHostAddsFakeGeneratorForGeneratedSource() { - using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath, BasicAnalyzerHostOptions.None); + using var reader = CompilerLogReader.Create(Fixture.ConsoleComplogPath.Value, BasicAnalyzerHostOptions.None); var data = reader.ReadCompilationData(0); var compilation1 = data.Compilation; var compilation2 = data.GetCompilationAfterGenerators(); @@ -298,7 +300,7 @@ public void NoneHostAddsFakeGeneratorForGeneratedSource() [Fact] public void NoneHostAddsNoGeneratorIfNoGeneratedSource() { - using var reader = CompilerLogReader.Create(Fixture.ConsoleNoGeneratorComplogPath, BasicAnalyzerHostOptions.None); + using var reader = CompilerLogReader.Create(Fixture.ConsoleNoGeneratorComplogPath.Value, BasicAnalyzerHostOptions.None); var data = reader.ReadCompilationData(0); var compilation1 = data.Compilation; var compilation2 = data.GetCompilationAfterGenerators(); @@ -344,11 +346,46 @@ public void KindWpf() if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Assert.NotNull(Fixture.WpfAppComplogPath); - using var reader = CompilerLogReader.Create(Fixture.WpfAppComplogPath); + using var reader = CompilerLogReader.Create(Fixture.WpfAppComplogPath.Value); var list = reader.ReadAllCompilationData(); Assert.Equal(2, list.Count); Assert.Equal(CompilerCallKind.WpfTemporaryCompile, list[0].Kind); Assert.Equal(CompilerCallKind.Regular, list[1].Kind); } } + + /// + /// Ensure diagnostics are issued for the cases where a #line refers to + /// a target that can't be exported to another computer correctly. + /// + [Fact] + public void EmbedLineIssues() + { + // Outside project full path + { + using var temp = new TempDir(); + Core(temp.NewFile("content.txt", "this is some content")); + } + + // Inside project full path + { + Core(Root.NewFile("content.txt", "this is some content")); + } + + void Core(string contentFilePath) + { + RunDotNet($"new console --name example --output ."); + AddProjectProperty("true"); + File.WriteAllText(Path.Combine(RootDirectory, "Util.cs"), + $""" + #line 42 "{contentFilePath}" + """); + RunDotNet("build -bl"); + var diagnostics = CompilerLogUtil.ConvertBinaryLog( + Path.Combine(RootDirectory, "msbuild.binlog"), + Path.Combine(RootDirectory, "msbuild.complog")); + Assert.Single(diagnostics); + Root.EmptyDirectory(); + } + } } diff --git a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs index 8190789..8b58b1b 100644 --- a/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs +++ b/src/Basic.CompilerLog.UnitTests/ExportUtilTests.cs @@ -24,7 +24,7 @@ public ExportUtilTests(ITestOutputHelper testOutputHelper, CompilerLogFixture fi Fixture = fixture; } - private void TestExport(int expectedCount, Action? callback = null) + private void TestExport(int expectedCount, Action? verifyExportCallback = null) { using var scratchDir = new TempDir("export test"); var binlogFilePath = Path.Combine(RootDirectory, "msbuild.binlog"); @@ -36,10 +36,10 @@ private void TestExport(int expectedCount, Action? callback = null) // ensures our builds below don't succeed because old files are being referenced Root.EmptyDirectory(); - TestExport(compilerLogFilePath, expectedCount, callback: callback); + TestExport(compilerLogFilePath, expectedCount, verifyExportCallback: verifyExportCallback); } - private void TestExport(string compilerLogFilePath, int? expectedCount, bool includeAnalyzers = true, Action? callback = null) + private void TestExport(string compilerLogFilePath, int? expectedCount, bool includeAnalyzers = true, Action? verifyExportCallback = null) { using var reader = CompilerLogReader.Create(compilerLogFilePath); #if NETCOREAPP @@ -60,7 +60,7 @@ private void TestExport(string compilerLogFilePath, int? expectedCount, bool inc var buildResult = RunBuildCmd(tempDir.DirectoryPath); TestOutputHelper.WriteLine(buildResult.StandardOut); TestOutputHelper.WriteLine(buildResult.StandardError); - Assert.True(buildResult.Succeeded); + Assert.True(buildResult.Succeeded, $"Cannot build {Path.GetFileName(compilerLogFilePath)}"); // Ensure that full paths aren't getting written out to the RSP file. That makes the // build non-xcopyable. @@ -69,7 +69,7 @@ private void TestExport(string compilerLogFilePath, int? expectedCount, bool inc Assert.False(line.Contains(tempDir.DirectoryPath, StringComparison.OrdinalIgnoreCase), $"Has full path: {line}"); } - callback?.Invoke(tempDir.DirectoryPath); + verifyExportCallback?.Invoke(tempDir.DirectoryPath); } if (expectedCount is { } ec) @@ -85,13 +85,13 @@ private void TestExport(string compilerLogFilePath, int? expectedCount, bool inc [Fact] public void Console() { - TestExport(Fixture.ConsoleComplogPath, 1); + TestExport(Fixture.ConsoleComplogPath.Value, 1); } [Fact] public void ClassLib() { - TestExport(Fixture.ClassLibComplogPath, 1); + TestExport(Fixture.ClassLibComplogPath.Value, 1); } /// @@ -100,7 +100,7 @@ public void ClassLib() [Fact] public void GeneratedText() { - TestExport(Fixture.ConsoleComplogPath, 1, callback: tempPath => + TestExport(Fixture.ConsoleComplogPath.Value, 1, verifyExportCallback: tempPath => { var generatedPath = Path.Combine(tempPath, "generated"); var files = Directory.GetFiles(generatedPath, "*.cs", SearchOption.AllDirectories); @@ -115,7 +115,7 @@ public void GeneratedText() [Fact] public void GeneratedTextExcludeAnalyzers() { - TestExport(Fixture.ConsoleComplogPath, 1, includeAnalyzers: false, callback: tempPath => + TestExport(Fixture.ConsoleComplogPath.Value, 1, includeAnalyzers: false, verifyExportCallback: tempPath => { var rspPath = Path.Combine(tempPath, "build.rsp"); var foundPath = false; @@ -272,31 +272,40 @@ This is an awesome resource public void StrongNameKey() { RunDotNet($"new console --name example --output ."); - var projectFileContent = """ - - - Exe - net7.0 - enable - enable - true - key.snk - - - """; - File.WriteAllText(Path.Combine(RootDirectory, "example.csproj"), projectFileContent, DefaultEncoding); + AddProjectProperty("true"); + AddProjectProperty("key.snk"); var keyBytes = ResourceLoader.GetResourceBlob("Key.snk"); File.WriteAllBytes(Path.Combine(RootDirectory, "key.snk"), keyBytes); RunDotNet("build -bl"); TestExport(1); } + private void EmbedLineCore(string contentFilePath) + { + RunDotNet($"new console --name example --output ."); + AddProjectProperty("true"); + File.WriteAllText(Path.Combine(RootDirectory, "Util.cs"), + $""" + #line 42 "{contentFilePath}" + """); + RunDotNet("build -bl"); + TestExport(1); + } + + [Fact] + public void EmbedLineInsideProject() + { + // Relative + _ = Root.NewFile("content.txt", "this is some content"); + EmbedLineCore("content.txt"); + } + [Theory] [InlineData(true)] [InlineData(false)] public void AllCompilerLogs(bool includeAnalyzers) { - foreach (var complogPath in Fixture.AllComplogs) + foreach (var complogPath in Fixture.GetAllCompLogs()) { TestExport(complogPath, expectedCount: null, includeAnalyzers); } diff --git a/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs b/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs index 027f7d2..495e7fc 100644 --- a/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs +++ b/src/Basic.CompilerLog.UnitTests/SolutionReaderTests.cs @@ -23,7 +23,7 @@ public SolutionReaderTests(ITestOutputHelper testOutputHelper, CompilerLogFixtur private void LoadAllCore(BasicAnalyzerHostOptions options) { - foreach (var complogPath in Fixture.AllComplogs) + foreach (var complogPath in Fixture.GetAllCompLogs()) { using var reader = SolutionReader.Create(complogPath, options); var workspace = new AdhocWorkspace(); @@ -46,7 +46,7 @@ public void LoadAllWithoutAnalyzers() => public async Task DocumentsHaveGeneratedTextWithAnalyzers(BasicAnalyzerKind kind) { var host = new BasicAnalyzerHostOptions(kind); - using var reader = SolutionReader.Create(Fixture.ConsoleComplogPath, host); + using var reader = SolutionReader.Create(Fixture.ConsoleComplogPath.Value, host); var workspace = new AdhocWorkspace(); var solution = workspace.AddSolution(reader.ReadSolutionInfo()); var project = solution.Projects.Single(); diff --git a/src/Basic.CompilerLog.UnitTests/TempDir.cs b/src/Basic.CompilerLog.UnitTests/TempDir.cs index 8f73122..dd59484 100644 --- a/src/Basic.CompilerLog.UnitTests/TempDir.cs +++ b/src/Basic.CompilerLog.UnitTests/TempDir.cs @@ -23,7 +23,24 @@ public TempDir(string? name = null) public void Dispose() { - Directory.Delete(DirectoryPath, recursive: true); + if (Directory.Exists(DirectoryPath)) + { + Directory.Delete(DirectoryPath, recursive: true); + } + } + + public string NewFile(string fileName, string content) + { + var filePath = Path.Combine(DirectoryPath, fileName); + File.WriteAllText(filePath, content); + return filePath; + } + + public string NewDirectory(string name) + { + var path = Path.Combine(DirectoryPath, name); + _ = Directory.CreateDirectory(path); + return path; } public void EmptyDirectory() diff --git a/src/Basic.CompilerLog.UnitTests/TestBase.cs b/src/Basic.CompilerLog.UnitTests/TestBase.cs index a00d334..60a7e9b 100644 --- a/src/Basic.CompilerLog.UnitTests/TestBase.cs +++ b/src/Basic.CompilerLog.UnitTests/TestBase.cs @@ -1,6 +1,7 @@ using Basic.CompilerLog.Util; using System; using System.Collections.Generic; +using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; @@ -53,6 +54,11 @@ protected void RunDotNet(string command, string? workingDirectory = null) Assert.Equal(0, result.ExitCode); } + protected void AddProjectProperty(string property, string? workingDirectory = null) + { + DotnetUtil.AddProjectProperty(property, workingDirectory ?? RootDirectory); + } + protected string GetBinaryLogFullPath(string? workingDirectory = null) => Path.Combine(workingDirectory ?? RootDirectory, "msbuild.binlog"); diff --git a/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs b/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs index e532b41..e50a049 100644 --- a/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs +++ b/src/Basic.CompilerLog.Util/CompilerLogBuilder.cs @@ -1,23 +1,18 @@ -using Basic.CompilerLog.Util.Impl; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.VisualBasic; -using System; -using System.Collections.Generic; +using Microsoft.CodeAnalysis.VisualBasic.Syntax; +using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO.Compression; -using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; -using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; -using System.Threading.Tasks; using static Basic.CompilerLog.Util.CommonUtil; namespace Basic.CompilerLog.Util; @@ -69,7 +64,7 @@ internal bool Add(CompilerCall compilerCall) compilationWriter.WriteLine(arg); } - var baseDirectory = Path.GetDirectoryName(compilerCall.ProjectFilePath); + var baseDirectory = Path.GetDirectoryName(compilerCall.ProjectFilePath)!; CommandLineArguments commandLineArguments = compilerCall.IsCSharp ? CSharpCommandLineParser.Default.Parse(arguments, baseDirectory, sdkDirectory: null, additionalReferenceDirectories: null) : VisualBasicCommandLineParser.Default.Parse(arguments, baseDirectory, sdkDirectory: null, additionalReferenceDirectories: null); @@ -83,7 +78,7 @@ internal bool Add(CompilerCall compilerCall) AddSources(compilationWriter, commandLineArguments); AddAdditionalTexts(compilationWriter, commandLineArguments); AddResources(compilationWriter, commandLineArguments); - AddedEmbeds(compilationWriter, commandLineArguments); + AddEmbeds(compilationWriter, compilerCall, commandLineArguments, baseDirectory); AddContentIf("link", commandLineArguments.SourceLink); AddContentIf("ruleset", commandLineArguments.RuleSetPath); AddContentIf("appconfig", commandLineArguments.AppConfigPath); @@ -195,6 +190,12 @@ void WriteSourceInfo() } } + private void AddContentCore(StreamWriter compilationWriter, string key, string filePath, Stream stream) + { + var contentHash = AddContent(stream); + compilationWriter.WriteLine($"{key}:{contentHash}:{filePath}"); + } + private void AddContentCore(StreamWriter compilationWriter, string key, string filePath) { var contentHash = AddContent(filePath); @@ -455,11 +456,75 @@ private void AddResources(StreamWriter compilationWriter, CommandLineArguments a } } - private void AddedEmbeds(StreamWriter compilationWriter, CommandLineArguments args) + private void AddEmbeds(StreamWriter compilationWriter, CompilerCall compilerCall, CommandLineArguments args, string baseDirectory) { + if (args.EmbeddedFiles.Length == 0) + { + return; + } + + // Embedded files is one place where the compiler requires strict ordinal matching + var sourceFileSet = new HashSet(args.SourceFiles.Select(static x => x.Path), StringComparer.Ordinal); + var lineSet = new HashSet(StringComparer.Ordinal); + var resolver = new SourceFileResolver(ImmutableArray.Empty, args.BaseDirectory, args.PathMap); foreach (var e in args.EmbeddedFiles) { - AddContentCore(compilationWriter, "embed", e.Path); + using var stream = OpenFileForRead(e.Path); + AddContentCore(compilationWriter, "embed", e.Path, stream); + + // When the compiler embeds a source file it will also embed the targets of any + // #line directives in the code + if (sourceFileSet.Contains(e.Path)) + { + foreach (string rawTarget in GetLineTargets()) + { + var resolvedTarget = resolver.ResolveReference(rawTarget, e.Path); + if (resolvedTarget is not null) + { + AddContentCore(compilationWriter, "embedline", resolvedTarget); + + // Presently the compiler does not use /pathhmap when attempting to resolve + // #line targets for embedded files. That means if the path is a full one here, or + // resolved outside the cone of the project then it can't be exported later so + // issue a diagnostic. + // + // The original project directory from a compiler point of view is arbitrary as + // compilers don't know about projects. Compiler logs center some operations, + // like export, around the project directory.For export anything under the + // original project directory will maintain the same relative relationship to + // each other. Outside that though there is no relative relationship. + // + // https://github.com/dotnet/roslyn/issues/69659 + if (Path.IsPathRooted(rawTarget) || + !resolvedTarget.StartsWith(baseDirectory, PathUtil.Comparison)) + { + Diagnostics.Add($"Cannot embed #line target {rawTarget} in {compilerCall.GetDiagnosticName()}"); + } + } + } + + IEnumerable GetLineTargets() + { + var sourceText = RoslynUtil.GetSourceText(stream, args.ChecksumAlgorithm, canBeEmbedded: false); + if (args.ParseOptions is CSharpParseOptions csharpParseOptions) + { + var syntaxTree = CSharpSyntaxTree.ParseText(sourceText, csharpParseOptions); + foreach (var line in syntaxTree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType()) + { + yield return line.File.Text.Trim('"'); + } + } + else + { + var basicParseOptions = (VisualBasicParseOptions)args.ParseOptions; + var syntaxTree = VisualBasicSyntaxTree.ParseText(sourceText, basicParseOptions); + foreach (var line in syntaxTree.GetRoot().GetDirectives(static x => x.Kind() == Microsoft.CodeAnalysis.VisualBasic.SyntaxKind.ExternalSourceDirectiveTrivia).OfType()) + { + yield return line.ExternalSource.Text.Trim('"'); + } + } + } + } } } diff --git a/src/Basic.CompilerLog.Util/CompilerLogReader.cs b/src/Basic.CompilerLog.Util/CompilerLogReader.cs index 627ea63..6961ca0 100644 --- a/src/Basic.CompilerLog.Util/CompilerLogReader.cs +++ b/src/Basic.CompilerLog.Util/CompilerLogReader.cs @@ -1,4 +1,5 @@ using Basic.CompilerLog.Util.Impl; +using Microsoft.Build.Logging.StructuredLogger; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -134,32 +135,32 @@ public CompilationData ReadCompilationData(CompilerCall compilerCall) : rawCompilationData.Resources.Select(x => x.ResourceDescription).ToList(); List? embeddedTexts = null; - foreach (var tuple in rawCompilationData.Contents) + foreach (var rawContent in rawCompilationData.Contents) { - switch (tuple.Kind) + switch (rawContent.Kind) { case RawContentKind.SourceText: - sourceTextList.Add((GetSourceText(tuple.ContentHash, hashAlgorithm), tuple.FilePath)); + sourceTextList.Add((GetSourceText(rawContent.ContentHash, hashAlgorithm), rawContent.FilePath)); break; case RawContentKind.GeneratedText: // Handled when creating the analyzer host break; case RawContentKind.AnalyzerConfig: - analyzerConfigList.Add((GetSourceText(tuple.ContentHash, hashAlgorithm), tuple.FilePath)); + analyzerConfigList.Add((GetSourceText(rawContent.ContentHash, hashAlgorithm), rawContent.FilePath)); break; case RawContentKind.AdditionalText: additionalTextList.Add(new BasicAdditionalTextFile( - tuple.FilePath, - GetSourceText(tuple.ContentHash, hashAlgorithm))); + rawContent.FilePath, + GetSourceText(rawContent.ContentHash, hashAlgorithm))); break; case RawContentKind.CryptoKeyFile: - HandleCryptoKeyFile(tuple.ContentHash); + HandleCryptoKeyFile(rawContent.ContentHash); break; case RawContentKind.SourceLink: - sourceLinkStream = GetStateAwareContentStream(tuple.ContentHash); + sourceLinkStream = GetStateAwareContentStream(rawContent.ContentHash); break; case RawContentKind.Win32Resource: - win32ResourceStream = GetStateAwareContentStream(tuple.ContentHash); + win32ResourceStream = GetStateAwareContentStream(rawContent.ContentHash); break; case RawContentKind.Embed: { @@ -168,12 +169,17 @@ public CompilationData ReadCompilationData(CompilerCall compilerCall) embeddedTexts = new List(); } - var sourceText = GetSourceText(tuple.ContentHash, hashAlgorithm, canBeEmbedded: true); - var embeddedText = EmbeddedText.FromSource(tuple.FilePath, sourceText); + var sourceText = GetSourceText(rawContent.ContentHash, hashAlgorithm, canBeEmbedded: true); + var embeddedText = EmbeddedText.FromSource(rawContent.FilePath, sourceText); embeddedTexts.Add(embeddedText); break; } + // not exposed as #line embeds don't matter for most API usages, it's only used in + // command line compiles + case RawContentKind.EmbedLine: + break; + // not exposed yet case RawContentKind.RuleSet: case RawContentKind.AppConfig: @@ -248,7 +254,7 @@ CSharpCompilationData CreateCSharp() var csharpArgs = (CSharpCommandLineArguments)rawCompilationData.Arguments; var csharpOptions = (CSharpCompilationOptions)compilationOptions; var parseOptions = csharpArgs.ParseOptions; - var syntaxTrees = ParseSourceTexts(sourceTextList); + var syntaxTrees = RoslynUtil.ParseAllCSharp(sourceTextList, parseOptions); var (syntaxProvider, analyzerProvider) = CreateOptionsProviders(syntaxTrees, additionalTextList); csharpOptions = csharpOptions @@ -269,25 +275,6 @@ CSharpCompilationData CreateCSharp() additionalTextList.ToImmutableArray(), ReadAnalyzers(rawCompilationData), analyzerProvider); - - SyntaxTree[] ParseSourceTexts(List<(SourceText SourceText, string Path)> sourceTextList) - { - if (sourceTextList.Count == 0) - { - return Array.Empty(); - } - - var syntaxTrees = new SyntaxTree[sourceTextList.Count]; - Parallel.For( - 0, - sourceTextList.Count, - i => - { - var t = sourceTextList[i]; - syntaxTrees[i] = CSharpSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); - }); - return syntaxTrees; - } } VisualBasicCompilationData CreateVisualBasic() @@ -295,7 +282,7 @@ VisualBasicCompilationData CreateVisualBasic() var basicArgs = (VisualBasicCommandLineArguments)rawCompilationData.Arguments; var basicOptions = (VisualBasicCompilationOptions)compilationOptions; var parseOptions = basicArgs.ParseOptions; - var syntaxTrees = ParseSourceTexts(sourceTextList); + var syntaxTrees = RoslynUtil.ParseAllVisualBasic(sourceTextList, parseOptions); var (syntaxProvider, analyzerProvider) = CreateOptionsProviders(syntaxTrees, additionalTextList); basicOptions = basicOptions @@ -316,25 +303,6 @@ VisualBasicCompilationData CreateVisualBasic() additionalTextList.ToImmutableArray(), ReadAnalyzers(rawCompilationData), analyzerProvider); - - SyntaxTree[] ParseSourceTexts(List<(SourceText SourceText, string Path)> sourceTextList) - { - if (sourceTextList.Count == 0) - { - return Array.Empty(); - } - - var syntaxTrees = new SyntaxTree[sourceTextList.Count]; - Parallel.For( - 0, - sourceTextList.Count, - i => - { - var t = sourceTextList[i]; - syntaxTrees[i] = VisualBasicSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); - }); - return syntaxTrees; - } } } @@ -378,7 +346,7 @@ internal RawCompilationData ReadRawCompilationData(CompilerCall compilerCall) : VisualBasicCommandLineParser.Default.Parse(compilerCall.Arguments, Path.GetDirectoryName(compilerCall.ProjectFilePath), sdkDirectory: null, additionalReferenceDirectories: null); var references = new List(); var analyzers = new List(); - var contents = new List<(string FilePath, string ContentHash, RawContentKind Kind)>(); + var contents = new List(); var resources = new List(); var readGeneratedFiles = false; @@ -411,6 +379,9 @@ internal RawCompilationData ReadRawCompilationData(CompilerCall compilerCall) case "embed": ParseContent(line, RawContentKind.Embed); break; + case "embedline": + ParseContent(line, RawContentKind.EmbedLine); + break; case "link": ParseContent(line, RawContentKind.SourceLink); break; @@ -484,7 +455,7 @@ void ParseMetadataReference(string line) void ParseContent(string line, RawContentKind kind) { var items = line.Split(':', count: 3); - contents.Add((items[2], items[1], kind)); + contents.Add(new(items[2], items[1], kind)); } void ParseResource(string line) @@ -737,9 +708,7 @@ internal SourceText GetSourceText(string contentHash, SourceHashAlgorithm checks stream = ZipArchive.OpenEntryOrThrow(GetContentEntryName(contentHash)); } - // TODO: need to expose the real API for how the compiler reads source files. - // move this comment to the rehydration code when we write it. - return SourceText.From(stream, checksumAlgorithm: checksumAlgorithm, canBeEmbedded: canBeEmbedded); + return RoslynUtil.GetSourceText(stream, checksumAlgorithm: checksumAlgorithm, canBeEmbedded: canBeEmbedded); } finally { diff --git a/src/Basic.CompilerLog.Util/ExportUtil.cs b/src/Basic.CompilerLog.Util/ExportUtil.cs index 5098a71..5b96d23 100644 --- a/src/Basic.CompilerLog.Util/ExportUtil.cs +++ b/src/Basic.CompilerLog.Util/ExportUtil.cs @@ -136,6 +136,7 @@ public void Export(CompilerCall compilerCall, string destinationDir, IEnumerable var data = Reader.ReadRawCompilationData(compilerCall); Directory.CreateDirectory(destinationDir); WriteGeneratedFiles(); + WriteEmbedLines(); WriteContent(); WriteAnalyzers(); WriteReferences(); @@ -295,6 +296,7 @@ void WriteContent() RawContentKind.AdditionalText => "/additionalfile:", RawContentKind.AnalyzerConfig => "/analyzerconfig:", RawContentKind.Embed => "/embed:", + RawContentKind.EmbedLine => null, RawContentKind.SourceLink => "/sourcelink:", RawContentKind.RuleSet => "/ruleset:", RawContentKind.AppConfig => "/appconfig:", @@ -330,6 +332,15 @@ void WriteGeneratedFiles() } } + void WriteEmbedLines() + { + foreach (var tuple in data.Contents.Where(x => x.Kind == RawContentKind.EmbedLine)) + { + using var contentStream = Reader.GetContentStream(tuple.ContentHash); + var newPath = builder.WriteContent(tuple.FilePath, contentStream); + } + } + void WriteResources() { foreach (var resourceData in data.Resources) @@ -392,6 +403,12 @@ private static string MaybeQuoteArgument(string arg) return str; } + if (arg.Contains('=') || arg.Contains(',')) + { + var str = $@"""{arg}"""; + return str; + } + return arg; } diff --git a/src/Basic.CompilerLog.Util/RawCompilationData.cs b/src/Basic.CompilerLog.Util/RawCompilationData.cs index b277fb6..ca25644 100644 --- a/src/Basic.CompilerLog.Util/RawCompilationData.cs +++ b/src/Basic.CompilerLog.Util/RawCompilationData.cs @@ -16,6 +16,13 @@ internal enum RawContentKind AdditionalText, AnalyzerConfig, Embed, + + /// + /// This represents a #line directive target in a file that was embedded. These are different + /// than normal line directives in that they are embedded into the compilation as well so the + /// file is read from disk. + /// + EmbedLine, SourceLink, RuleSet, AppConfig, @@ -65,12 +72,31 @@ internal RawResourceData(string contentHash, ResourceDescription d) } } +internal readonly struct RawContent +{ + internal string FilePath { get; } + internal string ContentHash { get; } + internal RawContentKind Kind { get; } + + internal RawContent( + string filePath, + string contentHash, + RawContentKind kind) + { + FilePath = filePath; + ContentHash = contentHash; + Kind = kind; + } + + public override string ToString() => $"{Path.GetFileName(FilePath)} {Kind}"; +} + internal sealed class RawCompilationData { internal CommandLineArguments Arguments { get; } internal List References { get; } internal List Analyzers { get; } - internal List<(string FilePath, string ContentHash, RawContentKind Kind)> Contents { get; } + internal List Contents { get; } internal List Resources { get; } /// @@ -85,7 +111,7 @@ internal RawCompilationData( CommandLineArguments arguments, List references, List analyzers, - List<(string FilePath, string ContentHash, RawContentKind Kind)> contents, + List contents, List resources, bool readGeneratedFiles) { diff --git a/src/Basic.CompilerLog.Util/RoslynUtil.cs b/src/Basic.CompilerLog.Util/RoslynUtil.cs new file mode 100644 index 0000000..b367937 --- /dev/null +++ b/src/Basic.CompilerLog.Util/RoslynUtil.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.VisualBasic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Basic.CompilerLog.Util; + +internal static class RoslynUtil +{ + internal static SourceText GetSourceText(string filePath, SourceHashAlgorithm checksumAlgorithm, bool canBeEmbedded) + { + var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + return GetSourceText(stream, checksumAlgorithm, canBeEmbedded); + } + + /// + /// Get a source text + /// + /// + /// TODO: need to expose the real API for how the compiler reads source files. + /// move this comment to the rehydration code when we write it. + /// + internal static SourceText GetSourceText(Stream stream, SourceHashAlgorithm checksumAlgorithm, bool canBeEmbedded) => + SourceText.From(stream, checksumAlgorithm: checksumAlgorithm, canBeEmbedded: canBeEmbedded); + + internal static SyntaxTree[] ParseAll(IReadOnlyList<(SourceText SourceText, string Path)> sourceTextList, ParseOptions parseOptions) => + parseOptions switch + { + CSharpParseOptions csharp => ParseAllCSharp(sourceTextList, csharp), + VisualBasicParseOptions vb => ParseAllVisualBasic(sourceTextList, vb), + _ => throw new ArgumentException(nameof(parseOptions)), + }; + + internal static VisualBasicSyntaxTree[] ParseAllVisualBasic(IReadOnlyList<(SourceText SourceText, string Path)> sourceTextList, VisualBasicParseOptions parseOptions) + { + if (sourceTextList.Count == 0) + { + return Array.Empty(); + } + + var syntaxTrees = new VisualBasicSyntaxTree[sourceTextList.Count]; + Parallel.For( + 0, + sourceTextList.Count, + i => + { + var t = sourceTextList[i]; + syntaxTrees[i] = (VisualBasicSyntaxTree)VisualBasicSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); + }); + return syntaxTrees; + } + + internal static CSharpSyntaxTree[] ParseAllCSharp(IReadOnlyList<(SourceText SourceText, string Path)> sourceTextList, CSharpParseOptions parseOptions) + { + if (sourceTextList.Count == 0) + { + return Array.Empty(); + } + + var syntaxTrees = new CSharpSyntaxTree[sourceTextList.Count]; + Parallel.For( + 0, + sourceTextList.Count, + i => + { + var t = sourceTextList[i]; + syntaxTrees[i] = (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(t.SourceText, parseOptions, t.Path); + }); + return syntaxTrees; + } +} diff --git a/src/Basic.CompilerLog.Util/SolutionReader.cs b/src/Basic.CompilerLog.Util/SolutionReader.cs index de6d54a..d407b73 100644 --- a/src/Basic.CompilerLog.Util/SolutionReader.cs +++ b/src/Basic.CompilerLog.Util/SolutionReader.cs @@ -97,6 +97,7 @@ public ProjectInfo ReadProjectInfo(int index) case RawContentKind.Win32Icon: case RawContentKind.CryptoKeyFile: case RawContentKind.Embed: + case RawContentKind.EmbedLine: // Not exposed via the workspace APIs yet break; default: diff --git a/src/Scratch/Program.cs b/src/Scratch/Program.cs index 9f67a01..d370af5 100644 --- a/src/Scratch/Program.cs +++ b/src/Scratch/Program.cs @@ -12,12 +12,12 @@ #pragma warning disable 8321 -// var filePath = @"c:\users\jaredpar\temp\console\msbuild.binlog"; +var filePath = @"c:\users\jaredpar\temp\console\msbuild.binlog"; // var filePath = @"C:\Users\jaredpar\code\MudBlazor\src\msbuild.binlog"; // var filePath = @"C:\Users\jaredpar\code\roslyn\src\Compilers\Core\Portable\msbuild.binlog"; // var filePath = @"C:\Users\jaredpar\Downloads\Roslyn.complog"; // var filePath = @"C:\Users\jaredpar\code\wt\ros2\artifacts\log\Debug\Build.binlog"; -var filePath = @"C:\Users\jaredpar\code\roslyn\artifacts\log\Debug\Build.binlog"; +// var filePath = @"C:\Users\jaredpar\code\roslyn\artifacts\log\Debug\Build.binlog"; //var filePath = @"C:\Users\jaredpar\code\roslyn\src\Compilers\CSharp\csc\msbuild.binlog"; //TestDiagnostics(filePath); @@ -26,7 +26,7 @@ // await SolutionScratchAsync(filePath); -using var reader = CompilerLogReader.Create(@"C:\Users\jaredpar\Downloads\7.0.306.zip.binlog"); +using var reader = CompilerLogReader.Create(filePath); VerifyAll(filePath); Console.WriteLine("Done"); diff --git a/src/Shared/DotnetUtil.cs b/src/Shared/DotnetUtil.cs index 494d7fb..83ff16b 100644 --- a/src/Shared/DotnetUtil.cs +++ b/src/Shared/DotnetUtil.cs @@ -1,6 +1,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; +using System.Configuration.Internal; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; @@ -29,6 +30,22 @@ internal static void CommandOrThrow(string args, string? workingDirectory = null internal static ProcessResult Build(string args, string? workingDirectory = null) => Command($"build {args}", workingDirectory); + internal static void AddProjectProperty(string property, string workingDirectory) + { + var projectFile = Directory.EnumerateFiles(workingDirectory, "*proj").Single(); + var lines = File.ReadAllLines(projectFile); + using var writer = new StreamWriter(projectFile, append: false); + foreach (var line in lines) + { + if (line.Contains("")) + { + writer.WriteLine(property); + } + + writer.WriteLine(line); + } + } + internal static List GetSdkDirectories() { // TODO: has to be a better way to find the runtime directory but this works for the moment