Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ jobs:
path: ui/dist
- run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev file patchelf xvfb xdotool
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev file patchelf xvfb xdotool gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
shared-key: canary-linux
Expand All @@ -298,8 +298,13 @@ jobs:
test "$(target/release/orx --no-telemetry version --build-channel)" = production
test "$(target/release/orx --no-telemetry version --build-id)" = "$ORX_DESKTOP_CANARY_TAG"
scripts/build-linux-appimage.sh target/release/orx app-dist
# shellcheck disable=SC2046 # pkg-config emits compiler arguments.
cc -Wall -Wextra scripts/appimage-media-probe.c -o app-dist/appimage-media-probe $(pkg-config --cflags --libs webkit2gtk-4.1)
scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe system
sudo apt-get remove -y libwebkit2gtk-4.1-0 libjavascriptcoregtk-4.1-0
scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage
scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe bundled
sudo apt-get remove -y gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe missing
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: canary-linux-${{ matrix.arch }}
Expand Down
33 changes: 27 additions & 6 deletions .github/workflows/release-linux-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ jobs:
persist-credentials: false
ref: ${{ needs.check.outputs.source-sha }}

- name: Take AppImage tools from the workflow revision
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.workflow_sha }}
path: packaging-workflow
sparse-checkout: |
linux
scripts

- name: Remove checked-in UI
if: needs.check.outputs.release-run-id != ''
run: rm -rf ui/dist
Expand All @@ -120,7 +130,7 @@ jobs:
- name: Install WebKitGTK and packaging tools
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev file patchelf xvfb xdotool
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev file patchelf xvfb xdotool gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav

# The runner's preinstalled stable toolchain, as release-macos-app.yml uses.
- name: Build the production orx
Expand All @@ -134,14 +144,25 @@ jobs:
run: test "$(target/release/orx version --build-channel)" = production

- name: Package the AppImage
run: scripts/build-linux-appimage.sh target/release/orx app-dist
run: |
packaging-workflow/scripts/build-linux-appimage.sh target/release/orx app-dist
# shellcheck disable=SC2046 # pkg-config emits compiler arguments.
cc -Wall -Wextra packaging-workflow/scripts/appimage-media-probe.c -o app-dist/appimage-media-probe $(pkg-config --cflags --libs webkit2gtk-4.1)

# Otherwise the host's WebKit would stand in for a broken bundled one.
- name: Remove WebKitGTK from the runner
- name: Smoke-test the system runtime
run: packaging-workflow/scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe system

- name: Remove the system WebKit
run: sudo apt-get remove -y libwebkit2gtk-4.1-0 libjavascriptcoregtk-4.1-0

- name: Smoke-test the AppImage
run: scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage
- name: Smoke-test the bundled fallback
run: packaging-workflow/scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe bundled

- name: Remove the system media plugins
run: sudo apt-get remove -y gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav

- name: Check the missing-dependency instructions
run: packaging-workflow/scripts/smoke-test-linux-appimage.sh app-dist/OpenResearch-${{ matrix.arch }}.AppImage app-dist/appimage-media-probe missing

- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
Expand Down
2 changes: 1 addition & 1 deletion arc/desktop.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Each platform's package wraps one `orx` binary that is the CLI an agent shells o
Anything but `app`, `app <openresearch: link>` or `app <file.orx>` is a CLI invocation. A file becomes a `file:` link, so both travel one path, including to a running app.

- **Windows** would flash a window for every console program a GUI process starts, so `windows/launcher/` starts `orx.exe app` in a hidden console shared by orx and everything it runs. **The launcher changes only with a new installer**, so `orx app [link]` is a contract with every installed launcher. `windows/OpenResearch.iss` installs per user, so `orx.exe` can replace itself without elevation.
- **Linux** bundles WebKitGTK in the AppImage, so its library floor is glibc's, set for every user by the Ubuntu runners that build it. AppRun records the session's values before running the GTK hooks once, points GTK at the bundle and starts orx inside the read-only mount; orx restores those values for the host programs it opens (`src/local/shell_env.rs`, in step with AppRun's list). The session chooses Wayland or X11 unless the user sets `GDK_BACKEND`; the bundling plugin's X11 default must not override that choice. Each start points a desktop entry at the AppImage's current path, which also claims `openresearch:` links and `.orx` files.
- **Linux** keeps a bundled WebKitGTK fallback at the Ubuntu builder's glibc floor. AppRun first checks the system WebKitGTK 4.1/GStreamer stack and verifies that it can load the desktop binary. A compatible system stack runs without the bundled GTK hooks; otherwise AppRun checks the bundled stack against the installed media plugins. If neither works, an installation dialog replaces the native media crash. GStreamer plugins and codecs stay on the host rather than increasing each AppImage's size. The package smoke tests cover system selection, bundled fallback and missing dependencies; media tests create Web Audio and decode a video frame. [Linux runtime dependencies](../linux/README.md) lists the installation commands. Stable release builds take AppImage tools from the workflow revision and the desktop binary from the release revision. AppRun records the session's values before changing library or GTK paths; orx restores those values for host programs (`src/local/shell_env.rs`, in step with AppRun's list). The session chooses Wayland or X11 unless the user sets `GDK_BACKEND`. Each start points a desktop entry at the AppImage's current path, which also claims `openresearch:` links and `.orx` files.

## App mode

Expand Down
76 changes: 59 additions & 17 deletions linux/AppRun
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}"
# The session's values of what this script and the hook below override, as
# `=<value>` or `-` for unset, so orx can hand them back to the host programs it
# opens (shell_env::host_gui_env; keep the two lists in step).
for var in GDK_BACKEND GDK_PIXBUF_MODULE_FILE GI_TYPELIB_PATH GIO_EXTRA_MODULES \
for var in LD_LIBRARY_PATH GDK_BACKEND GDK_PIXBUF_MODULE_FILE GI_TYPELIB_PATH GIO_EXTRA_MODULES \
GIO_MODULE_DIR GSETTINGS_SCHEMA_DIR GTK_DATA_PREFIX GTK_EXE_PREFIX GTK_IM_MODULE_FILE GTK_PATH \
GTK_THEME XDG_DATA_DIRS; do
if [ -n "${!var+set}" ]; then
Expand All @@ -19,26 +19,68 @@ for var in GDK_BACKEND GDK_PIXBUF_MODULE_FILE GI_TYPELIB_PATH GIO_EXTRA_MODULES
fi
done

# linuxdeploy-plugin-gtk's hook points GTK at the bundled modules and schemas.
for hook in "$APPDIR"/apprun-hooks/*.sh; do
if [ -e "$hook" ]; then
# shellcheck disable=SC1090
. "$hook"
cd "$APPDIR/usr"
runtime_log="$(mktemp)"
trap 'rm -f "$runtime_log"' EXIT
runtime="$APPDIR/usr/bin/orx-runtime"
check_runtime() {
(ulimit -c 0; timeout 15 "$runtime" check)
}
system_lib=
if system_lib="$(check_runtime 2>"$runtime_log")"; then
case "$system_lib/" in
"$APPDIR/"*) system_lib= ;;
*)
if ! LD_LIBRARY_PATH="$system_lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" check_runtime >>"$runtime_log" 2>&1 || \
! LD_LIBRARY_PATH="$system_lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \
LD_BIND_NOW=1 ORX_TELEMETRY_ENV=off timeout 10 "$APPDIR/usr/bin/orx" version >/dev/null 2>>"$runtime_log"; then
system_lib=
fi
;;
esac
fi
if [ -n "$system_lib" ]; then
export LD_LIBRARY_PATH="$system_lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export ORX_APPIMAGE_RUNTIME=system
else
# linuxdeploy-plugin-gtk's hook points GTK at the bundled modules and schemas.
for hook in "$APPDIR"/apprun-hooks/*.sh; do
if [ -e "$hook" ]; then
# shellcheck disable=SC1090
. "$hook"
fi
done
# Let GTK choose the session's backend unless the user selected one; the plugin
# forces X11 even in a Wayland session.
case "$ORX_HOST_GDK_BACKEND" in
=*) export GDK_BACKEND="${ORX_HOST_GDK_BACKEND#=}" ;;
-) unset GDK_BACKEND ;;
esac
# Only the bundled GIO modules (TLS): the host's need a newer GLib than the image's.
export GIO_MODULE_DIR="$APPDIR/usr/lib/gio/modules"
unset GIO_EXTRA_MODULES
export ORX_APPIMAGE_RUNTIME=bundled
if ! LD_LIBRARY_PATH="$APPDIR/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" check_runtime >>"$runtime_log" 2>&1 || \
! LD_BIND_NOW=1 ORX_TELEMETRY_ENV=off timeout 10 "$APPDIR/usr/bin/orx" version >/dev/null 2>>"$runtime_log"; then
cat "$runtime_log" >&2
rm -f "$runtime_log"
trap - EXIT
exec env LD_LIBRARY_PATH="$APPDIR/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$runtime" error "OpenResearch could not find compatible WebKitGTK / GStreamer media libraries.

Install the system libraries, then reopen OpenResearch:

Ubuntu / Debian:
sudo apt install libwebkit2gtk-4.1-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav

Arch Linux:
sudo pacman -S --needed webkit2gtk-4.1 gst-plugins-base-libs gst-plugins-good gst-libav"
fi
done
# Let GTK choose the session's backend unless the user selected one; the plugin
# forces X11 even in a Wayland session.
case "$ORX_HOST_GDK_BACKEND" in
=*) export GDK_BACKEND="${ORX_HOST_GDK_BACKEND#=}" ;;
-) unset GDK_BACKEND ;;
esac
# Only the bundled GIO modules (TLS): the host's need a newer GLib than the image's.
export GIO_MODULE_DIR="$APPDIR/usr/lib/gio/modules"
unset GIO_EXTRA_MODULES
fi
rm -f "$runtime_log"
trap - EXIT

# scripts/build-linux-appimage.sh rewrote libwebkit2gtk's /usr paths to ././,
# so WebKit finds its bundled helper processes relative to this directory.
cd "$APPDIR/usr"
link=()
# Any position: AppImage integrators keep the bundled entry's `app` before %u.
shopt -s nocasematch
Expand Down
23 changes: 23 additions & 0 deletions linux/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Linux desktop runtime

OpenResearch prefers the system's WebKitGTK 4.1 and GStreamer libraries so HTML previews use media plugins built for the same runtime. Installing new system plugins does not make them compatible with the older GStreamer inside an AppImage.

Install the runtime and common video codecs before opening the AppImage:

Ubuntu / Debian:

```sh
sudo apt install libwebkit2gtk-4.1-0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
```

Arch Linux:

```sh
sudo pacman -S --needed webkit2gtk-4.1 gst-plugins-base-libs gst-plugins-good gst-libav
```

AppRun discovers the library directory, checks the required media elements and verifies the desktop binary's imports before selecting the system stack. It leaves the session's GTK settings in place. No global `LD_LIBRARY_PATH` setting or manual launch command is needed, and host programs launched by OpenResearch receive the session's original library path.

If the system stack is unavailable, the existing bundled WebKit can still run with compatible system media plugins, as on Ubuntu 22.04. If neither stack has the required media elements, a dialog gives installation commands before the dashboard starts. A video format can still require additional codecs; the libav package above supplies the H.264/AAC support exercised by the package test.

The fallback keeps its previous bundle size: media plugins and their codec dependencies are not added to the AppImage. Both stable and Canary builds test system selection, the bundled fallback after removing system WebKit, and the dependency dialog after removing media plugins.
102 changes: 102 additions & 0 deletions linux/appimage-runtime.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void *required_symbol(void *library, const char *name)
{
void *symbol = dlsym(library, name);
if (!symbol) fprintf(stderr, "Missing runtime function %s: %s\n", name, dlerror());
return symbol;
}

static int check_runtime(void)
{
void *webkit = dlopen("libwebkit2gtk-4.1.so.0", RTLD_NOW | RTLD_LOCAL);
if (!webkit) {
fprintf(stderr, "Cannot load WebKitGTK 4.1: %s\n", dlerror());
return 1;
}
void *gst = dlopen("libgstreamer-1.0.so.0", RTLD_NOW | RTLD_LOCAL);
if (!gst) {
fprintf(stderr, "Cannot load GStreamer: %s\n", dlerror());
return 1;
}
int (*init)(int *, char ***, void **) = required_symbol(gst, "gst_init_check");
void *(*make)(const char *, const char *) = required_symbol(gst, "gst_element_factory_make");
void (*unref)(void *) = required_symbol(gst, "g_object_unref");
if (!init || !make || !unref) return 1;
if (!init(NULL, NULL, NULL)) {
fputs("Cannot initialize GStreamer\n", stderr);
return 1;
}
const char *elements[] = {
"appsrc", "appsink", "playbin", "decodebin", "autoaudiosink",
"audioconvert", "audioresample", "volume", "scaletempo", "qtdemux", "videoconvert"
};
for (size_t i = 0; i < sizeof(elements) / sizeof(elements[0]); i++) {
void *element = make(elements[i], NULL);
if (!element) {
fprintf(stderr, "Missing or incompatible GStreamer element: %s\n", elements[i]);
return 1;
}
unref(element);
}
struct link_map *map = NULL;
if (dlinfo(webkit, RTLD_DI_LINKMAP, &map) != 0 || !map) {
fprintf(stderr, "Cannot locate WebKitGTK: %s\n", dlerror());
return 1;
}
char *path = realpath(map->l_name, NULL);
if (!path) {
perror("Cannot resolve WebKitGTK path");
return 1;
}
char *slash = strrchr(path, '/');
if (!slash) {
free(path);
return 1;
}
*slash = '\0';
puts(path);
free(path);
return 0;
}

static int show_error(const char *text)
{
fputs(text, stderr);
fputc('\n', stderr);
void *gtk = dlopen("libgtk-3.so.0", RTLD_NOW | RTLD_LOCAL);
if (!gtk) {
fprintf(stderr, "Cannot display dependency instructions: %s\n", dlerror());
return 1;
}
int (*init)(int *, char ***) = required_symbol(gtk, "gtk_init_check");
void *(*dialog)(void *, int, int, int, const char *, ...) = required_symbol(gtk, "gtk_message_dialog_new");
void (*title)(void *, const char *) = required_symbol(gtk, "gtk_window_set_title");
int (*run)(void *) = required_symbol(gtk, "gtk_dialog_run");
void (*destroy)(void *) = required_symbol(gtk, "gtk_widget_destroy");
if (!init || !dialog || !title || !run || !destroy) return 1;
if (!init(NULL, NULL)) {
fputs("Cannot display dependency instructions: no GTK display\n", stderr);
return 1;
}
// GTK 3 ABI values; this helper must start even when GTK is not installed.
enum { DIALOG_MODAL = 1, MESSAGE_ERROR = 3, BUTTONS_CLOSE = 2 };
void *window = dialog(NULL, DIALOG_MODAL, MESSAGE_ERROR, BUTTONS_CLOSE, "%s", text);
title(window, "OpenResearch needs multimedia libraries");
run(window);
destroy(window);
return 1;
}

int main(int argc, char **argv)
{
if (argc == 2 && strcmp(argv[1], "check") == 0) return check_runtime();
if (argc == 3 && strcmp(argv[1], "error") == 0) return show_error(argv[2]);
fputs("Usage: orx-runtime check | error <message>\n", stderr);
return 1;
}
61 changes: 61 additions & 0 deletions scripts/appimage-media-probe.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include <webkit2/webkit2.h>

static int result = 1;

static void received(WebKitUserContentManager *manager, WebKitJavascriptResult *message, gpointer data)
{
(void)manager;
(void)data;
JSCValue *value = webkit_javascript_result_get_js_value(message);
JSCValue *pass = jsc_value_is_object(value) ? jsc_value_object_get_property(value, "pass") : NULL;
char *text = jsc_value_to_json(value, 0);
g_print("%s\n", text ? text : "Invalid media probe response");
result = text && pass && jsc_value_is_boolean(pass) && jsc_value_to_boolean(pass) ? 0 : 1;
g_clear_object(&pass);
g_free(text);
gtk_main_quit();
}

static void terminated(WebKitWebView *view, WebKitWebProcessTerminationReason reason, gpointer data)
{
(void)view;
(void)data;
g_printerr("WebKit web process terminated: %d\n", reason);
result = 2;
gtk_main_quit();
}

static gboolean timed_out(gpointer data)
{
(void)data;
g_printerr("WebKit media preview timed out\n");
gtk_main_quit();
return G_SOURCE_REMOVE;
}

int main(int argc, char **argv)
{
if (argc != 2) return 1;
char *html = NULL;
GError *error = NULL;
if (!g_file_get_contents(argv[1], &html, NULL, &error)) {
g_printerr("%s\n", error->message);
g_error_free(error);
return 1;
}
gtk_init(&argc, &argv);
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
GtkWidget *view = webkit_web_view_new();
WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(view));
webkit_user_content_manager_register_script_message_handler(manager, "mediaProbe");
g_signal_connect(manager, "script-message-received::mediaProbe", G_CALLBACK(received), NULL);
g_signal_connect(view, "web-process-terminated", G_CALLBACK(terminated), NULL);
gtk_container_add(GTK_CONTAINER(window), view);
gtk_widget_show_all(window);
webkit_web_view_load_html(WEBKIT_WEB_VIEW(view), html, "http://127.0.0.1/");
g_free(html);
g_timeout_add_seconds(25, timed_out, NULL);
gtk_main();
gtk_widget_destroy(window);
return result;
}
3 changes: 3 additions & 0 deletions scripts/build-linux-appimage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export APPIMAGE_EXTRACT_AND_RUN=1

echo "==> Staging the AppDir"
install -Dm755 "$ORX" "$APPDIR/usr/bin/orx"
cc -Wall -Wextra -Werror "$ROOT/linux/appimage-runtime.c" -ldl -o "$APPDIR/usr/bin/orx-runtime"
# WebKit starts these helpers from a path compiled into libwebkit2gtk, which the
# sed below makes relative to AppRun's working directory, $APPDIR/usr.
for helper in WebKitNetworkProcess WebKitWebProcess injected-bundle/libwebkit2gtkinjectedbundle.so; do
Expand All @@ -85,6 +86,8 @@ DEPLOY_GTK_VERSION=3 linuxdeploy \
# host environment before running those hooks itself, exactly once.
install -m755 "$ROOT/linux/AppRun" "$APPDIR/AppRun"
rm -f "$APPDIR/AppRun.wrapped"
# The runtime checker must discover the host before considering the bundled GTK.
patchelf --remove-rpath "$APPDIR/usr/bin/orx-runtime"

# linuxdeploy leaves files below usr/lib looking beside themselves, so they
# would load the host's libraries (or none); point them at the bundled usr/lib.
Expand Down
Loading
Loading