///|
fn BundlePlan::platform_files(self : BundlePlan) -> Array[BundleFile] {
match self.target {
MacOS =>
[
BundleFile::new(
path="\{self.bundle_name}/Contents/Info.plist",
content=self.info_plist(),
),
BundleFile::new(
path="\{self.bundle_name}/Contents/MacOS/\{self.executable_name}",
content=self.macos_launcher(),
executable=true,
),
]
Windows =>
[
BundleFile::new(
path="\{self.executable_name}/app.manifest.xml",
content=self.windows_manifest(),
),
BundleFile::new(
path="\{self.executable_name}/\{self.executable_name}-launcher.c",
content=self.windows_native_launcher_source(),
),
BundleFile::new(
path="\{self.executable_name}/\{self.executable_name}.cmd",
content=self.windows_launcher(),
),
]
Linux =>
[
BundleFile::new(
path="\{self.executable_name}/\{self.executable_name}.desktop",
content=self.desktop_entry(),
),
BundleFile::new(
path="\{self.executable_name}/\{self.executable_name}",
content=self.linux_launcher(),
executable=true,
),
]
}
}
///|
fn BundlePlan::info_plist(self : BundlePlan) -> String {
let identifier = self.identifier().xml_escape()
let product = self.metadata.product_name().xml_escape()
let version = self.metadata.version().xml_escape()
let executable = self.executable_name.xml_escape()
[
"",
"",
"",
"CFBundleIdentifier\{identifier}",
"CFBundleName\{product}",
"CFBundleDisplayName\{product}",
"CFBundleExecutable\{executable}",
"CFBundleShortVersionString\{version}",
"CFBundleVersion\{version}",
if self.icon_path is Some(_) {
"CFBundleIconFileAppIcon"
} else {
""
},
"\n",
].join("")
}
///|
fn BundlePlan::windows_manifest(self : BundlePlan) -> String {
let identifier = self.identifier().xml_escape()
let product = self.metadata.product_name().xml_escape()
[
"",
"",
"",
"\{product}",
"\n",
].join("")
}
///|
fn BundlePlan::desktop_entry(self : BundlePlan) -> String {
[
"[Desktop Entry]",
"Type=Application",
"Name=\{self.metadata.product_name()}",
"Exec=\{self.executable_name}",
"Icon=\{self.executable_name}",
"Categories=Utility;",
"",
].join("\n")
}
///|
fn BundlePlan::macos_launcher(self : BundlePlan) -> String {
let identifier = self.identifier().shell_double_quote_escape()
(
#|#!/bin/sh
#|APP_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
#|RUNTIME="$APP_DIR/MacOS/lepusa-runtime"
#|if [ ! -x "$RUNTIME" ]; then
#| RUNTIME=lepusa-runtime
#|fi
#|export LEPUSA_APP_DIR="$APP_DIR"
#|export LEPUSA_RESOURCE_DIR="$APP_DIR/Resources"
#|export LEPUSA_APP_DATA_DIR="${HOME}/Library/Application Support/__LEPUSA_IDENTIFIER__"
#|export PATH="$APP_DIR/MacOS:$PATH"
#|mkdir -p "$LEPUSA_APP_DATA_DIR" || exit 1
#|cd "$LEPUSA_RESOURCE_DIR" || exit 1
#|cleanup() {
#| trap - INT TERM HUP
#| if [ -n "${RUNTIME_PID:-}" ]; then
#| kill -TERM "$RUNTIME_PID" 2>/dev/null || true
#| fi
#| exit 130
#|}
#|trap cleanup INT TERM HUP
#|"$RUNTIME" launch --manifest "$APP_DIR/Resources/lepusa/runtime.json" "$@" &
#|RUNTIME_PID=$!
#|wait "$RUNTIME_PID"
#|STATUS=$?
#|trap - INT TERM HUP
#|exit "$STATUS"
#|
).replace_all(old="__LEPUSA_IDENTIFIER__", new=identifier)
}
///|
fn BundlePlan::windows_launcher(self : BundlePlan) -> String {
let identifier = self.identifier().replace_all(old="%", new="%%")
(
#|@echo off
#|set "APP_DIR=%~dp0"
#|set "RUNTIME=%APP_DIR%lepusa-runtime.exe"
#|if not exist "%RUNTIME%" set "RUNTIME=lepusa-runtime.exe"
#|set "LEPUSA_APP_DIR=%APP_DIR%"
#|set "LEPUSA_RESOURCE_DIR=%APP_DIR%"
#|set "LEPUSA_APP_DATA_DIR=%LOCALAPPDATA%\__LEPUSA_IDENTIFIER__"
#|set "PATH=%APP_DIR%;%PATH%"
#|if not exist "%LEPUSA_APP_DATA_DIR%" mkdir "%LEPUSA_APP_DATA_DIR%"
#|cd /d "%LEPUSA_RESOURCE_DIR%"
#|"%RUNTIME%" launch --manifest "%APP_DIR%lepusa\runtime.json" %*
#|
).replace_all(old="__LEPUSA_IDENTIFIER__", new=identifier)
}
///|
fn BundlePlan::windows_native_launcher_source(self : BundlePlan) -> String {
let product = self.metadata.product_name().c_string_escape()
(
#|#define UNICODE
#|#define _UNICODE
#|#include
#|#include
#|#include
#|#include
#|
#|static wchar_t *lepusa_join_path(const wchar_t *left, const wchar_t *right) {
#| size_t left_len = wcslen(left);
#| size_t right_len = wcslen(right);
#| int needs_slash = left_len > 0 && left[left_len - 1] != L'\\';
#| wchar_t *out = (wchar_t *)calloc(left_len + right_len + (needs_slash ? 2 : 1), sizeof(wchar_t));
#| if (out == NULL) {
#| return NULL;
#| }
#| wcscpy(out, left);
#| if (needs_slash) {
#| wcscat(out, L"\\");
#| }
#| wcscat(out, right);
#| return out;
#|}
#|
#|static wchar_t *lepusa_app_dir(void) {
#| DWORD cap = MAX_PATH;
#| for (;;) {
#| wchar_t *buffer = (wchar_t *)calloc(cap, sizeof(wchar_t));
#| if (buffer == NULL) {
#| return NULL;
#| }
#| DWORD len = GetModuleFileNameW(NULL, buffer, cap);
#| if (len == 0) {
#| free(buffer);
#| return NULL;
#| }
#| if (len < cap - 1) {
#| wchar_t *slash = wcsrchr(buffer, L'\\');
#| if (slash != NULL) {
#| *slash = L'\0';
#| }
#| return buffer;
#| }
#| free(buffer);
#| cap *= 2;
#| }
#|}
#|
#|static int lepusa_file_exists(const wchar_t *path) {
#| DWORD attributes = GetFileAttributesW(path);
#| return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY);
#|}
#|
#|int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, PWSTR command_line, int show_command) {
#| (void)instance;
#| (void)previous;
#| (void)show_command;
#| wchar_t *app_dir = lepusa_app_dir();
#| if (app_dir == NULL) {
#| MessageBoxW(NULL, L"Could not resolve application directory.", L"__LEPUSA_PRODUCT__", MB_ICONERROR);
#| return 1;
#| }
#| wchar_t *runtime = lepusa_join_path(app_dir, L"lepusa-runtime.exe");
#| wchar_t *manifest = lepusa_join_path(app_dir, L"lepusa\\runtime.json");
#| if (runtime == NULL || manifest == NULL) {
#| free(app_dir);
#| free(runtime);
#| free(manifest);
#| return 1;
#| }
#| const wchar_t *runtime_exe = lepusa_file_exists(runtime) ? runtime : L"lepusa-runtime.exe";
#| size_t command_len = wcslen(runtime_exe) + wcslen(manifest) + wcslen(command_line == NULL ? L"" : command_line) + 64;
#| wchar_t *command = (wchar_t *)calloc(command_len, sizeof(wchar_t));
#| if (command == NULL) {
#| free(app_dir);
#| free(runtime);
#| free(manifest);
#| return 1;
#| }
#| swprintf(command, command_len, L"\"%ls\" launch --manifest \"%ls\" %ls", runtime_exe, manifest, command_line == NULL ? L"" : command_line);
#| STARTUPINFOW startup;
#| PROCESS_INFORMATION process;
#| ZeroMemory(&startup, sizeof(startup));
#| ZeroMemory(&process, sizeof(process));
#| startup.cb = sizeof(startup);
#| BOOL created = CreateProcessW(NULL, command, NULL, NULL, FALSE, 0, NULL, app_dir, &startup, &process);
#| if (!created) {
#| MessageBoxW(NULL, L"Could not start lepusa-runtime.exe.", L"__LEPUSA_PRODUCT__", MB_ICONERROR);
#| free(app_dir);
#| free(runtime);
#| free(manifest);
#| free(command);
#| return 1;
#| }
#| WaitForSingleObject(process.hProcess, INFINITE);
#| DWORD exit_code = 1;
#| GetExitCodeProcess(process.hProcess, &exit_code);
#| CloseHandle(process.hThread);
#| CloseHandle(process.hProcess);
#| free(app_dir);
#| free(runtime);
#| free(manifest);
#| free(command);
#| return (int)exit_code;
#|}
#|
).replace_all(old="__LEPUSA_PRODUCT__", new=product)
}
///|
fn BundlePlan::linux_launcher(self : BundlePlan) -> String {
let identifier = self.identifier().shell_double_quote_escape()
(
#|#!/bin/sh
#|APP_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
#|RUNTIME="$APP_DIR/lepusa-runtime"
#|if [ ! -x "$RUNTIME" ]; then
#| RUNTIME=lepusa-runtime
#|fi
#|export LEPUSA_APP_DIR="$APP_DIR"
#|export LEPUSA_RESOURCE_DIR="$APP_DIR"
#|export LEPUSA_APP_DATA_DIR="${XDG_DATA_HOME:-${HOME}/.local/share}/__LEPUSA_IDENTIFIER__"
#|export PATH="$APP_DIR:$PATH"
#|mkdir -p "$LEPUSA_APP_DATA_DIR" || exit 1
#|cd "$LEPUSA_RESOURCE_DIR" || exit 1
#|cleanup() {
#| trap - INT TERM HUP
#| if [ -n "${RUNTIME_PID:-}" ]; then
#| kill -TERM "$RUNTIME_PID" 2>/dev/null || true
#| fi
#| exit 130
#|}
#|trap cleanup INT TERM HUP
#|"$RUNTIME" launch --manifest "$APP_DIR/lepusa/runtime.json" "$@" &
#|RUNTIME_PID=$!
#|wait "$RUNTIME_PID"
#|STATUS=$?
#|trap - INT TERM HUP
#|exit "$STATUS"
#|
).replace_all(old="__LEPUSA_IDENTIFIER__", new=identifier)
}
///|
fn String::xml_escape(self : String) -> String {
self
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
.replace_all(old="'", new="'")
}
///|
fn String::c_string_escape(self : String) -> String {
self
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="\n", new="\\n")
.replace_all(old="\r", new="\\r")
}
///|
fn String::shell_double_quote_escape(self : String) -> String {
self
.replace_all(old="\\", new="\\\\")
.replace_all(old="\"", new="\\\"")
.replace_all(old="$", new="\\$")
.replace_all(old="`", new="\\`")
}