From ec42050faab6a39f450db9e727b2e614e3615549 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 18:29:14 +0200 Subject: [PATCH 1/5] fix: replace ReadAllLines with ReadAllText in MigrationChecks.targets (fixes #1035) System.IO.File::ReadAllLines is not available as an MSBuild property function on all platforms (e.g. macOS GitHub Actions), causing MSB4185. ReadAllText is universally supported by MSBuild and sufficient for the regex check that detects 'electron' references in a root package.json (ELECTRON008). The intermediate ItemGroup for line accumulation is no longer needed. --- .../Tests/MigrationChecksTargetsTests.cs | 111 ++++++++++++++++++ .../build/ElectronNET.MigrationChecks.targets | 9 +- 2 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs diff --git a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs new file mode 100644 index 00000000..e87decfe --- /dev/null +++ b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs @@ -0,0 +1,111 @@ +using System.Diagnostics; + +namespace ElectronNET.IntegrationTests.Tests; + +/// +/// Unit tests for ElectronNET.MigrationChecks.targets — no Electron runtime required. +/// Covers GitHub issue #1035: System.IO.File.ReadAllLines is not available as an MSBuild +/// property function on all platforms (e.g. macOS GitHub Actions), causing MSB4185. +/// +public class MigrationChecksTargetsTests +{ + // AppContext.BaseDirectory resolves to: + // src\ElectronNET.IntegrationTests\bin\Debug\net10.0\win-x64\ + // Five levels up => src\ + private static readonly string TargetsFilePath = Path.GetFullPath( + Path.Combine( + AppContext.BaseDirectory, + "..", "..", "..", "..", "..", + "ElectronNET", "build", "ElectronNET.MigrationChecks.targets")); + + // ----------------------------------------------------------------------- + // Content-level test (RED before fix, GREEN after fix on ALL platforms) + // ----------------------------------------------------------------------- + + [Fact] + public void MigrationChecksTargets_ShouldNotUseReadAllLines() + { + // The file must exist — if this fails the path constant above is wrong. + File.Exists(TargetsFilePath).Should().BeTrue( + $"targets file must exist at '{TargetsFilePath}'"); + + var content = File.ReadAllText(TargetsFilePath); + + // System.IO.File::ReadAllLines is not in the MSBuild property-function + // whitelist on all platforms (MSB4185 on macOS GitHub Actions, see #1035). + // ReadAllText must be used instead. + content.Should().NotContain( + "::ReadAllLines(", + "because ReadAllLines is not available as an MSBuild property function on all " + + "platforms. Use ReadAllText instead (GitHub issue #1035)."); + } + + // ----------------------------------------------------------------------- + // Functional build test — verifies no MSB4185 at runtime + // (RED on platforms where ReadAllLines is restricted, GREEN after fix) + // ----------------------------------------------------------------------- + + [Fact] + public async Task MigrationChecksTargets_BuildWithPackageJsonContainingElectron_ShouldSucceedWithoutMSB4185() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + try + { + // Create a minimal package.json that contains "electron" so ELECTRON008 fires. + await File.WriteAllTextAsync( + Path.Combine(tempDir, "package.json"), + """{ "devDependencies": { "electron": "^30.0.0" } }"""); + + // Create a minimal csproj that only imports the migration checks targets. + // We deliberately import just that one targets file to keep the build fast. + var targetsPathEscaped = TargetsFilePath.Replace("'", "'"); + await File.WriteAllTextAsync( + Path.Combine(tempDir, "TestApp.csproj"), + $""" + + + {tempDir} + + + + + """); + + // ACT — run the Build target + var (exitCode, output) = await RunDotnetBuildAsync(tempDir); + + // ASSERT — the build must not produce the MSB4185 "unavailable function" error + output.Should().NotContain( + "MSB4185", + $"ReadAllLines must not be used as an MSBuild property function. " + + $"Full build output:\n{output}"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + // ----------------------------------------------------------------------- + // Helper + // ----------------------------------------------------------------------- + + private static async Task<(int ExitCode, string Output)> RunDotnetBuildAsync(string workingDirectory) + { + var psi = new ProcessStartInfo("dotnet", "build --nologo -v:minimal") + { + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + + using var process = Process.Start(psi)!; + var stdOut = await process.StandardOutput.ReadToEndAsync(); + var stdErr = await process.StandardError.ReadToEndAsync(); + await process.WaitForExitAsync(); + + return (process.ExitCode, stdOut + stdErr); + } +} diff --git a/src/ElectronNET/build/ElectronNET.MigrationChecks.targets b/src/ElectronNET/build/ElectronNET.MigrationChecks.targets index 666c1785..ab138a5d 100644 --- a/src/ElectronNET/build/ElectronNET.MigrationChecks.targets +++ b/src/ElectronNET/build/ElectronNET.MigrationChecks.targets @@ -73,12 +73,11 @@ EXCEPTION: - - <_RootPackageJsonLines Include="$([System.IO.File]::ReadAllLines('$(MSBuildProjectDirectory)\package.json'))" /> - - - <_RootPackageJsonContent>@(_RootPackageJsonLines, ' ') + + <_RootPackageJsonContent>$([System.IO.File]::ReadAllText('$(MSBuildProjectDirectory)\package.json')) <_RootPackageJsonHasElectron>false <_RootPackageJsonHasElectron Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('$(_RootPackageJsonContent)', 'electron', System.Text.RegularExpressions.RegexOptions.IgnoreCase))">true From 830d7bf9059dca4e8ca7e2ec91a42a532cfbe709 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 21:15:57 +0200 Subject: [PATCH 2/5] test: strengthen migration checks test with exit code assertion and remove reserved MSBuildProjectDirectory override --- .../Tests/MigrationChecksTargetsTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs index e87decfe..628b4f2a 100644 --- a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs @@ -59,14 +59,13 @@ await File.WriteAllTextAsync( // Create a minimal csproj that only imports the migration checks targets. // We deliberately import just that one targets file to keep the build fast. + // Note: MSBuildProjectDirectory is a reserved MSBuild property — it must not be + // redefined manually. MSBuild sets it automatically to the csproj's folder (tempDir). var targetsPathEscaped = TargetsFilePath.Replace("'", "'"); await File.WriteAllTextAsync( Path.Combine(tempDir, "TestApp.csproj"), $""" - - {tempDir} - @@ -75,7 +74,10 @@ await File.WriteAllTextAsync( // ACT — run the Build target var (exitCode, output) = await RunDotnetBuildAsync(tempDir); - // ASSERT — the build must not produce the MSB4185 "unavailable function" error + // ASSERT — the build must succeed and must not produce MSB4185 + exitCode.Should().Be(0, + $"the temporary MSBuild project should build successfully. Full build output:\n{output}"); + output.Should().NotContain( "MSB4185", $"ReadAllLines must not be used as an MSBuild property function. " + From 272e6ef7d58198f271e2f0766e8de21c800a8e28 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 21:19:31 +0200 Subject: [PATCH 3/5] test: locate MigrationChecks.targets via directory walk for path robustness --- .../Tests/MigrationChecksTargetsTests.cs | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs index 628b4f2a..c3075ee0 100644 --- a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs @@ -9,14 +9,42 @@ namespace ElectronNET.IntegrationTests.Tests; /// public class MigrationChecksTargetsTests { - // AppContext.BaseDirectory resolves to: - // src\ElectronNET.IntegrationTests\bin\Debug\net10.0\win-x64\ - // Five levels up => src\ - private static readonly string TargetsFilePath = Path.GetFullPath( - Path.Combine( - AppContext.BaseDirectory, - "..", "..", "..", "..", "..", - "ElectronNET", "build", "ElectronNET.MigrationChecks.targets")); + private static readonly string TargetsFilePath = FindTargetsFile(); + + /// + /// Walks up the directory tree from until it finds + /// the migration checks targets file. This is robust against varying output paths + /// (with or without RID subfolder, debug/release, etc.). + /// + private static string FindTargetsFile() + { + const string RelativeFromRepoRoot = + "src/ElectronNET/build/ElectronNET.MigrationChecks.targets"; + const string RelativeFromSrc = + "ElectronNET/build/ElectronNET.MigrationChecks.targets"; + + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var fromRepoRoot = Path.Combine(dir.FullName, RelativeFromRepoRoot); + if (File.Exists(fromRepoRoot)) + { + return Path.GetFullPath(fromRepoRoot); + } + + var fromSrc = Path.Combine(dir.FullName, RelativeFromSrc); + if (File.Exists(fromSrc)) + { + return Path.GetFullPath(fromSrc); + } + + dir = dir.Parent; + } + + throw new FileNotFoundException( + "Could not locate ElectronNET.MigrationChecks.targets by walking up from " + + $"'{AppContext.BaseDirectory}'."); + } // ----------------------------------------------------------------------- // Content-level test (RED before fix, GREEN after fix on ALL platforms) From 7b3f9001090d69b25dcd571485afc8bb9bafade4 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 21:40:27 +0200 Subject: [PATCH 4/5] test: replace em-dashes with ASCII hyphens to avoid bidi/hidden Unicode warning --- .../Tests/MigrationChecksTargetsTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs index c3075ee0..e6dadd19 100644 --- a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs @@ -3,7 +3,7 @@ namespace ElectronNET.IntegrationTests.Tests; /// -/// Unit tests for ElectronNET.MigrationChecks.targets — no Electron runtime required. +/// Unit tests for ElectronNET.MigrationChecks.targets - no Electron runtime required. /// Covers GitHub issue #1035: System.IO.File.ReadAllLines is not available as an MSBuild /// property function on all platforms (e.g. macOS GitHub Actions), causing MSB4185. /// @@ -53,7 +53,7 @@ private static string FindTargetsFile() [Fact] public void MigrationChecksTargets_ShouldNotUseReadAllLines() { - // The file must exist — if this fails the path constant above is wrong. + // The file must exist - if this fails the path constant above is wrong. File.Exists(TargetsFilePath).Should().BeTrue( $"targets file must exist at '{TargetsFilePath}'"); @@ -69,7 +69,7 @@ public void MigrationChecksTargets_ShouldNotUseReadAllLines() } // ----------------------------------------------------------------------- - // Functional build test — verifies no MSB4185 at runtime + // Functional build test - verifies no MSB4185 at runtime // (RED on platforms where ReadAllLines is restricted, GREEN after fix) // ----------------------------------------------------------------------- @@ -87,7 +87,7 @@ await File.WriteAllTextAsync( // Create a minimal csproj that only imports the migration checks targets. // We deliberately import just that one targets file to keep the build fast. - // Note: MSBuildProjectDirectory is a reserved MSBuild property — it must not be + // Note: MSBuildProjectDirectory is a reserved MSBuild property - it must not be // redefined manually. MSBuild sets it automatically to the csproj's folder (tempDir). var targetsPathEscaped = TargetsFilePath.Replace("'", "'"); await File.WriteAllTextAsync( @@ -99,10 +99,10 @@ await File.WriteAllTextAsync( """); - // ACT — run the Build target + // ACT - run the Build target var (exitCode, output) = await RunDotnetBuildAsync(tempDir); - // ASSERT — the build must succeed and must not produce MSB4185 + // ASSERT - the build must succeed and must not produce MSB4185 exitCode.Should().Be(0, $"the temporary MSBuild project should build successfully. Full build output:\n{output}"); From 694edd3bbc81fd36fc06cafd04a430c144522f99 Mon Sep 17 00:00:00 2001 From: Gregor Biswanger Date: Sat, 9 May 2026 22:05:28 +0200 Subject: [PATCH 5/5] test: split build test into clean and electron-containing package.json scenarios --- .../Tests/MigrationChecksTargetsTests.cs | 96 ++++++++++++++----- 1 file changed, 74 insertions(+), 22 deletions(-) diff --git a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs index e6dadd19..93b18173 100644 --- a/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs +++ b/src/ElectronNET.IntegrationTests/Tests/MigrationChecksTargetsTests.cs @@ -74,42 +74,70 @@ public void MigrationChecksTargets_ShouldNotUseReadAllLines() // ----------------------------------------------------------------------- [Fact] - public async Task MigrationChecksTargets_BuildWithPackageJsonContainingElectron_ShouldSucceedWithoutMSB4185() + public async Task MigrationChecksTargets_BuildWithCleanPackageJson_ShouldSucceedWithoutMSB4185() { - var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-test-{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); + // Positive case: a package.json that does NOT mention electron. + // The migration check must successfully read the file via ReadAllText + // (the code path fixed by issue #1035) without producing MSB4185. + + var tempDir = CreateTempProjectDirectory(); try { - // Create a minimal package.json that contains "electron" so ELECTRON008 fires. await File.WriteAllTextAsync( Path.Combine(tempDir, "package.json"), - """{ "devDependencies": { "electron": "^30.0.0" } }"""); + """{ "devDependencies": { "vite": "^5.0.0" } }"""); + + await WriteMinimalCsprojAsync(tempDir); + + var (exitCode, output) = await RunDotnetBuildAsync(tempDir); + + exitCode.Should().Be(0, + $"the build must succeed when the package.json contains no electron references. " + + $"Full build output:\n{output}"); - // Create a minimal csproj that only imports the migration checks targets. - // We deliberately import just that one targets file to keep the build fast. - // Note: MSBuildProjectDirectory is a reserved MSBuild property - it must not be - // redefined manually. MSBuild sets it automatically to the csproj's folder (tempDir). - var targetsPathEscaped = TargetsFilePath.Replace("'", "'"); + output.Should().NotContain( + "MSB4185", + $"ReadAllLines must not be used as an MSBuild property function. " + + $"Full build output:\n{output}"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + [Fact] + public async Task MigrationChecksTargets_BuildWithPackageJsonContainingElectron_ShouldEmitELECTRON008WarningWithoutMSB4185() + { + // Negative case: a package.json that DOES contain "electron". + // The migration check must still read the file successfully (no MSB4185) + // and must emit the expected ELECTRON008 warning. ELECTRON008 is a + // , not an , so the build itself still succeeds. + + var tempDir = CreateTempProjectDirectory(); + try + { await File.WriteAllTextAsync( - Path.Combine(tempDir, "TestApp.csproj"), - $""" - - - - - """); - - // ACT - run the Build target + Path.Combine(tempDir, "package.json"), + """{ "devDependencies": { "electron": "^30.0.0" } }"""); + + await WriteMinimalCsprojAsync(tempDir); + var (exitCode, output) = await RunDotnetBuildAsync(tempDir); - // ASSERT - the build must succeed and must not produce MSB4185 exitCode.Should().Be(0, - $"the temporary MSBuild project should build successfully. Full build output:\n{output}"); + $"ELECTRON008 is a Warning (not an Error) so the build itself must still " + + $"succeed. Full build output:\n{output}"); output.Should().NotContain( "MSB4185", $"ReadAllLines must not be used as an MSBuild property function. " + $"Full build output:\n{output}"); + + output.Should().Contain( + "ELECTRON008", + $"the migration check must still detect electron references in package.json " + + $"after the ReadAllText migration. Full build output:\n{output}"); } finally { @@ -118,9 +146,33 @@ await File.WriteAllTextAsync( } // ----------------------------------------------------------------------- - // Helper + // Helpers // ----------------------------------------------------------------------- + private static string CreateTempProjectDirectory() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + return tempDir; + } + + private static Task WriteMinimalCsprojAsync(string tempDir) + { + // A minimal csproj that only imports the migration checks targets to keep the + // build fast. Note: MSBuildProjectDirectory is a reserved MSBuild property and + // must not be redefined manually; MSBuild sets it automatically to the folder + // of the csproj (which is tempDir here). + var targetsPathEscaped = TargetsFilePath.Replace("'", "'"); + return File.WriteAllTextAsync( + Path.Combine(tempDir, "TestApp.csproj"), + $""" + + + + + """); + } + private static async Task<(int ExitCode, string Output)> RunDotnetBuildAsync(string workingDirectory) { var psi = new ProcessStartInfo("dotnet", "build --nologo -v:minimal")