From 7cd552a78f664a96f350d36eb91ae85235665b13 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Fri, 4 Sep 2026 14:05:36 +0200 Subject: [PATCH] Finished #1106 --- Changelog.md | 2 + docs/Using/Configuration.md | 44 ++++++++++++++++++ docs/Using/Package-Building.md | 9 ++++ src/ElectronNET.API/Common/ProcessRunner.cs | 19 +++++++- src/ElectronNET.API/Runtime/Data/BuildInfo.cs | 2 + .../Helpers/ElectronRootDirResolver.cs | 31 ++++++++++++ .../Runtime/Helpers/UnpackagedDetector.cs | 9 ++++ .../ElectronProcess/ElectronProcessActive.cs | 3 +- src/ElectronNET.API/Runtime/StartupManager.cs | 1 + src/ElectronNET/build/ElectronNET.Build.dll | Bin 8192 -> 8192 bytes src/ElectronNET/build/ElectronNET.Core.props | 9 +++- .../build/ElectronNET.Core.targets | 1 + .../build/ElectronNET.LateImport.targets | 28 +++++------ .../build/ElectronNETRules.Project.xaml | 18 +++++++ 14 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 src/ElectronNET.API/Runtime/Helpers/ElectronRootDirResolver.cs diff --git a/Changelog.md b/Changelog.md index 3df8cdc..14c4706 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,8 @@ - Fixed cross-compilation behavior on same platform (#1098) @epsnm - Fixed resolution of unrelated target (#1099) @epsnm - Added target framework customization (#1095) @epsnm +- Added configurable Electron root directory for custom packaging layouts (#1106) @DYH1319 +- Added build extensibility properties `ElectronSkipExecCommands` and `ElectronIntermediatePublishDir` (#1106) @DYH1319 # 0.5.2 diff --git a/docs/Using/Configuration.md b/docs/Using/Configuration.md index dd16fc5..e37bf5c 100644 --- a/docs/Using/Configuration.md +++ b/docs/Using/Configuration.md @@ -28,9 +28,53 @@ These are the current default values when you don't make any changes: $(MSBuildProjectName.Replace(".", "-").ToLower()) electron-builder.json $(MSBuildProjectName) + ../.. + false ``` +### Custom Packaging Layout + +`ElectronRootDir` tells the .NET app where to find the Electron binary when it is started first (DotNet-First startup of a packaged app). The path is resolved relative to the directory of the .NET executable; absolute paths are used as-is. + +The default `../..` matches the standard Electron-First packaging layout produced by `dotnet publish`, where the .NET app is placed in `resources/bin` of the Electron package: + +``` +/ + MyApp # Electron executable + resources/ + bin/ + MyApp # .NET executable +``` + +If your own packaging pipeline places the .NET executable at the package root and Electron in a subdirectory, set `ElectronRootDir` accordingly: + +```xml +electron +``` + +``` +/ + MyApp # .NET executable + electron/ + MyApp # Electron executable + resources/ + locales/ +``` + +The property is also used to detect whether the app runs packaged or unpackaged, so it must point to the directory that actually contains the Electron binary. + +### Build Extensibility + +These properties are useful when Electron.NET is embedded into a custom build or CI pipeline: + +| Property | Default | Description | +| --- | --- | --- | +| `ElectronSkipExecCommands` | `false` | Skips all `npm`/`npx` invocations of the Electron build and publish targets. Use it when the dependencies and the Electron distribution are provided by the pipeline itself (for example from a cache or an offline mirror). | +| `ElectronIntermediatePublishDir` | `$(IntermediateOutputPath)PubTmp\` | Overrides the intermediate directory the .NET app is published to before it is assembled into the Electron package. | + +`ElectronPackageId` and `Title` only fall back to the project name when they are not already set, so both can be defined by a `Directory.Build.props` or passed on the command line. + ### Relation to package.json ElectronNET.Core does not work with an `electron-manifest.json` file anymore. diff --git a/docs/Using/Package-Building.md b/docs/Using/Package-Building.md index 6b6325e..d6f7473 100644 --- a/docs/Using/Package-Building.md +++ b/docs/Using/Package-Building.md @@ -222,6 +222,15 @@ The publish process will: > macOS builds can't be created on Windows machines because they require symlinks that aren't supported on Windows (per [this Electron issue](https://github.com/electron-userland/electron-packager/issues/71)). macOS builds can be produced on either Linux or macOS machines. +## Custom Packaging Pipelines + +The default layout is Electron-First: electron-builder produces the Electron package and the .NET app is placed in `resources/bin`. If you assemble the package yourself and put the .NET executable at the package root with Electron in a subdirectory, set `ElectronRootDir` so the .NET app can locate the Electron binary at runtime. + +For pipelines that provide the npm dependencies themselves, `ElectronSkipExecCommands` suppresses all `npm`/`npx` invocations, and `ElectronIntermediatePublishDir` redirects the intermediate publish output. + +See [Configuration](Configuration.md#custom-packaging-layout) for details. + + ## 🚀 Next Steps - **[Startup Methods](Startup-Methods.md)** - Understanding different launch modes for packaged apps diff --git a/src/ElectronNET.API/Common/ProcessRunner.cs b/src/ElectronNET.API/Common/ProcessRunner.cs index d561020..2d44e92 100644 --- a/src/ElectronNET.API/Common/ProcessRunner.cs +++ b/src/ElectronNET.API/Common/ProcessRunner.cs @@ -37,10 +37,18 @@ public ProcessRunner(string name) { this.Name = name; + this.ExitWaitTimeout = DefaultExitWaitTimeout; } public event EventHandler ProcessExited; + /// Gets or sets the value is initialized with. + public static int DefaultExitWaitTimeout { get; set; } = 5000; + + /// Gets or sets the time in milliseconds to wait for the process and its I/O to finish + /// after the exited event has been raised. Values of zero or less wait indefinitely. + public int ExitWaitTimeout { get; set; } + public bool IsDisposed { get; private set; } private Process Process @@ -498,8 +506,17 @@ { try { + var timeout = this.ExitWaitTimeout; + // This shouldn't throw here, but the mono process implementation doesn't always behave as it should. - this.process.WaitForExit(); + if (timeout > 0) + { + this.process.WaitForExit(timeout); + } + else + { + this.process.WaitForExit(); + } } catch (Exception ex) { diff --git a/src/ElectronNET.API/Runtime/Data/BuildInfo.cs b/src/ElectronNET.API/Runtime/Data/BuildInfo.cs index 75febe0..faefd12 100644 --- a/src/ElectronNET.API/Runtime/Data/BuildInfo.cs +++ b/src/ElectronNET.API/Runtime/Data/BuildInfo.cs @@ -6,6 +6,8 @@ public string ElectronVersion { get; internal set; } + public string ElectronRootDir { get; internal set; } + public string RuntimeIdentifier { get; internal set; } public bool ElectronSingleInstance { get; internal set; } diff --git a/src/ElectronNET.API/Runtime/Helpers/ElectronRootDirResolver.cs b/src/ElectronNET.API/Runtime/Helpers/ElectronRootDirResolver.cs new file mode 100644 index 0000000..6d17a61 --- /dev/null +++ b/src/ElectronNET.API/Runtime/Helpers/ElectronRootDirResolver.cs @@ -0,0 +1,31 @@ +namespace ElectronNET.Runtime.Helpers +{ + using System; + using System.IO; + + /// + /// Resolves the directory that contains the Electron binary of a packaged application. + /// + internal static class ElectronRootDirResolver + { + /// The location of the Electron binary relative to the .NET application directory in the + /// default (Electron-first) layout, where the .NET application is placed in 'resources/bin'. + internal const string DefaultElectronRootDir = "../.."; + + /// Resolves the Electron root directory relative to the given .NET application directory. + /// The directory containing the .NET application. + /// The directory containing the Electron binary. + public static DirectoryInfo Resolve(DirectoryInfo baseDirectory) + { + var rootDir = ElectronNetRuntime.BuildInfo?.ElectronRootDir; + + if (string.IsNullOrWhiteSpace(rootDir)) + { + rootDir = DefaultElectronRootDir; + } + + // An absolute ElectronRootDir is used as-is by Path.Combine. + return new DirectoryInfo(Path.GetFullPath(Path.Combine(baseDirectory.FullName, rootDir))); + } + } +} diff --git a/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs b/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs index f5c5f54..ed814a2 100644 --- a/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs +++ b/src/ElectronNET.API/Runtime/Helpers/UnpackagedDetector.cs @@ -69,6 +69,15 @@ return false; } + // Packaged, if the Electron binary is found where ElectronRootDir points to. + var electronExecutable = ElectronNetRuntime.ElectronExecutable; + + if (!string.IsNullOrEmpty(electronExecutable) && + File.Exists(Path.Combine(ElectronRootDirResolver.Resolve(dir).FullName, electronExecutable))) + { + return false; + } + if (dir.GetDirectories().Any(e => e.Name == ".electron")) { return true; diff --git a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs index 51c409e..55e502d 100644 --- a/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs +++ b/src/ElectronNET.API/Runtime/Services/ElectronProcess/ElectronProcessActive.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using ElectronNET.Common; using ElectronNET.Runtime.Data; + using ElectronNET.Runtime.Helpers; /// /// Launches and manages the Electron app process. @@ -87,7 +88,7 @@ } else { - dir = dir.Parent!.Parent!; + dir = ElectronRootDirResolver.Resolve(dir); startCmd = Path.Combine(dir.FullName, this.electronBinaryName); args = $"-dotnetpacked -electronforcedport={this.socketPort:D} " + this.extraArguments; workingDir = dir.FullName; diff --git a/src/ElectronNET.API/Runtime/StartupManager.cs b/src/ElectronNET.API/Runtime/StartupManager.cs index 7817894..c945ef1 100644 --- a/src/ElectronNET.API/Runtime/StartupManager.cs +++ b/src/ElectronNET.API/Runtime/StartupManager.cs @@ -178,6 +178,7 @@ { buildInfo.ElectronExecutable = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronExecutable))?.Value; buildInfo.ElectronVersion = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronVersion))?.Value; + buildInfo.ElectronRootDir = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.ElectronRootDir))?.Value; buildInfo.RuntimeIdentifier = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.RuntimeIdentifier))?.Value; buildInfo.Title = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Title))?.Value; buildInfo.Version = attributes.FirstOrDefault(e => e.Key == nameof(buildInfo.Version))?.Value; diff --git a/src/ElectronNET/build/ElectronNET.Build.dll b/src/ElectronNET/build/ElectronNET.Build.dll index c850ff39c5c2e15f819f51f90fb83cb8b80b3306..0455b5618c3ac18fb4a70f56e28da7fa94054bf6 100644 GIT binary patch delta 508 zcmZp0XmFU&!IJY&_rt^<8AhRrE5jN0PiADSXH=Nn$XL(#c{3vuBNwlPIRgV514{z~ z0|NuwW`3Tl90FIjUf1=?jQ5z|%pAh^b-woIFCvmm=4N^Z3=GDM40;B7W_kwN1|~)+ z7O7^*<_6}eNd}f?rfG@BX{IU3i3X|0mZ^qDhN)==hL%amlU>9ccyCPI^`C*!H-L$O zfuUvcMR9r78DaP0DcWt-q`n2(}8*kye z=VqWFf?&qU=Om;VKTm!op~d!Jl|g|)V6uYbyNL!Illi2oIN2bI7}z#Xl*E5jKNPG)4RXVjS7$XL(#bu%LqBNwlNIRgV514{z~ z0|Nu&W`3Tl90DzQvvOS&Py4k@H&d5f9hA#sCnY97DJ#Y$HK{Z` zCNH(5xFj(zC9x>QNY4OMsa}CiN)kh`hoh0H83WWMBIlqkF{yc}UAb~j{MILnHY&=m zDSBxJ3M2?-HfLa9P?)GF#`tye3kfYj28REt3t8F=MzPgC2tcgEoUPLn=cOg9U>rg8_p%gC&?{#E`;Z!jQ&b$&k!o z4i-;kuw*b|0EwqDq%jyWq%xQ@Br+H>STYzeSTH0pq%v4ASTYzg7*1X;YshFg`KGKs PlZ5$ZLAm#g6Ajn_b9a)@ diff --git a/src/ElectronNET/build/ElectronNET.Core.props b/src/ElectronNET/build/ElectronNET.Core.props index 2291e3d..4b4eed4 100644 --- a/src/ElectronNET/build/ElectronNET.Core.props +++ b/src/ElectronNET/build/ElectronNET.Core.props @@ -11,9 +11,14 @@ - $(MSBuildProjectName.Replace(".", "-").ToLower()) + $(MSBuildProjectName.Replace(".", "-").ToLower()) electron-builder.json - $(MSBuildProjectName) + $(MSBuildProjectName) + + ../.. + + false diff --git a/src/ElectronNET/build/ElectronNET.Core.targets b/src/ElectronNET/build/ElectronNET.Core.targets index 962ed6f..753b130 100644 --- a/src/ElectronNET/build/ElectronNET.Core.targets +++ b/src/ElectronNET/build/ElectronNET.Core.targets @@ -19,6 +19,7 @@ + diff --git a/src/ElectronNET/build/ElectronNET.LateImport.targets b/src/ElectronNET/build/ElectronNET.LateImport.targets index 5e9d965..5866c7b 100644 --- a/src/ElectronNET/build/ElectronNET.LateImport.targets +++ b/src/ElectronNET/build/ElectronNET.LateImport.targets @@ -170,7 +170,9 @@ <_NonIntermediatePublishDir>$(PublishDir) $(PublishDir) - $(IntermediateOutputPath)PubTmp\ + + $(ElectronIntermediatePublishDir) + $(IntermediateOutputPath)PubTmp\ @@ -459,13 +461,13 @@ - + - + - + <_NpmCmd>node node_modules/electron/install.js @@ -474,13 +476,13 @@ <_NpmCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmCmd)' - + - + - + @@ -615,11 +617,11 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr <_NpmAppDepsCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpmAppDepsCmd)' - + - + @@ -673,11 +675,11 @@ For more information, see: https://github.com/ElectronNET/Electron.NET/wiki/Migr <_NpxCmd Condition="'$(IsLinuxWsl)' == 'true'">wsl bash -ic '$(_NpxCmd)' - + - + diff --git a/src/ElectronNET/build/ElectronNETRules.Project.xaml b/src/ElectronNET/build/ElectronNETRules.Project.xaml index 0331ae8..4782916 100644 --- a/src/ElectronNET/build/ElectronNETRules.Project.xaml +++ b/src/ElectronNET/build/ElectronNETRules.Project.xaml @@ -553,6 +553,24 @@ Category="General"> + + + + + + + + +