diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml new file mode 100644 index 0000000..5a1af9b --- /dev/null +++ b/.github/workflows/appimage.yml @@ -0,0 +1,83 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + release_tag: + description: "Release tag (example: 1.0.0)" + required: false + type: string + + prerelease: + description: "Mark as prerelease" + required: false + default: "false" + type: choice + options: + - "false" + - "true" + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + + container: + image: archlinux:latest + + steps: + - name: Install base dependencies + run: | + pacman -Syu --noconfirm archlinux-keyring + pacman -Syu --noconfirm pkg-config \ + base-devel \ + sudo \ + wget \ + patchelf + + - name: Create build user + run: | + useradd -m builder + echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Fix permissions + run: chown -R builder:builder . + + - name: Build + run: | + sudo -u builder sh <<'EOF' + set -e + cd src/protontricks/data/appimage + makepkg -p PKGBUILD-git -fs --noconfirm + EOF + ls -lh + cd src/protontricks/data/appimage + pacman --noconfirm -U ./*.pkg.tar.zst + chmod +x ./make-appimage.sh + ./make-appimage.sh + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: AppImage + path: "src/protontricks/data/appimage/dist/*.AppImage" + archive: false + + - name: Create GitHub Release + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.release_tag != '') + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ github.event.inputs.release_tag || github.ref_name }} + name: ${{ github.event.inputs.release_tag || github.ref_name }} + files: "src/protontricks/data/appimage/dist/*.AppImage" + generate_release_notes: true + draft: false + prerelease: ${{ github.event.inputs.prerelease == 'true' }} diff --git a/.github/workflows/appstream.yaml b/.github/workflows/appstream.yml similarity index 90% rename from .github/workflows/appstream.yaml rename to .github/workflows/appstream.yml index fb26a35..30d4690 100644 --- a/.github/workflows/appstream.yaml +++ b/.github/workflows/appstream.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Install appstreamcli run: sudo apt install appstream diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bae4ee0..6fc4e16 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,9 +15,9 @@ jobs: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.gitignore b/.gitignore index 0c1eaed..defaa4c 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,14 @@ pip-selfcheck.json # End of https://www.gitignore.io/api/python,virtualenv + +# AppImage +AppDir/ + +*.tar.zst + +quick-sharun* +get-debloated-pkgs* +src/protontricks/data/appimage/protontricks/ +src/protontricks/data/appimage/protontricks/src/ +src/protontricks/data/appimage/pkg/ diff --git a/src/protontricks/data/appimage/PKGBUILD-git b/src/protontricks/data/appimage/PKGBUILD-git new file mode 100644 index 0000000..471d13e --- /dev/null +++ b/src/protontricks/data/appimage/PKGBUILD-git @@ -0,0 +1,62 @@ +# Kudos to maintainers from official arch repos +# Carl Smedstad +# Jason Stryker +# Konstantin Liberty + +pkgname=protontricks +pkgver=1.14.1 +pkgrel=1 +pkgdesc="Run Winetricks commands for Steam Play/Proton games among other common Wine features" +arch=('any') +url="https://github.com/twig6943/protontricks" +license=('GPL-3.0-only') +depends=( + 'python' + 'python-pillow' + 'python-vdf' + 'winetricks' + 'openjpeg2' + 'libimagequant' + 'zenity' + 'yad' +) +makedepends=( + 'git' + 'python-build' + 'python-installer' + 'python-setuptools' + 'python-setuptools-scm' + 'python-wheel' +) +checkdepends=('python-pytest') + +source=() +b2sums=() + +prepare() { + git clone --depth 1 --branch appimage "$url.git" "$pkgname" +} + +build() { + cd $pkgname + python -m build --wheel --no-isolation +} + +check() { + cd $pkgname + python -m venv --system-site-packages test-env + test-env/bin/python -m installer dist/*.whl + test-env/bin/python -m pytest +} + +package() { + cd $pkgname + install -vDm644 src/protontricks/data/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg "$pkgdir/usr/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg" + python -m installer --destdir="$pkgdir" dist/*.whl + install -vDm644 -t "$pkgdir/usr/share/doc/$pkgname" ./*.md + + # We already install the desktop entries, not needed + rm -v "$pkgdir/usr/bin/protontricks-desktop-install" + local site_packages=$(python -c "import site; print(site.getsitepackages()[0])") + rm -vr "$pkgdir/$site_packages/protontricks/data/share" +} diff --git a/src/protontricks/data/appimage/execve-sharun-hack.c b/src/protontricks/data/appimage/execve-sharun-hack.c new file mode 100644 index 0000000..369b674 --- /dev/null +++ b/src/protontricks/data/appimage/execve-sharun-hack.c @@ -0,0 +1,715 @@ +/* + * execve-sharun-hack.c — LD_PRELOAD library that intercepts execve()/posix_spawn() + * calls to dynamic 64-bit ELF binaries under directories listed in + * ANYLINUX_EXECVE_WRAP_PATHS and redirects them through: + * + * execve("$APPDIR/sharun", + * ["sharun", "env", + * "$APPDIR/lib/ld-linux-.so.?", + * "--library-path", "$APPDIR/lib:$APPDIR/lib/sub:...", + * "--preload", "/path/to/this/lib.so", + * "/path/to/target", ], + * envp) + * + * ANYLINUX_EXECVE_WRAP_PATHS — colon-separated list of directories; wrapping + * only occurs when the resolved target starts with one of these directories. + * If unset or empty, no wrapping occurs. + * + * Supported: glibc only, x86_64 and aarch64. + * + * Build: + * cc -shared -fPIC -O2 -Wall -Wextra -o execve-sharun-hack.so execve-sharun-hack.c -ldl + * + * Debug: + * export EXECVE_SHARUN_DEBUG=1 + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +typedef int (*execve_func_t)(const char *, char *const [], char *const []); +typedef int (*execvpe_func_t)(const char *, char *const [], char *const []); + +typedef int (*posix_spawn_func_t)(pid_t *, const char *, + const posix_spawn_file_actions_t *, const posix_spawnattr_t *, + char *const [], char *const []); + +typedef int (*posix_spawnp_func_t)(pid_t *, const char *, + const posix_spawn_file_actions_t *, const posix_spawnattr_t *, + char *const [], char *const []); + +typedef ssize_t (*readlink_func_t)(const char *, char *, size_t); +typedef ssize_t (*readlinkat_func_t)(int, const char *, char *, size_t); + +#define VISIBLE __attribute__((visibility("default"))) + +/* ───────────────────────── debug ───────────────────────── */ + +static int debug_enabled(void) { + static int cached = -1; + if (cached == -1) { + const char *v = getenv("EXECVE_SHARUN_DEBUG"); + cached = (v && v[0] == '1' && v[1] == '\0'); + } + return cached; +} + +#define DEBUG(...) do { \ + if (debug_enabled()) \ + fprintf(stderr, " [execve_sharun.so] >> " __VA_ARGS__); \ +} while (0) + +/* ───────────────────── self path ──────────────────────── */ + +static const char *get_self_path(void) { + static char self_path[PATH_MAX]; + static int resolved; + + if (resolved) + return self_path[0] ? self_path : NULL; + resolved = 1; + + Dl_info info; + if (dladdr((void *)get_self_path, &info) && info.dli_fname) { + if (realpath(info.dli_fname, self_path)) + return self_path; + } + + self_path[0] = '\0'; + return NULL; +} + +/* Constructor ping: proves the library loaded */ +__attribute__((constructor)) +static void preload_ctor(void) { + if (debug_enabled()) { + const char *self = get_self_path(); + fprintf(stderr, " [execve_sharun.so] >> LOADED pid=%d self=%s\n", + getpid(), self ? self : "(null)"); + const char *lp = getenv("LD_PRELOAD"); + fprintf(stderr, " [execve_sharun.so] >> LD_PRELOAD=%s\n", lp ? lp : "(null)"); + } +} + +/* ───────────────────── ELF helpers ─────────────────────── */ + +static int is_dynamic_elf64(const char *path) { + int fd = open(path, O_RDONLY); + if (fd < 0) return 0; + + unsigned char e_ident[EI_NIDENT]; + if (read(fd, e_ident, EI_NIDENT) != EI_NIDENT) { + close(fd); + return 0; + } + + if (e_ident[EI_MAG0] != ELFMAG0 || e_ident[EI_MAG1] != ELFMAG1 || + e_ident[EI_MAG2] != ELFMAG2 || e_ident[EI_MAG3] != ELFMAG3) { + close(fd); + return 0; + } + + if (e_ident[EI_CLASS] != ELFCLASS64) { + close(fd); + return 0; + } + + Elf64_Ehdr ehdr; + if (lseek(fd, 0, SEEK_SET) != 0 || + read(fd, &ehdr, sizeof(ehdr)) != (ssize_t)sizeof(ehdr)) { + close(fd); + return 0; + } + + int found = 0; + for (int i = 0; i < ehdr.e_phnum && !found; i++) { + Elf64_Phdr phdr; + off_t off = ehdr.e_phoff + (off_t)i * ehdr.e_phentsize; + if (lseek(fd, off, SEEK_SET) == off && + read(fd, &phdr, sizeof(phdr)) == (ssize_t)sizeof(phdr)) { + if (phdr.p_type == PT_INTERP) + found = 1; + } + } + + close(fd); + return found; +} + +/* ───────────────────── path helpers ────────────────────── */ + +static int path_starts_with(const char *path, const char *prefix) { + size_t plen = strlen(prefix); + if (strncmp(path, prefix, plen) != 0) return 0; + return (path[plen] == '/' || path[plen] == '\0'); +} + +static int find_sharun(const char *appdir, char *out, size_t outsz) { + snprintf(out, outsz, "%s/sharun", appdir); + return (access(out, X_OK) == 0); +} + +/* glibc only: x86_64/aarch64 */ +static int find_ld_linux(const char *appdir, char *out, size_t outsz) { + struct utsname uts; + if (uname(&uts) != 0) + return 0; + + const char *ld_name = NULL; + if (strcmp(uts.machine, "x86_64") == 0) { + ld_name = "ld-linux-x86-64.so.2"; + } else if (strcmp(uts.machine, "aarch64") == 0) { + ld_name = "ld-linux-aarch64.so.1"; + } else { + return 0; + } + + snprintf(out, outsz, "%s/lib/%s", appdir, ld_name); + return (access(out, R_OK) == 0); +} + +/* Resolve to canonical path; returns malloc'd path or NULL */ +static char *resolve_binary_path(const char *filename) { + if (!filename || !*filename) return NULL; + + if (strchr(filename, '/')) + return realpath(filename, NULL); + + const char *pathenv = getenv("PATH"); + if (!pathenv) return NULL; + + char *pathcopy = strdup(pathenv); + if (!pathcopy) return NULL; + + char *saveptr = NULL; + for (char *dir = strtok_r(pathcopy, ":", &saveptr); + dir; + dir = strtok_r(NULL, ":", &saveptr)) + { + char candidate[PATH_MAX]; + snprintf(candidate, sizeof(candidate), "%s/%s", dir, filename); + if (access(candidate, X_OK) == 0) { + char *rp = realpath(candidate, NULL); + free(pathcopy); + return rp; + } + } + + free(pathcopy); + return NULL; +} + +/* ───────────────────── redirect logic ───────────────────── */ + +/* + * Returns 1 if target_resolved should be wrapped. + * Reads ANYLINUX_EXECVE_WRAP_PATHS (colon-separated directory list) and + * wraps only when the resolved target starts with one of those directories. + * Returns 0 if the env var is unset/empty or the target does not match. + */ +static int should_redirect_target(const char *target_resolved) { + const char *wrap_paths = getenv("ANYLINUX_EXECVE_WRAP_PATHS"); + if (!wrap_paths || !*wrap_paths) return 0; + + char *paths_copy = strdup(wrap_paths); + if (!paths_copy) return 0; + + int match = 0; + char *saveptr = NULL; + for (char *dir = strtok_r(paths_copy, ":", &saveptr); + dir && !match; + dir = strtok_r(NULL, ":", &saveptr)) + { + if (!*dir) continue; + char canon[PATH_MAX]; + if (!realpath(dir, canon)) + continue; + if (path_starts_with(target_resolved, canon)) + match = 1; + } + + free(paths_copy); + if (!match) return 0; + if (!is_dynamic_elf64(target_resolved)) return 0; + return 1; +} + +/* + * Append immediate subdirectories of `base` (one level deep, real dirs only — + * symlinks and hidden entries are skipped) to a dynamic string buffer. + * Each found directory is appended as ":". + * Returns 1 on success, 0 on allocation failure. + */ +static int append_immediate_subdirs(const char *base, char **buf, size_t *len, + size_t *cap) { + DIR *d = opendir(base); + if (!d) return 1; /* inaccessible — not a hard error */ + + struct dirent *entry; + while ((entry = readdir(d)) != NULL) { + /* Skip ".", "..", and any other hidden entry */ + if (entry->d_name[0] == '.') continue; + + char child[PATH_MAX]; + int rc = snprintf(child, sizeof(child), "%s/%s", base, entry->d_name); + if (rc < 0 || (size_t)rc >= sizeof(child)) continue; /* path too long */ + + struct stat st; + if (lstat(child, &st) != 0 || !S_ISDIR(st.st_mode)) continue; + + /* Grow buffer as needed: colon + path + NUL */ + size_t child_len = (size_t)rc; + size_t needed = *len + 1 + child_len + 1; + if (needed > *cap) { + size_t new_cap = *cap * 2; + if (new_cap < needed) new_cap = needed + 256; + char *tmp = realloc(*buf, new_cap); + if (!tmp) { closedir(d); return 0; } + *buf = tmp; + *cap = new_cap; + } + + (*buf)[*len] = ':'; + memcpy(*buf + *len + 1, child, child_len + 1); + *len += 1 + child_len; + } + + closedir(d); + return 1; +} + +/* + * Build a colon-separated library path string: $APPDIR/lib followed by every + * immediate (one-level-deep) real subdirectory within it. + * Returns a malloc'd string that the caller must free. + */ +static char *build_library_path(const char *appdir) { + char base[PATH_MAX]; + int rc = snprintf(base, sizeof(base), "%s/lib", appdir); + if (rc < 0 || (size_t)rc >= sizeof(base)) return NULL; + + size_t base_len = (size_t)rc; + size_t cap = base_len + 1024; + char *buf = malloc(cap); + if (!buf) return NULL; + + memcpy(buf, base, base_len + 1); + size_t len = base_len; + + if (!append_immediate_subdirs(base, &buf, &len, &cap)) { + free(buf); + return NULL; + } + return buf; +} + +/* ─────────────────────── redirect context ──────────────────── */ + +/* + * All information needed to perform a redirect, gathered in one place. + * `resolved` is heap-allocated; all others point to static/existing storage. + */ +typedef struct { + char *resolved; /* malloc'd canonical target path */ + char sharun_path[PATH_MAX]; + char ld_path[PATH_MAX]; + const char *self; /* points to static buffer */ + const char *appdir; /* points to environ */ +} redirect_ctx_t; + +/* + * Populate `ctx` with everything needed to redirect `filename`. + * Returns 1 if the redirect should proceed (ctx fully populated). + * Returns 0 if no redirect should happen; in that case ctx->resolved is + * always freed internally and set to NULL. + */ +static int gather_redirect_ctx(const char *filename, redirect_ctx_t *ctx) { + ctx->resolved = resolve_binary_path(filename); + if (!ctx->resolved) return 0; + + if (!should_redirect_target(ctx->resolved)) { + free(ctx->resolved); + ctx->resolved = NULL; + return 0; + } + + ctx->appdir = getenv("APPDIR"); + if (!ctx->appdir || !*ctx->appdir) { + free(ctx->resolved); + ctx->resolved = NULL; + return 0; + } + + if (!find_sharun(ctx->appdir, ctx->sharun_path, sizeof(ctx->sharun_path)) || + !find_ld_linux(ctx->appdir, ctx->ld_path, sizeof(ctx->ld_path))) { + free(ctx->resolved); + ctx->resolved = NULL; + return 0; + } + + ctx->self = get_self_path(); + if (!ctx->self) { + free(ctx->resolved); + ctx->resolved = NULL; + return 0; + } + + return 1; +} + +/* + * Build the redirected argv: + * sharun env ld-linux --library-path --preload self target [flags...] + * + * Returns malloc'd new_argv on success, NULL on failure. + * new_argv[4] (the library path string) is heap-allocated; use + * free_redirect_resources() to clean up. + */ +static char **build_redirect_argv(const redirect_ctx_t *ctx, + char *const argv[], + int *out_argc) +{ + int orig_argc = 0; + if (argv) while (argv[orig_argc]) orig_argc++; + + int skip = (orig_argc > 0) ? 1 : 0; + int trailing = orig_argc - skip; + + /* Fixed prefix: + * [0] sharun + * [1] env + * [2] ld-linux + * [3] --library-path + * [4] lib-path (heap-allocated) + * [5] --preload + * [6] self + * [7] target + */ + int new_total = 8 + trailing; + + char *lib_path = build_library_path(ctx->appdir); + if (!lib_path) return NULL; + + char **new_argv = calloc((size_t)new_total + 1, sizeof(char *)); + if (!new_argv) { + free(lib_path); + return NULL; + } + + new_argv[0] = (char *)ctx->sharun_path; + new_argv[1] = (char *)"env"; + new_argv[2] = (char *)ctx->ld_path; + new_argv[3] = (char *)"--library-path"; + new_argv[4] = lib_path; /* caller frees via free_redirect_resources */ + new_argv[5] = (char *)"--preload"; + new_argv[6] = (char *)ctx->self; + new_argv[7] = (char *)ctx->resolved; + + for (int i = 0; i < trailing; i++) + new_argv[8 + i] = argv[skip + i]; + + new_argv[new_total] = NULL; + if (out_argc) *out_argc = new_total; + return new_argv; +} + +/* Release resources allocated by build_redirect_argv and gather_redirect_ctx. */ +static void free_redirect_resources(char **new_argv, redirect_ctx_t *ctx) { + if (new_argv) { + free(new_argv[4]); /* lib_path */ + free(new_argv); + } + if (ctx) { + free(ctx->resolved); + ctx->resolved = NULL; + } +} + +/* ─────────── _SHARUN_TARGET_EXE helpers ────────────────────────── + * + * When redirecting a binary through sharun/ld-linux, we inject + * _SHARUN_TARGET_EXE= into the child's environment. + * The readlink() interceptor below reads this variable so that + * /proc/self/exe queries return the real binary path rather than the + * ld-linux interpreter path. + * ──────────────────────────────────────────────────────────────── */ + +#define SHARUN_TARGET_VAR "_SHARUN_TARGET_EXE=" +#define SHARUN_TARGET_VAR_LEN (sizeof(SHARUN_TARGET_VAR) - 1) + +/* + * Build a new envp array identical to `envp` except that + * _SHARUN_TARGET_EXE is set to `target`. + * + * Returns a heap-allocated array; free with free_envp_with_target(). + * `envp` may be NULL (treated as empty). + * Returns NULL if `target` is NULL or memory allocation fails. + */ +static char **build_envp_with_target(const char *target, + char *const envp[]) +{ + if (!target) return NULL; + + int n = 0; + int var_idx = -1; + + if (envp) { + while (envp[n]) { + if (strncmp(envp[n], SHARUN_TARGET_VAR, SHARUN_TARGET_VAR_LEN) == 0) + var_idx = n; + n++; + } + } + + size_t target_len = strlen(target); + size_t sz = SHARUN_TARGET_VAR_LEN + target_len + 1; + char *entry = malloc(sz); + if (!entry) return NULL; + memcpy(entry, SHARUN_TARGET_VAR, SHARUN_TARGET_VAR_LEN); + memcpy(entry + SHARUN_TARGET_VAR_LEN, target, target_len + 1); + + int new_n = (var_idx < 0) ? n + 1 : n; + char **new_envp = malloc(((size_t)new_n + 1) * sizeof(char *)); + if (!new_envp) { free(entry); return NULL; } + + int j = 0; + for (int i = 0; i < n; i++) { + if (i == var_idx) continue; + new_envp[j++] = (char *)envp[i]; + } + new_envp[j++] = entry; + new_envp[j] = NULL; + return new_envp; +} + +/* Release resources allocated by build_envp_with_target(). */ +static void free_envp_with_target(char **new_envp) +{ + if (!new_envp) return; + for (int i = 0; new_envp[i]; i++) { + if (strncmp(new_envp[i], SHARUN_TARGET_VAR, SHARUN_TARGET_VAR_LEN) == 0) { + free(new_envp[i]); + break; + } + } + free(new_envp); +} + +/* ───────────────────── execve interception ───────────────────── */ + +static int maybe_redirect_execve(execve_func_t real_execve, + const char *filename, + char *const argv[], + char *const envp[]) +{ + redirect_ctx_t ctx = {0}; + if (!gather_redirect_ctx(filename, &ctx)) + return real_execve(filename, argv, envp); + + int new_argc = 0; + char **new_argv = build_redirect_argv(&ctx, argv, &new_argc); + if (!new_argv) { + free(ctx.resolved); + return real_execve(filename, argv, envp); + } + + DEBUG("Redirect execve: %s -> %s\n", filename, ctx.sharun_path); + if (debug_enabled()) { + for (int i = 0; i < new_argc; i++) + fprintf(stderr, " [execve_sharun.so] >> argv[%d]=%s\n", i, new_argv[i]); + } + + /* + * Propagate the real binary path via _SHARUN_TARGET_EXE so that the + * readlink("/proc/self/exe") interceptor in the child returns the + * correct path instead of the ld-linux interpreter path. + */ + char **new_envp = build_envp_with_target(ctx.resolved, envp); + + /* Execute sharun; only returns on failure */ + (void)real_execve(ctx.sharun_path, new_argv, new_envp ? new_envp : envp); + + int saved = errno; + DEBUG("Redirect execve failed errno=%d (%s), falling back\n", saved, strerror(saved)); + + free_envp_with_target(new_envp); + free_redirect_resources(new_argv, &ctx); + return real_execve(filename, argv, envp); +} + +VISIBLE int execve(const char *filename, char *const argv[], char *const envp[]) { + execve_func_t real = (execve_func_t)dlsym(RTLD_NEXT, "execve"); + if (!real) { + errno = ENOSYS; + return -1; + } + return maybe_redirect_execve(real, filename, argv, envp); +} + +VISIBLE int execv(const char *filename, char *const argv[]) { + return execve(filename, argv, environ); +} + +VISIBLE int execvpe(const char *filename, char *const argv[], char *const envp[]) { + /* Prefer our logic by resolving and calling execve; falls back to real execvpe */ + char *resolved = resolve_binary_path(filename); + if (resolved) { + int ret = execve(resolved, argv, envp); + free(resolved); + return ret; + } + + execvpe_func_t real = (execvpe_func_t)dlsym(RTLD_NEXT, "execvpe"); + if (!real) { + errno = ENOSYS; + return -1; + } + return real(filename, argv, envp); +} + +VISIBLE int execvp(const char *filename, char *const argv[]) { + return execvpe(filename, argv, environ); +} + +/* ───────────────────── posix_spawn interception ────────────────── */ + +VISIBLE int posix_spawn(pid_t *pid, const char *path, + const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, + char *const argv[], char *const envp[]) +{ + posix_spawn_func_t real = (posix_spawn_func_t)dlsym(RTLD_NEXT, "posix_spawn"); + if (!real) return ENOSYS; + + redirect_ctx_t ctx = {0}; + if (!gather_redirect_ctx(path, &ctx)) + return real(pid, path, fa, attr, argv, envp); + + int new_argc = 0; + char **new_argv = build_redirect_argv(&ctx, argv, &new_argc); + if (!new_argv) { + free(ctx.resolved); + return real(pid, path, fa, attr, argv, envp); + } + + DEBUG("Redirect posix_spawn: %s -> %s\n", path, ctx.sharun_path); + + char **new_envp = build_envp_with_target(ctx.resolved, envp); + int rc = real(pid, ctx.sharun_path, fa, attr, new_argv, + new_envp ? new_envp : envp); + free_envp_with_target(new_envp); + free_redirect_resources(new_argv, &ctx); + return rc; +} + +VISIBLE int posix_spawnp(pid_t *pid, const char *file, + const posix_spawn_file_actions_t *fa, + const posix_spawnattr_t *attr, + char *const argv[], char *const envp[]) +{ + posix_spawnp_func_t real = (posix_spawnp_func_t)dlsym(RTLD_NEXT, "posix_spawnp"); + if (!real) return ENOSYS; + + /* Resolve first so ANYLINUX_EXECVE_WRAP_PATHS check can apply */ + redirect_ctx_t ctx = {0}; + if (!gather_redirect_ctx(file, &ctx)) + return real(pid, file, fa, attr, argv, envp); + + int new_argc = 0; + char **new_argv = build_redirect_argv(&ctx, argv, &new_argc); + if (!new_argv) { + free(ctx.resolved); + return real(pid, file, fa, attr, argv, envp); + } + + DEBUG("Redirect posix_spawnp: %s -> %s\n", file, ctx.sharun_path); + + char **new_envp = build_envp_with_target(ctx.resolved, envp); + int rc = real(pid, ctx.sharun_path, fa, attr, new_argv, + new_envp ? new_envp : envp); + free_envp_with_target(new_envp); + free_redirect_resources(new_argv, &ctx); + return rc; +} + +/* ───────────────────── readlink interception ──────────────────── + * + * When a binary runs under "ld-linux /path/to/binary", /proc/self/exe + * resolves to the ld-linux interpreter path, not the binary itself. + * Programs like the JDK launcher read /proc/self/exe to locate their + * installation directory (JAVA_HOME); if it returns the ld-linux path + * the derived path is wrong. + * + * To fix this, we intercept readlink()/readlinkat() for /proc/self/exe + * and /proc/thread-self/exe and return the real target binary path + * stored in _SHARUN_TARGET_EXE, which is injected into the process + * environment by build_envp_with_target() when our execve/posix_spawn + * hooks redirect through sharun. + * ─────────────────────────────────────────────────────────────── */ + +static int is_proc_self_exe(const char *pathname) +{ + if (!pathname) return 0; + return (strcmp(pathname, "/proc/self/exe") == 0 || + strcmp(pathname, "/proc/thread-self/exe") == 0); +} + +VISIBLE ssize_t readlink(const char *pathname, char *buf, size_t bufsz) +{ + readlink_func_t real = (readlink_func_t)dlsym(RTLD_NEXT, "readlink"); + if (!real) { errno = ENOSYS; return -1; } + + if (is_proc_self_exe(pathname)) { + const char *target = getenv("_SHARUN_TARGET_EXE"); + if (target && *target) { + size_t len = strlen(target); + /* readlink(2) truncates to bufsz without NUL-terminating. */ + size_t copy_len = (len < bufsz) ? len : bufsz; + memcpy(buf, target, copy_len); + DEBUG("readlink(%s) -> %.*s\n", pathname, (int)copy_len, buf); + return (ssize_t)copy_len; + } + } + + return real(pathname, buf, bufsz); +} + +VISIBLE ssize_t readlinkat(int dirfd, const char *pathname, + char *buf, size_t bufsz) +{ + readlinkat_func_t real = (readlinkat_func_t)dlsym(RTLD_NEXT, "readlinkat"); + if (!real) { errno = ENOSYS; return -1; } + + /* AT_FDCWD with an absolute /proc path is the only case we handle. */ + if (dirfd == AT_FDCWD && is_proc_self_exe(pathname)) { + const char *target = getenv("_SHARUN_TARGET_EXE"); + if (target && *target) { + size_t len = strlen(target); + size_t copy_len = (len < bufsz) ? len : bufsz; + memcpy(buf, target, copy_len); + DEBUG("readlinkat(AT_FDCWD, %s) -> %.*s\n", pathname, + (int)copy_len, buf); + return (ssize_t)copy_len; + } + } + + return real(dirfd, pathname, buf, bufsz); +} diff --git a/src/protontricks/data/appimage/make-appimage.sh b/src/protontricks/data/appimage/make-appimage.sh new file mode 100755 index 0000000..ea062a9 --- /dev/null +++ b/src/protontricks/data/appimage/make-appimage.sh @@ -0,0 +1,51 @@ +#!/bin/sh + +set -eu + +ARCH=$(uname -m) +VERSION=$(pacman -Q protontricks | awk '{print $2; exit}') # example command to get version of application here +export ARCH VERSION + +SHARUN="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/quick-sharun.sh" +DEBLOATED_PKGS="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/refs/heads/main/useful-tools/get-debloated-pkgs.sh" + +export OUTPATH=./dist +export ADD_HOOKS="self-updater.bg.hook" +export UPINFO="gh-releases-zsync|${GITHUB_REPOSITORY%/*}|${GITHUB_REPOSITORY#*/}|latest|*$ARCH.AppImage.zsync" +export ICON=/usr/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg +export DESKTOP=/usr/share/applications/protontricks.desktop +export LIB_DIR=/usr/lib +export DEPLOY_PYTHON=1 +export DEPLOY_OPENGL=1 +export DEPLOY_VULKAN=1 + +#Remove leftovers +rm -rf AppDir dist + +# ADD LIBRARIES +wget --retry-connrefused --tries=30 "$SHARUN" -O ./quick-sharun +wget --retry-connrefused --tries=30 "$DEBLOATED_PKGS" -O ./get-debloated-pkgs +chmod +x ./quick-sharun ./get-debloated-pkgs + +# Debloated pkgs +./get-debloated-pkgs --add-common --prefer-nano + +# Deploy dependencies +./quick-sharun \ + /usr/bin/*tricks* \ + /usr/lib/libopenjp2.so* \ + /usr/lib/libtiff.so* \ + /usr/lib/libimagequant.so* \ + /usr/bin/zenity \ + /usr/bin/yad + +echo 'unset VK_DRIVER_FILES' >> ./AppDir/.env + +cc -shared -fPIC -O2 -o ./AppDir/lib/execve-sharun-hack.so execve-sharun-hack.c -ldl +echo 'execve-sharun-hack.so' >> ./AppDir/.preload +echo 'export ANYLINUX_EXECVE_WRAP_PATHS="$DATADIR:$HOME/.steam"' >> ./AppDir/bin/execve-wrap-path.hook + +# Additional changes can be done in between here + +# Turn AppDir into AppImage +./quick-sharun --make-appimage diff --git a/src/protontricks/data/share/applications/protontricks-launch.desktop b/src/protontricks/data/share/applications/protontricks-launch.desktop index 56112d7..ed9070d 100644 --- a/src/protontricks/data/share/applications/protontricks-launch.desktop +++ b/src/protontricks/data/share/applications/protontricks-launch.desktop @@ -5,5 +5,6 @@ Type=Application Terminal=false NoDisplay=true Categories=Utility -Icon=wine +StartupWMClass=yad +Icon=com.github.Matoking.protontricks MimeType=application/x-ms-dos-executable;application/x-msi;application/x-ms-shortcut; diff --git a/src/protontricks/data/share/applications/protontricks.desktop b/src/protontricks/data/share/applications/protontricks.desktop index ec0e964..e3e2de0 100644 --- a/src/protontricks/data/share/applications/protontricks.desktop +++ b/src/protontricks/data/share/applications/protontricks.desktop @@ -5,5 +5,6 @@ Comment=A simple wrapper that does winetricks things for Proton enabled games Type=Application Terminal=false Categories=Utility; -Icon=wine +StartupWMClass=yad +Icon=com.github.Matoking.protontricks Keywords=Steam;Proton;Wine;Winetricks; diff --git a/src/protontricks/data/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg b/src/protontricks/data/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg new file mode 100644 index 0000000..b44e802 --- /dev/null +++ b/src/protontricks/data/share/icons/hicolor/scalable/apps/com.github.Matoking.protontricks.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +