Repository: AppImageCrafters/appimage-builder (language: python)

Issue:
[Feature Request] Allow Excluding File From `PT_INTERP` Change (Or Add `AppDir.files.do_not_modify`)
(This change is extremely niche, but shouldn't be too hard to implement.)

I'm making a predominately x86_64 AppImage that contains an ARM binary.

Normally, my program depends on `libc6-armhf-cross:all` which installs a binary interpreter at `/usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3`. Then I can use QEMU user-mode's `QEMU_LD_PREFIX` environmental variable to make `/lib/ld-linux-armhf.so.3` resolve correctly to it. And it works great!

This allows me to install and run an ARM binary without actually installing `/lib/ld-linux-armhf.so.3` at the correct place. Which is necessary because installing it there requires an ARM package from an ARM repo. And that is bad because Ubuntu's multi-arch support with ARM is terrible, like it requires you to add the Ubuntu Ports repo manually, then block ARM from all other repos to prevent `apt update` from failing. Bottom-line: it is extremely beneficial to just use `libc6-armhf-cross:all` at `/usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3`.

The behavior of replacing `PT_INTERP` breaks this for 2 reasons:
1. It fully replaces `PT_INTERP`, meaning I can't just slap on a `$APPDIR/usr/arm-linux-gnueabihf/` prefix and have it resolve. Instead it would resolve to `$APPDIR/usr/arm-linux-gnueabihf/tmp/appimage-8t2fP7a-ld-linux-armhf.so.3` which doesn't exist.
2. It tries to mount `/lib/ld-linux-armhf.so.3` from the host system, which for reasons mentioned above doesn't exist. This is justa warning though.

The relevant code: https://github.com/AppImageCrafters/appimage-builder/blob/f10b7d19181d8dbf911ee9bc08dc0c188d7f6601/appimagebuilder/modules/setup/helpers/libc.py#L93

My suggestion would be to add a `do_not_modify` list to `AppDir.files`, and all files listed would remain intact.

Thank you!


Discussion / hints:
Also, I'd be willing to write the code for this if approved.
@TheBrokenRail patching is performed at setup time. This is configured from the `AppDir > runtime` section. I guess we could add a key there with the list of files to exclude from patching. Something like:

```
AppDir:
  runtime:
    preserve:
    - usr/bin/my_native_app
```
So all files that get listed there will be not patched/nor processed by the setup scripts.
Please feel free to suggest a better name for the key and to send a PR.
`AppDir.runtime.preserve` sounds good to me! I'll start working on a PR.

Fix patch:
diff --git a/appimagebuilder/commands/setup_app_info.py b/appimagebuilder/commands/setup_app_info.py
index 57b3050a..83920ae2 100644
--- a/appimagebuilder/commands/setup_app_info.py
+++ b/appimagebuilder/commands/setup_app_info.py
@@ -29,4 +29,6 @@ def __call__(self, *args, **kwargs):
         icon_bundler.bundle_icon()
 
         desktop_entry_generator = DesktopEntryGenerator(self.context.app_dir)
-        desktop_entry_generator.generate(self.context.app_info, self.context.bundle_info.runtime_arch)
+        desktop_entry_generator.generate(
+            self.context.app_info, self.context.bundle_info.runtime_arch
+        )
diff --git a/appimagebuilder/commands/setup_symlinks.py b/appimagebuilder/commands/setup_symlinks.py
index de590614..d1fbfec3 100644
--- a/appimagebuilder/commands/setup_symlinks.py
+++ b/appimagebuilder/commands/setup_symlinks.py
@@ -12,26 +12,47 @@
 import os
 import pathlib
 
+from appimagebuilder.recipe import Roamer
 from appimagebuilder.utils.finder import Finder
 from appimagebuilder.commands.command import Command
 
 
 class SetupSymlinksCommand(Command):
-    def __init__(self, context, finder: Finder):
+    def __init__(self, context, recipe: Roamer, finder: Finder):
         super().__init__(context, "symlinks setup")
         self._finder = finder
 
+        self._preserve_files = []
+        base_paths = [
+            self._finder.base_path,
+            self._finder.base_path / "runtime" / "compat",
+        ]
+        preserve_paths = recipe.AppDir.runtime.preserve() or []
+        for pattern in preserve_paths:
+            for base_path in base_paths:
+                for match in base_path.glob(pattern):
+                    if match.is_dir():
+                        self._preserve_files.extend(match.glob("**/*"))
+                    else:
+                        self._preserve_files.append(match)
+
     def id(self):
         return "symlinks-setup"
 
     def __call__(self, *args, **kwargs):
         for link in self._finder.find("*", [Finder.is_symlink]):
-            relative_root = (
-                self.context.app_dir
-                if "runtime/compat" not in str(link)
-                else self.context.app_dir / "runtime" / "compat"
-            )
-            self._make_symlink_relative(link, relative_root)
+            allowed = True
+            for preserve_file in self._preserve_files:
+                if preserve_file.samefile(bin):
+                    allowed = False
+                    break
+            if allowed:
+                relative_root = (
+                    self.context.app_dir
+                    if "runtime/compat" not in str(link)
+                    else self.context.app_dir / "runtime" / "compat"
+                )
+                self._make_symlink_relative(link, relative_root)
 
     @staticmethod
     def _make_symlink_relative(path, relative_root):
diff --git a/appimagebuilder/context.py b/appimagebuilder/context.py
index 45a727ba..feb28ceb 100644
--- a/appimagebuilder/context.py
+++ b/appimagebuilder/context.py
@@ -88,7 +88,12 @@ class Context:
     record: dict
 
     def __init__(
-        self, recipe: pathlib.Path, app_info, bundle_info, app_dir: pathlib.Path, cache_dir: pathlib.Path
+        self,
+        recipe: pathlib.Path,
+        app_info,
+        bundle_info,
+        app_dir: pathlib.Path,
+        cache_dir: pathlib.Path,
     ):
         self.recipe = recipe
         self.app_info = app_info
diff --git a/appimagebuilder/modules/deploy/apt/listings.py b/appimagebuilder/modules/deploy/apt/listings.py
index 0b9083ae..9ed3a45f 100644
--- a/appimagebuilder/modules/deploy/apt/listings.py
+++ b/appimagebuilder/modules/deploy/apt/listings.py
@@ -67,7 +67,7 @@
     "libgbm*",
     "libgl1*",
     "libglapi*",
-    "libgles2*",
+    "libgles*",
     "libglvnd*",
     "libglx*",
     "mesa-*",
diff --git a/appimagebuilder/modules/setup/generator.py b/appimagebuilder/modules/setup/generator.py
index 9f527313..c9b31c8c 100644
--- a/appimagebuilder/modules/setup/generator.py
+++ b/appimagebuilder/modules/setup/generator.py
@@ -53,6 +53,8 @@ def __init__(self, recipe: {}, finder: Finder):
             logging.error("Hooks required when setting path mappings")
             raise RuntimeError("Path Mappings set without hooks")
 
+        self.preserve_paths = recipe.AppDir.runtime.preserve() or []
+
         self.finder = finder
 
         self.path_mappings_env: Final = "APPDIR_PATH_MAPPINGS"
@@ -76,6 +78,21 @@ def generate(self):
         self._write_appdir_env(runtime_env)
         self._deploy_apprun(resolver)
 
+    def _get_preserve_files(self):
+        preserve_files = []
+        base_paths = [
+            self.finder.base_path,
+            self.finder.base_path / "runtime" / "compat",
+        ]
+        for pattern in self.preserve_paths:
+            for base_path in base_paths:
+                for match in base_path.glob(pattern):
+                    if match.is_dir():
+                        preserve_files.extend(match.glob("**/*"))
+                    else:
+                        preserve_files.append(match)
+        return preserve_files
+
     def _setup_path_mappings(self, runtime_env, interpreter_paths: list):
         # map used interpreters
         interpreter_paths = sorted(set(interpreter_paths))
@@ -94,8 +111,15 @@ def _patch_interpreted_executables(self, executables, patcher: ExecutablesPatche
             if isinstance(executable, InterpretedExecutable)
         ]
 
+        preserve_files = self._get_preserve_files()
         for executable in interpreted_executables:
-            patcher.patch_interpreted_executable(executable.path)
+            allowed = True
+            for preserve_file in preserve_files:
+                if preserve_file.samefile(executable):
+                    allowed = False
+                    break
+            if allowed:
+                patcher.patch_interpreted_executable(executable.path)
 
     def _find_embed_archs(self, executables):
         embed_archs = set()
@@ -136,7 +160,8 @@ def _configure_runtime_environment(self):
             }
         )
 
