Initial macOS WebView support

This is a gigantic kerfuffle because Chromium expects a very specific directory & app bundle layout. Have to change a bunch of resource loading code to account for content development being launched from an app bundle, and also had to make automatic MSBuild tooling & a python script to generate such an app bundle
This commit is contained in:
PJB3005
2025-11-09 00:06:16 +01:00
parent feb9e1db69
commit 602d7833a1
18 changed files with 336 additions and 32 deletions

View File

@@ -0,0 +1,24 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!--
Depend on this in your client project (e.g. Content.Client) to generate a development app bundle for macOS.
This is required for WebView.
-->
<PropertyGroup>
<RTMakeAppBundle Condition="'$(TargetOS)' == 'MacOS' And '$(RTMakeAppBundle)' == '' And '$(FullRelease)' != 'True'">True</RTMakeAppBundle>
<RTAppBundleName Condition="'$(RTAppBundleName)' == ''">RobustToolbox Project</RTAppBundleName>
<RTAppBundleIdentifier Condition="'$(RTAppBundleIdentifier)' == ''">org.robusttoolbox.project</RTAppBundleIdentifier>
<!-- RTAppBundleIcon controls icon -->
</PropertyGroup>
<PropertyGroup>
<_RTMacOSAppBundle_targets_imported>True</_RTMacOSAppBundle_targets_imported>
</PropertyGroup>
<Target Name="RTMakeAppBundleAfterBuild" Condition="'$(RTMakeAppBundle)' == 'True'" AfterTargets="AfterBuild">
<PropertyGroup>
<_RTMacOSAppBundle_icon Condition="'$(RTAppBundleIcon)' != ''">--icon &quot;$(RTAppBundleIcon)&quot;</_RTMacOSAppBundle_icon>
</PropertyGroup>
<Exec Command="$(MSBuildThisFileDirectory)/../Tools/macos_make_appbundle.py $(_RTMacOSAppBundle_for_webview) --name &quot;$(RTAppBundleName)&quot; --directory &quot;$(OutputPath)&quot; --apphost &quot;$(AssemblyName)&quot; --identifier &quot;$(RTAppBundleIdentifier)&quot; $(_RTMacOSAppBundle_icon)" />
</Target>
</Project>

14
MSBuild/WebView.targets Normal file
View File

@@ -0,0 +1,14 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- If you are using Robust.Client.WebView, import this to depend on it. -->
<Import Condition="'$(_RTMacOSAppBundle_targets_imported)' != 'True'"
Project="$(MSBuildThisFileDirectory)\MacOSAppBundle.targets" />
<PropertyGroup>
<_RTMacOSAppBundle_for_webview>--webview</_RTMacOSAppBundle_for_webview>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="$(MSBuildThisFileDirectory)\..\Robust.Client.WebView\Robust.Client.WebView.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,9 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using Robust.Shared.ContentPack;
using Xilium.CefGlue;
namespace Robust.Client.WebView.Cef
@@ -20,6 +22,20 @@ namespace Robust.Client.WebView.Cef
argv[0] = "-";
}
#if MACOS
NativeLibrary.SetDllImportResolver(typeof(CefSettings).Assembly,
(name, assembly, path) =>
{
if (name == "libcef")
{
var libPath = PathHelpers.ExecutableRelativeFile("../../../../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework");
return NativeLibrary.Load(libPath, assembly, path);
}
return 0;
});
#endif
var mainArgs = new CefMainArgs(argv);
StartWatchThread();

View File

