diff --git a/src/ElectronNET.IntegrationTests/Tests/ElectronIconTargetsTests.cs b/src/ElectronNET.IntegrationTests/Tests/ElectronIconTargetsTests.cs new file mode 100644 index 0000000..124bb97 --- /dev/null +++ b/src/ElectronNET.IntegrationTests/Tests/ElectronIconTargetsTests.cs @@ -0,0 +1,127 @@ +using System.Diagnostics; + +namespace ElectronNET.IntegrationTests.Tests; + +/// +/// Tests for ElectronNET.Core.targets icon copy behavior. +/// Covers GitHub issue #1047: modern macOS .icon app-icon packages are folders +/// and must be copied recursively, not treated as a single file. +/// +public class ElectronIconTargetsTests +{ + private static readonly string CorePropsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.props", "ElectronNET/build/ElectronNET.Core.props"); + private static readonly string CoreTargetsPath = FindBuildFile("src/ElectronNET/build/ElectronNET.Core.targets", "ElectronNET/build/ElectronNET.Core.targets"); + + [Fact] + public async Task ElectronCoreTargets_ElectronIconDirectory_ShouldBeMappedIntoElectronOutput() + { + var tempDir = CreateTempProjectDirectory(); + try + { + var iconPackageDir = Path.Combine(tempDir, "Assets", "MyApp.icon"); + Directory.CreateDirectory(Path.Combine(iconPackageDir, "layers")); + + await File.WriteAllTextAsync(Path.Combine(iconPackageDir, "manifest.json"), "{}"); + await File.WriteAllTextAsync(Path.Combine(iconPackageDir, "layers", "foreground.png"), "not-a-real-png"); + + await WriteMinimalCsprojAsync(tempDir); + + var (exitCode, output) = await RunDotnetMsBuildAsync(tempDir, "DumpElectronIconCopyItems"); + + exitCode.Should().Be(0, + $"MSBuild target evaluation must succeed. Full output:\n{output}"); + + output.Should().Contain( + @".electron\MyApp.icon\manifest.json", + $"the icon package root file must be mapped into .electron/MyApp.icon. Full output:\n{output}"); + + output.Should().Contain( + @".electron\MyApp.icon\layers\foreground.png", + $"nested files in .icon package must preserve structure in .electron output. Full output:\n{output}"); + } + finally + { + Directory.Delete(tempDir, recursive: true); + } + } + + private static string FindBuildFile(string relativeFromRepoRoot, string relativeFromSrc) + { + 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 '{relativeFromRepoRoot}' by walking up from '{AppContext.BaseDirectory}'."); + } + + private static string CreateTempProjectDirectory() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"electron-net-icon-test-{Guid.NewGuid():N}"); + Directory.CreateDirectory(tempDir); + Directory.CreateDirectory(Path.Combine(tempDir, "Properties")); + return tempDir; + } + + private static Task WriteMinimalCsprojAsync(string tempDir) + { + var propsPathEscaped = CorePropsPath.Replace("'", "'"); + var targetsPathEscaped = CoreTargetsPath.Replace("'", "'"); + + return File.WriteAllTextAsync( + Path.Combine(tempDir, "TestApp.csproj"), + $$""" + + + net10.0 + + + + + + Assets\MyApp.icon + + + + + + + + + """); + } + + private static async Task<(int ExitCode, string Output)> RunDotnetMsBuildAsync(string workingDirectory, string target) + { + var psi = new ProcessStartInfo("dotnet", $"msbuild TestApp.csproj --nologo -v:minimal /restore /t:{target}") + { + 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.LateImport.targets b/src/ElectronNET/build/ElectronNET.LateImport.targets index 10185e0..9019c09 100644 --- a/src/ElectronNET/build/ElectronNET.LateImport.targets +++ b/src/ElectronNET/build/ElectronNET.LateImport.targets @@ -313,10 +313,10 @@ <_ElectronFiles Include="$(ElectronSplashScreen)" Condition="'$(ElectronSplashScreen)'!=''" /> - <_ElectronIconDirectories Include="$(ElectronIcon)" - Condition="'$(ElectronIcon)'!='' AND Exists('$(ElectronIcon)') AND $([System.IO.Directory]::Exists('$(ElectronIcon)') )" /> <_ElectronFiles Include="$(ElectronIcon)" Condition="'$(ElectronIcon)'!='' AND ( !Exists('$(ElectronIcon)') OR !$([System.IO.Directory]::Exists('$(ElectronIcon)') ) )" /> + <_ElectronIconDirectoryFiles Include="$(ElectronIcon)\**\*" + Condition="'$(ElectronIcon)'!='' AND Exists('$(ElectronIcon)') AND $([System.IO.Directory]::Exists('$(ElectronIcon)') )" /> @@ -331,14 +331,6 @@ <_ElectronFilesToCopy Include="@(_ElectronFiles->'%(FullPath)')" /> - - <_ElectronIconDirectoryFiles Include="@(_ElectronIconDirectories->'%(FullPath)\**\*')" /> - <_ElectronIconDirectoryFiles Remove="@(_ElectronIconDirectoryFiles)" Condition="$([System.IO.Directory]::Exists('%(Identity)'))" /> - <_ElectronIconDirectoryFiles Update="@(_ElectronIconDirectoryFiles)"> - $([System.String]::Copy('%(Identity)').Replace('$([MSBuild]::EnsureTrailingSlash(%(_ElectronIconDirectories.FullPath)))', '')) - - - @@ -348,7 +340,7 @@ <_ElectronFilesToCopyWithTargetPath Include="@(_ElectronIconDirectoryFiles)"> - $(ElectronDirName)\$(ElectronIconFileName)\%(ElectronIconRelativePath) + $(ElectronDirName)\$(ElectronIconFileName)\%(RecursiveDir)%(FileName)%(Extension)