-        self._run_configuration_helpers(global_env)
+        preserve_files = self._get_preserve_files()
+        self._run_configuration_helpers(global_env, preserve_files)
         for k, v in self.user_env.items():
             if k in global_env:
                 logging.info("Overriding runtime environment %s" % k)
@@ -147,7 +172,7 @@ def _configure_runtime_environment(self):
 
         return global_env
 
-    def _run_configuration_helpers(self, global_env):
+    def _run_configuration_helpers(self, global_env, preserve_files: [Path]):
         execution_list = [
             helpers.GdkPixbuf,
             helpers.GLib,
@@ -164,7 +189,7 @@ def _run_configuration_helpers(self, global_env):
         for helper in execution_list:
             logging.info("Running configuration helper: %s" % helper.__name__)
             inst = helper(self.appdir_path, self.finder)
-            inst.configure(global_env)
+            inst.configure(global_env, preserve_files)
 
     def _deploy_apprun(self, resolver: AppRunBinariesResolver):
         bin_path = self.appdir_path / self.main_exec
@@ -256,11 +281,13 @@ def _get_appdir_library_paths(self):
         return sorted(paths)
 
     def _get_bin_paths(self):
-        paths = set(self.finder.find_dirs_containing(
-            pattern="*",
-            file_checks=[Finder.is_file, Finder.is_executable],
-            excluded_patterns=["*/runtime/compat*"],
-        ))
+        paths = set(
+            self.finder.find_dirs_containing(
+                pattern="*",
+                file_checks=[Finder.is_file, Finder.is_executable],
+                excluded_patterns=["*/runtime/compat*"],
+            )
+        )
         return sorted([path.__str__() for path in paths])
 
     def _create_default_runtime(self, runtime_env):
diff --git a/appimagebuilder/modules/setup/helpers/base_helper.py b/appimagebuilder/modules/setup/helpers/base_helper.py
index de69b336..cc543e31 100644
--- a/appimagebuilder/modules/setup/helpers/base_helper.py
+++ b/appimagebuilder/modules/setup/helpers/base_helper.py
@@ -22,7 +22,7 @@ def __init__(self, app_dir, finder: Finder):
         self.env = {}
         self.scripts = {}
 
-    def configure(self, app_run):
+    def configure(self, env, preserve_files):
         pass
 
     @staticmethod
diff --git a/appimagebuilder/modules/setup/helpers/gdk_pixbuf.py b/appimagebuilder/modules/setup/helpers/gdk_pixbuf.py
index 2a4bc1e8..11cba5db 100644
--- a/appimagebuilder/modules/setup/helpers/gdk_pixbuf.py
+++ b/appimagebuilder/modules/setup/helpers/gdk_pixbuf.py
@@ -21,7 +21,7 @@
 
 
 class GdkPixbuf(BaseHelper):
-    def configure(self, global_env: Environment):
+    def configure(self, env: Environment, preserve_files):
         loaders_dir_path = self.finder.find_one(
             "*/gdk-pixbuf-2.0/*/loaders", [Finder.is_dir]
         )
@@ -32,8 +32,8 @@ def configure(self, global_env: Environment):
             logging.info("GDK loaders cache modules dir: %s" % loaders_cache_path)
             self._generate_loaders_cache(loaders_cache_path)
 
-            global_env.set("GDK_PIXBUF_MODULEDIR", loaders_dir_path)
-            global_env.set("GDK_PIXBUF_MODULE_FILE", loaders_cache_path)
+            env.set("GDK_PIXBUF_MODULEDIR", loaders_dir_path)
+            env.set("GDK_PIXBUF_MODULE_FILE", loaders_cache_path)
 
     def _generate_loaders_cache(self, loaders_cache_path):
         bin_path = self._find_gdk_pixbuf_query_loaders_bin()
diff --git a/appimagebuilder/modules/setup/helpers/glib.py b/appimagebuilder/modules/setup/helpers/glib.py
index 89956355..43ef9668 100644
--- a/appimagebuilder/modules/setup/helpers/glib.py
+++ b/appimagebuilder/modules/setup/helpers/glib.py
@@ -18,7 +18,7 @@
 
 
 class GLib(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         self._configure_gio_modules(env)
         self._configure_schemas(env)
         self._configure_girepository(env)
diff --git a/appimagebuilder/modules/setup/helpers/gstreamer.py b/appimagebuilder/modules/setup/helpers/gstreamer.py
index 444548ea..1639ef34 100644
--- a/appimagebuilder/modules/setup/helpers/gstreamer.py
+++ b/appimagebuilder/modules/setup/helpers/gstreamer.py
@@ -20,7 +20,7 @@
 
 
 class GStreamer(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         self._set_gst_plugins_path(env)
         self._set_gst_plugins_scanner_path(env)
         self._set_ptp_helper_path(env)
diff --git a/appimagebuilder/modules/setup/helpers/gtk.py b/appimagebuilder/modules/setup/helpers/gtk.py
index ca17081f..510388c9 100644
--- a/appimagebuilder/modules/setup/helpers/gtk.py
+++ b/appimagebuilder/modules/setup/helpers/gtk.py
@@ -23,7 +23,7 @@ class Gtk(BaseHelper):
     Reference: https://developer.gnome.org/gtk3/stable/gtk-running.html
     """
 
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         prefix = self.app_dir / "usr"
         env.set("GTK_EXE_PREFIX", str(prefix))
         env.set("GTK_DATA_PREFIX", str(prefix))
diff --git a/appimagebuilder/modules/setup/helpers/java.py b/appimagebuilder/modules/setup/helpers/java.py
index 7d3f3455..cf2ac09a 100644
--- a/appimagebuilder/modules/setup/helpers/java.py
+++ b/appimagebuilder/modules/setup/helpers/java.py
@@ -18,7 +18,7 @@
 
 
 class Java(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         java_path = self.finder.find_one("java", [Finder.is_file, Finder.is_executable])
         if java_path:
             java_home = Path(java_path).parent.parent
diff --git a/appimagebuilder/modules/setup/helpers/libc.py b/appimagebuilder/modules/setup/helpers/libc.py
index 42de2ac1..3f01f353 100644
--- a/appimagebuilder/modules/setup/helpers/libc.py
+++ b/appimagebuilder/modules/setup/helpers/libc.py
@@ -53,9 +53,9 @@ def get_glibc_versioned_path(self) -> Optional[Path]:
         else:
             return None
 
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files: [pathlib.Path]):
         try:
-            self._patch_executables_interpreter()
+            self._patch_executables_interpreter(preserve_files)
             env.set("APPDIR_LIBC_LINKER_PATH", list(self.interpreters))
             env.set("APPDIR_LIBC_LIBRARY_PATH", self._get_libc_library_paths())
             env.set("APPDIR_LIBC_VERSION", self._guess_libc_version())
@@ -112,7 +112,7 @@ def read_libc_version_from_embed_strings(libc_path):
             else:
                 raise InterpreterHandlerError("Unable to determine glibc version")
 
-    def _patch_executables_interpreter(self):
+    def _patch_executables_interpreter(self, preserve_files: [pathlib.Path]):
         binaries = self.finder.find(
             pattern="*",
             check_true=[
@@ -123,7 +123,13 @@ def _patch_executables_interpreter(self):
             ],
         )
         for bin_path in binaries:
-            self._make_interpreter_path_relative(bin_path)
+            allowed = True
+            for preserve_file in preserve_files:
+                if preserve_file.samefile(bin):
+                    allowed = False
+                    break
+            if allowed:
+                self._make_interpreter_path_relative(bin_path)
 
     def _make_interpreter_path_relative(self, bin_path):
         try:
diff --git a/appimagebuilder/modules/setup/helpers/libgl.py b/appimagebuilder/modules/setup/helpers/libgl.py
index ba98f02d..c3527825 100644
--- a/appimagebuilder/modules/setup/helpers/libgl.py
+++ b/appimagebuilder/modules/setup/helpers/libgl.py
@@ -17,7 +17,7 @@
 
 
 class LibGL(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         dri_path = self.finder.find_one("*/dri/*.so", [Finder.is_file, Finder.is_elf])
         if dri_path:
             env.set("LIBGL_DRIVERS_PATH", dri_path.parent)
diff --git a/appimagebuilder/modules/setup/helpers/openssl.py b/appimagebuilder/modules/setup/helpers/openssl.py
index 4b6bcffc..e4907030 100644
--- a/appimagebuilder/modules/setup/helpers/openssl.py
+++ b/appimagebuilder/modules/setup/helpers/openssl.py
@@ -18,7 +18,7 @@
 
 
 class OpenSSL(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         engines_dir = self.finder.find_one("*/openssl-*/engines", [Finder.is_dir])
         if engines_dir:
             env.set("OPENSSL_ENGINES", engines_dir)
diff --git a/appimagebuilder/modules/setup/helpers/python.py b/appimagebuilder/modules/setup/helpers/python.py
index 18382f9c..bdedfd60 100644
--- a/appimagebuilder/modules/setup/helpers/python.py
+++ b/appimagebuilder/modules/setup/helpers/python.py
@@ -18,7 +18,7 @@
 
 
 class Python(BaseHelper):
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         python_path = self.finder.find_one(
             "*/bin/python?", [Finder.is_file, Finder.is_executable]
         )
diff --git a/appimagebuilder/modules/setup/helpers/qt.py b/appimagebuilder/modules/setup/helpers/qt.py
index 8af465ce..0b3b46cb 100644
--- a/appimagebuilder/modules/setup/helpers/qt.py
+++ b/appimagebuilder/modules/setup/helpers/qt.py
@@ -25,7 +25,7 @@ def __init__(self, app_dir, app_dir_files):
         self.app_dir = Path(app_dir)
         self._qt_dirs = {}
 
-    def configure(self, env: Environment):
+    def configure(self, env: Environment, preserve_files):
         self._locate_qt5_dirs()
         if self._qt_dirs:
             # deploy a qt.conf file next to executable files that may start a Qt application
diff --git a/appimagebuilder/orchestrator.py b/appimagebuilder/orchestrator.py
index 64ad6cd9..99523066 100644
--- a/appimagebuilder/orchestrator.py
+++ b/appimagebuilder/orchestrator.py
@@ -112,7 +112,7 @@ def _create_setup_commands(self, context, recipe):
             commands.append(command)
 
         finder = Finder(context.app_dir)
-        commands.append(SetupSymlinksCommand(context, finder))
+        commands.append(SetupSymlinksCommand(context, recipe, finder))
 
         commands.append(SetupRuntimeCommand(context, recipe, finder))
 
diff --git a/appimagebuilder/recipe/schema.py b/appimagebuilder/recipe/schema.py
index ae340dab..da1f6e82 100644
--- a/appimagebuilder/recipe/schema.py
+++ b/appimagebuilder/recipe/schema.py
@@ -41,6 +41,7 @@ def __init__(self):
             Optional("no_hooks"): bool,
             Optional("path_mappings"): [str],
             Optional("env"): {str: Or(str, int, bool)},
+            Optional("preserve"): [str],
         }
 
         self.v1_tests = {


Test patch:
diff --git a/tests/recipe/test_validator.py b/tests/recipe/test_validator.py
index 936d3c04..dae5adcb 100644
--- a/tests/recipe/test_validator.py
+++ b/tests/recipe/test_validator.py
@@ -67,6 +67,9 @@ def test_validate_appdir(self):
                 "env": {
                     "PATH": "${APPDIR}/usr/bin:${PATH}",
                 },
+                "preserve": [
+                    "usr/bin/example"
+                ],
                 "debug": False,
             },
             "files": {"include": ["/one", "/two"], "exclude": ["three", "four"]},