@@ -17,11 +17,13 @@ namespace Robust.Client.WebView.Cef
{
internal partial class WebViewManagerCef
{
private readonly List<ControlImpl> _activeControls = new();
public IWebViewControlImpl MakeControlImpl(WebViewControl owner)
{
var shader = _prototypeManager.Index<ShaderPrototype>("bgra");
var shaderInstance = shader.Instance();
var impl = new ControlImpl(owner, shaderInstance);
var impl = new ControlImpl(this, owner, shaderInstance);
_dependencyCollection.InjectDependencies(impl);
return impl;
}
@@ -133,11 +135,13 @@ namespace Robust.Client.WebView.Cef
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IInputManager _inputMgr = default!;
private readonly WebViewManagerCef _manager;
public readonly WebViewControl Owner;
private readonly ShaderInstance _shaderInstance;
public ControlImpl(WebViewControl owner, ShaderInstance shaderInstance)
public ControlImpl(WebViewManagerCef manager, WebViewControl owner, ShaderInstance shaderInstance)
{
_manager = manager;
Owner = owner;
_shaderInstance = shaderInstance;
}
@@ -194,6 +198,7 @@ namespace Robust.Client.WebView.Cef
var texture = _clyde.CreateBlankTexture<Rgba32>(Vector2i.One);
_data = new LiveData(texture, client, browser, renderer);
_manager._activeControls.Add(this);
}
public void CloseBrowser()
@@ -203,6 +208,8 @@ namespace Robust.Client.WebView.Cef
_data!.Texture.Dispose();
_data.Browser.GetHost().CloseBrowser(true);
_data = null;
_manager._activeControls.Remove(this);
}
public void MouseMove(GUIMouseMoveEventArgs args)
@@ -279,6 +286,7 @@ namespace Robust.Client.WebView.Cef
// Logger.Debug($"{guiRawEvent.Action} {guiRawEvent.Key} {guiRawEvent.ScanCode} {vkKey}");
#if !MACOS
var lParam = 0;
lParam |= (guiRawEvent.ScanCode & 0xFF) << 16;
if (guiRawEvent.Action != RawKeyAction.Down)
@@ -286,7 +294,9 @@ namespace Robust.Client.WebView.Cef
if (guiRawEvent.Action == RawKeyAction.Up)
lParam |= 1 << 31;
#else
var lParam = guiRawEvent.RawCode;
#endif
var modifiers = CalcModifiers(guiRawEvent.Key);
host.SendKeyEvent(new CefKeyEvent
@@ -307,7 +317,7 @@ namespace Robust.Client.WebView.Cef
host.SendKeyEvent(new CefKeyEvent
{
EventType = CefKeyEventType.Char,
WindowsKeyCode = '\r',
WindowsKeyCode = '\b',
NativeKeyCode = lParam,
Modifiers = modifiers
});

View File

@@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using Robust.Client.Console;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
@@ -47,11 +48,12 @@ namespace Robust.Client.WebView.Cef
string subProcessName;
if (OperatingSystem.IsWindows())
subProcessName = "Robust.Client.WebView.exe";
else if (OperatingSystem.IsLinux())
else if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
subProcessName = "Robust.Client.WebView";
else
throw new NotSupportedException("Unsupported platform for CEF!");
#if !MACOS
var subProcessPath = Path.Combine(BasePath, subProcessName);
var cefResourcesPath = LocateCefResources();
_sawmill.Debug($"Subprocess path: {subProcessPath}, resources: {cefResourcesPath}");
@@ -60,19 +62,36 @@ namespace Robust.Client.WebView.Cef
if (cefResourcesPath == null)
throw new InvalidOperationException("Unable to locate cef_resources directory!");
#endif
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
var cachePath = FindAndLockCacheDirectory();
#if MACOS
NativeLibrary.SetDllImportResolver(typeof(CefSettings).Assembly,
(name, assembly, path) =>
{
if (name == "libcef")
{
var libPath = PathHelpers.ExecutableRelativeFile("../Frameworks/Chromium Embedded Framework.framework/Chromium Embedded Framework");
return NativeLibrary.Load(libPath, assembly, path);
}
return 0;
});
#endif
var settings = new CefSettings()
{
WindowlessRenderingEnabled = true, // So we can render to our UI controls.
ExternalMessagePump = false, // Unsure, honestly. TODO CEF: Research this?
NoSandbox = true, // Not disabling the sandbox crashes CEF.
#if !MACOS
BrowserSubprocessPath = subProcessPath,
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
ResourcesDirPath = cefResourcesPath,
#endif
RemoteDebuggingPort = remoteDebugPort,
CookieableSchemesList = "usr,res",
CachePath = cachePath,
@@ -113,7 +132,6 @@ namespace Robust.Client.WebView.Cef
if (ProbeDir(BasePath, out var path))
return path;
foreach (var searchDir in NativeDllSearchDirectories())
{
if (ProbeDir(searchDir, out path))
@@ -147,6 +165,16 @@ namespace Robust.Client.WebView.Cef
public void Shutdown()
{
foreach (var control in _activeControls.ToArray())
{
control.CloseBrowser();
}
foreach (var window in _browserWindows.ToArray())
{
window.Dispose();
}
CefRuntime.Shutdown();
}

View File

@@ -8,10 +8,10 @@ internal sealed class TestBrowseWindow : DefaultWindow
{
protected override Vector2 ContentsMinimumSize => new Vector2(640, 480);
public TestBrowseWindow()
public TestBrowseWindow(string url)
{
var wv = new WebViewControl();
wv.Url = "https://spacestation14.io";
wv.Url = url;
Contents.AddChild(wv);
}
@@ -23,6 +23,15 @@ internal sealed class TestBrowseWindowCommand : LocalizedCommands
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
new TestBrowseWindow().Open();
var url = args.Length > 0 ? args[0] : "https://spacestation14.com";
new TestBrowseWindow(url).Open();
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
return CompletionResult.FromHint("<url>");
return CompletionResult.Empty;
}
}

View File

@@ -105,6 +105,7 @@ namespace Robust.Client
private IMainArgs? _loaderArgs;
public bool ContentStart { get; set; } = false;
public StartType StartTypeValue { get; private set; }
public GameControllerOptions Options { get; private set; } = new();
public InitialLaunchState LaunchState { get; private set; } = default!;
@@ -398,9 +399,18 @@ namespace Robust.Client
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions)
: Options.MountOptions;
StartTypeValue = ContentStart ? StartType.Content : StartType.Engine;
#if FULL_RELEASE
if (_loaderArgs != null || Options.ResourceMountDisabled)
StartTypeValue = StartType.Loader;
#else
if (StartTypeValue == StartType.Content && Path.GetFileName(PathHelpers.GetExecutableDirectory()) == "MacOS")
StartTypeValue = StartType.ContentAppBundle;
#endif
ProgramShared.DoMounts(_resManager, mountOptions, Options.ContentBuildDirectory,
Options.AssemblyDirectory,
Options.LoadContentResources, _loaderArgs != null && !Options.ResourceMountDisabled, ContentStart);
Options.LoadContentResources, StartTypeValue);
if (_loaderArgs != null)
{

View File

@@ -163,7 +163,7 @@ internal partial class Clyde
var button = ConvertSdl3Button(ev.Button);
var key = Mouse.MouseButtonToKey(button);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0);
EmitKeyEvent(key, ev.Type, false, ev.Mods, 0, 0);
}
private void ProcessEventMouseMotion(EventMouseMotion ev)
@@ -227,10 +227,10 @@ internal partial class Clyde
private void ProcessEventKey(EventKey ev)
{
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode);
EmitKeyEvent(ConvertSdl3Scancode(ev.Scancode), ev.Type, ev.Repeat, ev.Mods, ev.Scancode, ev.Raw);
}
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode)
private void EmitKeyEvent(Key key, ET type, bool repeat, SDL.SDL_Keymod mods, SDL.SDL_Scancode scancode, ushort rawCode)
{
var shift = (mods & SDL_Keymod.SDL_KMOD_SHIFT) != 0;
var alt = (mods & SDL_Keymod.SDL_KMOD_ALT) != 0;
@@ -241,7 +241,8 @@ internal partial class Clyde
key,
repeat,
alt, control, shift, system,
(int)scancode);
(int)scancode,
rawCode);
switch (type)
{

View File

@@ -140,6 +140,7 @@ internal partial class Clyde
{
WindowId = ev.windowID,
Scancode = ev.scancode,
Raw = ev.raw,
Type = ev.type,
Repeat = ev.repeat,
Mods = ev.mod,
@@ -194,6 +195,7 @@ internal partial class Clyde
{
public uint WindowId;
public SDL.SDL_Scancode Scancode;
public ushort Raw;
public ET Type;
public bool Repeat;
public SDL.SDL_Keymod Mods;

View File

@@ -1,5 +1,6 @@
using System;
using Robust.Client.Input;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Timing;
@@ -9,6 +10,7 @@ namespace Robust.Client
{
GameControllerOptions Options { get; }
bool ContentStart { get; set; }
StartType StartTypeValue { get; }
void SetCommandLineArgs(CommandLineArgs args);
void Run(GameController.DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null);
void KeyDown(KeyEventArgs keyEvent);

View File

@@ -106,17 +106,20 @@ namespace Robust.Client.Input
public bool IsRepeat { get; }
public int ScanCode { get; }
internal ushort RawCode { get; }
public KeyEventArgs(
Keyboard.Key key,
bool repeat,
bool alt, bool control, bool shift, bool system,
int scanCode)
int scanCode,
ushort rawCode=0)
: base(alt, control, shift, system)
{
Key = key;
IsRepeat = repeat;
ScanCode = scanCode;
RawCode = rawCode;
}
}

View File

@@ -299,7 +299,8 @@ namespace Robust.Client.Input
args.Key,
args.ScanCode,
action,
(Vector2i) (mousePos ?? Vector2.Zero));
(Vector2i) (mousePos ?? Vector2.Zero),
args.RawCode);
var block = rawInput.RawKeyEvent(keyEvent);
return block;

View File

@@ -35,13 +35,15 @@ namespace Robust.Client.UserInterface
public readonly int ScanCode;
public readonly RawKeyAction Action;
public readonly Vector2i MouseRelative;
public readonly ushort RawCode;
public GuiRawKeyEvent(Keyboard.Key key, int scanCode, RawKeyAction action, Vector2i mouseRelative)
public GuiRawKeyEvent(Keyboard.Key key, int scanCode, RawKeyAction action, Vector2i mouseRelative, ushort rawCode)
{
Key = key;
ScanCode = scanCode;
Action = action;
MouseRelative = mouseRelative;
RawCode = rawCode;
}
}

View File

@@ -302,8 +302,14 @@ namespace Robust.Server
var mountOptions = _commandLineArgs != null
? MountOptions.Merge(_commandLineArgs.MountOptions, Options.MountOptions) : Options.MountOptions;
var startType = ContentStart ? StartType.Content : StartType.Engine;
#if FULL_RELEASE
if (Options.ResourceMountDisabled)
startType = StartType.Loader;
#endif
ProgramShared.DoMounts(_resources, mountOptions, Options.ContentBuildDirectory, Options.AssemblyDirectory,
Options.LoadContentResources, Options.ResourceMountDisabled, ContentStart);
Options.LoadContentResources, startType);
// When the game is ran with the startup executable being content,
// we have to disable the separate load context.

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using Robust.Shared.Utility;
@@ -18,8 +19,18 @@ namespace Robust.Shared.ContentPack
{
// TODO: remove this shitty hack, either through making it less hardcoded into shared,
// or by making our file structure less spaghetti somehow.
var assembly = typeof(PathHelpers).Assembly;
var location = assembly.Location;
string location;
if (Process.GetCurrentProcess().MainModule is { } mod)
{
location = mod.FileName;
}
else
{
// Fallback in case the above doesn't work ig?
var assembly = typeof(PathHelpers).Assembly;
location = assembly.Location;
}
if (location == string.Empty)
{
// See https://docs.microsoft.com/en-us/dotnet/api/system.reflection.assembly.location?view=net-5.0#remarks

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Robust.Shared.Configuration;
@@ -28,14 +29,30 @@ internal static class ProgramShared
}
#if !FULL_RELEASE
private static string FindContentRootDir(bool contentStart)
private static string FindContentRootDir(StartType startType)
{
return PathOffset + (contentStart ? "../../" : "../../../");
var relative = startType switch
{
StartType.Engine => "../../../",
StartType.Content => "../../",
StartType.Loader => throw new InvalidOperationException(),
StartType.ContentAppBundle => "../../../../../",
_ => throw new ArgumentOutOfRangeException(nameof(startType), startType, null)
};
return PathOffset + relative;
}
private static string FindEngineRootDir(bool contentStart)
private static string FindEngineRootDir(StartType startType)
{
return PathOffset + (contentStart ? "../../RobustToolbox/" : "../../");
var relative = startType switch
{
StartType.Engine => "../../",
StartType.Content => "../../RobustToolbox/",
StartType.Loader => throw new InvalidOperationException(),
StartType.ContentAppBundle => "../../../../../RobustToolbox/",
_ => throw new ArgumentOutOfRangeException(nameof(startType), startType, null)
};
return PathOffset + relative;
}
#endif
@@ -47,19 +64,35 @@ internal static class ProgramShared
}
}
internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, ResPath assembliesPath, bool loadContentResources = true,
bool loader = false, bool contentStart = false)
internal static void DoMounts(
IResourceManagerInternal res,
MountOptions? options,
string contentBuildDir,
ResPath assembliesPath,
bool loadContentResources = true,
StartType startType = StartType.Engine)
{
#if FULL_RELEASE
if (!loader)
res.MountContentDirectory(@"Resources/");
if (startType != StartType.Loader)
res.MountContentDirectory(@"Resources/");
#else
res.MountContentDirectory($@"{FindEngineRootDir(contentStart)}Resources/");
var engineRoot = FindEngineRootDir(startType);
// System.Console.WriteLine($"ENGINE DIR IS {engineRoot}");
res.MountContentDirectory($@"{engineRoot}Resources/");
if (loadContentResources)
{
var contentRootDir = FindContentRootDir(contentStart);
res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", assembliesPath);
var contentRootDir = FindContentRootDir(startType);
// System.Console.WriteLine($"CONTENT DIR IS {Path.GetFullPath(contentRootDir)}");
if (startType == StartType.ContentAppBundle)
{
res.MountContentDirectory("./", assembliesPath);
}
else
{
res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", assembliesPath);
}
res.MountContentDirectory($@"{contentRootDir}Resources/");
}
#endif
@@ -103,3 +136,27 @@ internal static class ProgramShared
task.Wait();
}
}
internal enum StartType
{
/// <summary>
/// We've been started from <c>RobustToolbox/bin/Client/Robust.Client</c>
/// </summary>
Engine,
/// <summary>
/// We've been started from e.g. <c>bin/Content.Client/Content.Client</c>
/// </summary>
Content,
/// <summary>
/// We've been started from the launcher loader.
/// </summary>
Loader,
/// <summary>
/// (macOS only)
/// We've been started from e.g. <c>bin/Content.Client/Space Station 14.app/Contents/MacOS/Content.Client</c>
/// </summary>
ContentAppBundle
}

View File

@@ -1,6 +1,7 @@
using System;
using Robust.Client;
using Robust.Client.Input;
using Robust.Shared;
using Robust.Shared.Log;
using Robust.Shared.Timing;
@@ -11,6 +12,7 @@ namespace Robust.UnitTesting
public InitialLaunchState LaunchState { get; } = new(false, null, null, null);
public GameControllerOptions Options { get; } = new();
public bool ContentStart { get; set; }
public StartType StartTypeValue => ContentStart ? StartType.Content : StartType.Engine;
public event Action<FrameEventArgs>? TickUpdateOverride { add { } remove { } }

106
Tools/macos_make_appbundle.py Executable file
View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
import argparse
import os
import re
import shutil
import plistlib
p = os.path.join
symlinkable_re = re.compile(r"(?:runtimes|.+\.(?:dll|pdb|json))$", re.IGNORECASE)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--webview", action="store_true")
parser.add_argument("--name", required=True)
parser.add_argument("--directory", required=True)
parser.add_argument("--apphost", required=True)
parser.add_argument("--identifier", required=True)
parser.add_argument("--icon")
args = parser.parse_args()
dir: str = args.directory
name: str = args.name
# Create base app directory structure.
os.makedirs(p(dir, f"{name}.app", "Contents", "MacOS"), exist_ok=True)
os.makedirs(p(dir, f"{name}.app", "Contents", "Resources"), exist_ok=True)
os.makedirs(p(dir, f"{name}.app", "Contents", "Frameworks"), exist_ok=True)
# Copy apphost
dest_apphost = p(dir, f"{name}.app", "Contents", "MacOS", name)
shutil.copy(p(dir, args.apphost), dest_apphost)
# Symlink most files in the bin dir.
symlink_files(args.directory, p(dir, f"{name}.app", "Contents", "MacOS"), "")
# Copy icon
if args.icon:
shutil.copy(args.icon, p(dir, f"{name}.app", "Contents", "Resources", "icon.icns"))
# Write plist
plist_dat = {
"CFBundleName": name,
"CFBundleDisplayName": name,
"CFBundleIdentifier": args.identifier,
"CFBundleIconFile": "icon",
"CFBundleExecutable": name,
"LSApplicationCategoryType": "public.app-category.games"
}
with open(p(dir, f"{name}.app", "Contents", "Info.plist"), "wb") as f:
plistlib.dump(plist_dat, f)
if args.webview:
chromium_framework_path = p(dir, f"{name}.app", "Contents", "Frameworks", "Chromium Embedded Framework.framework")
if not os.path.exists(chromium_framework_path):
os.symlink("../../../Chromium Embedded Framework.framework", chromium_framework_path)
create_webview_helper(dir, name, args.identifier, None, None)
create_webview_helper(dir, name, args.identifier, "GPU", "gpu")
create_webview_helper(dir, name, args.identifier, "Renderer", "renderer")
create_webview_helper(dir, name, args.identifier, "Alerts", "alerts")
def create_webview_helper(dir: str, name: str, identifier: str, suffix: str | None, identifier_suffix: str | None):
helper_name = f"{name} helper"
if suffix is not None:
helper_name += f" ({suffix})"
sub_app_path = p(dir, f"{name}.app", "Contents", "Frameworks", f"{helper_name}.app")
os.makedirs(p(sub_app_path, "Contents", "MacOS"), exist_ok=True)
os.makedirs(p(sub_app_path, "Contents", "Resources"), exist_ok=True)
# Copy apphost for Robust.Client.WebView
shutil.copy(p(dir, "Robust.Client.WebView"), p(sub_app_path, "Contents", "MacOS", helper_name))
# Symlink files
symlink_files(dir, p(sub_app_path, "Contents", "MacOS"), "../../../")
helper_identifier = f"{identifier}.cef.{identifier_suffix}"
if identifier_suffix is not None:
helper_identifier += "." + identifier_suffix
plist_dat = {
"CFBundleName": f"{name} helper",
"CFBundleDisplayName": f"{name} helper",
"CFBundleIdentifier": f"{identifier}.cef.{identifier_suffix}",
"CFBundleExecutable": helper_name
}
with open(p(sub_app_path, "Contents", "Info.plist"), "wb") as f:
plistlib.dump(plist_dat, f)
def symlink_files(src_dir: str, dest_dir: str, relative: str):
for file in os.listdir(src_dir):
if not symlinkable_re.match(file):
continue
dest_symlink = p(dest_dir, file)
if not os.path.islink(dest_symlink):
os.symlink(f"../../../{relative}{file}", dest_symlink)
main()