Repository: beeware/briefcase
Pull request #829 (2022-08-17 19:34:43)

Ensure local linuxdeploy plugins are executable
## Issue
If a user specifies a local file without execute permissions as a `linuxdeploy` plugin, `linuxdeploy` will [ignore the plugin](https://github.com/linuxdeploy/linuxdeploy/blob/9fb2ac35d25316095a04500760a77df0e8f3b7f4/src/plugin/plugin.cpp#L77).

`linuxdeploy` Log Excerpt
```
DEBUG: Searching for plugins in directory /app/appimage/new
DEBUG: File/symlink is not executable, skipping: /app/appimage/new/linuxdeploy-plugin-gtk.sh
DEBUG: Entry is a directory, skipping: /app/appimage/new/new.AppDir
DEBUG: File/symlink is not executable, skipping: /app/appimage/new/briefcase.toml
DEBUG: File/symlink is not executable, skipping: /app/appimage/new/Dockerfile 
```
Note: these entries only show in `linuxdeploy`'s log with the `-v0` flag and `DEBUG_PLUGIN_DETECTION` env var non-null.

Additionally, plugins at that are AppImages (whether user-provided or downloaded) causes errors in Docker either because their ELF header is not patched or they are not extracted before they are run.

## Resolution
After the plugin is copied in to the app's bundle directory, set the `execute` permissions for the file. Plugins specified via URL already have execute permissions added after the download.

All AppImage files now have their ELF header patched. The `--appimage-extract-and-run` flag for `linuxdeploy` is replaced with `APPIMAGE_EXTRACT_AND_RUN=1` environment variable so all AppImages are extracted before use.

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct


codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (07e6bb7) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (07e6bb7) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 07e6bb7 differs from pull request most recent head a842396. Consider uploading reports for the commit a842396 to get more accurate results

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (a842396) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |

rmartin16: Ahhh....I didn't realize plugins could be an AppImage themselves. Nonetheless, when I try to use a local version of the Qt plugin or just specify `qt` as a plugin in `pyproject.toml` right now, `linuxdeploy` errors out.

```
>>> Running Command:
>>>     docker run --volume /home/user/tmp/beeware/newapp/linux:/app:z --volume /home/user/.cache/briefcase:/home/brutus/.cache/briefcase:z --rm --env PATH=/home/brutus/.cache/briefcase/tools/linuxdeploy_plugins/qt:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin --env VERSION=0.0.1 --env DISABLE_COPYRIGHT_FILES_DEPLOYMENT=1 --env DEBUG_PLUGIN_DETECTION=1 briefcase/com.example.newapp:py3.10 /home/brutus/.cache/briefcase/tools/linuxdeploy-x86_64.AppImage --appimage-extract-and-run --appdir=/app/appimage/new/new.AppDir -d /app/appimage/new/new.AppDir/com.example.newapp.desktop -o appimage -v0 --deploy-deps-only /app/appimage/new/new.AppDir/usr/app_packages/cairo --deploy-deps-only /app/appimage/new/new.AppDir/usr/app_packages/gi --deploy-deps-only /app/appimage/new/new.AppDir/usr/lib --deploy-deps-only /app/appimage/new/new.AppDir/usr/lib/python3.10/lib-dynload --plugin qt
>>> Working Directory:
>>>     /home/user/tmp/beeware/newapp/linux
linuxdeploy version 1-alpha (git commit ID 287c436), GitHub actions build 110 built on 2022-08-17 20:56:20 UTC
DEBUG: Searching for plugins in directory /home/brutus/.cache/briefcase/tools 
DEBUG: Entry is a directory, skipping: /home/brutus/.cache/briefcase/tools/linuxdeploy_plugins 
DEBUG: Doesn't match plugin regex, skipping: /home/brutus/.cache/briefcase/tools/linuxdeploy-x86_64.AppImage 
DEBUG: Searching for plugins in directory /tmp/appimage_extracted_f792e8e8b92755a0433a3895ce0e35fc/usr/bin 
DEBUG: Doesn't match plugin regex, skipping: /tmp/appimage_extracted_f792e8e8b92755a0433a3895ce0e35fc/usr/bin/linuxdeploy 
DEBUG: output from plugin: 0
 
DEBUG: Found plugin 'appimage': /tmp/appimage_extracted_f792e8e8b92755a0433a3895ce0e35fc/usr/bin/linuxdeploy-plugin-appimage 
DEBUG: Doesn't match plugin regex, skipping: /tmp/appimage_extracted_f792e8e8b92755a0433a3895ce0e35fc/usr/bin/patchelf 
DEBUG: Doesn't match plugin regex, skipping: /tmp/appimage_extracted_f792e8e8b92755a0433a3895ce0e35fc/usr/bin/strip 
DEBUG: Searching for plugins in directory /home/brutus/.cache/briefcase/tools/linuxdeploy_plugins/qt 
terminate called after throwing an instance of 'std::logic_error'
  what():  subprocess failed (exit code 1)
>>> Return code: 127
Building...

Error while building app newapp.
```
Using `qt` in `pyproject.toml`:
[briefcase.2022_08_17-20_23_54.build.log](https://github.com/beeware/briefcase/files/9366755/briefcase.2022_08_17-20_23_54.build.log)

Using local Qt plugin (that's been patched):
[briefcase.2022_08_17-20_28_43.build.log](https://github.com/beeware/briefcase/files/9366769/briefcase.2022_08_17-20_28_43.build.log)

The error is only happening in Docker. I'd be curious if you're hitting the error as well with `qt` in `pyproject.toml`....if only to confirm its probably just a local problem.
freakboy3742: I can confirm I'm seeing the same error in Docker; I'm wondering if this might be because of the ELF header - or possibly because the AppImage needs to be unpacked to run in Docker...
rmartin16: I also tried it unpatched and had the same problem.

This https://github.com/linuxdeploy/linuxdeploy/issues/154#issuecomment-932740990 does suggest unpacking may be a possible solution. Alternatively, there's even some interesting solutions around `dd`...
freakboy3742: I do so enjoy using a project where the project-recommended solution for getting something to run at all is "run dd over a binary"... (/me grumbles)

It would appear that the dd fix is at least partially overlapping with the patch-elf-binary fix... if the `dd` approach actually resolves the issue, then I guess we replace the elf patching with `dd`.
rmartin16: I was able to hack things together to get the unpacked Qt AppImage plugin to run in Docker....but that approach is going to encompass a lot more management for that style of plugin...

Initial testing with `dd` hasn't been successful.....I'll experiment some more tomorrow.
codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (07e6bb7) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 07e6bb7 differs from pull request most recent head 4fd2746. Consider uploading reports for the commit 4fd2746 to get more accurate results

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (4fd2746) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |
| [src/briefcase/platforms/linux/appimage.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9wbGF0Zm9ybXMvbGludXgvYXBwaW1hZ2UucHk=) | `97.45% <100.00%> (+0.02%)` | :arrow_up: |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (4fd2746) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 4fd2746 differs from pull request most recent head 1efcde5. Consider uploading reports for the commit 1efcde5 to get more accurate results

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |
| [src/briefcase/platforms/linux/appimage.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9wbGF0Zm9ybXMvbGludXgvYXBwaW1hZ2UucHk=) | `97.45% <100.00%> (+0.02%)` | :arrow_up: |

rmartin16: Ok....here's the story.

The `dd` command and our `patch_elf_header()` here are identical; they both just replace 3 bytes with zeros starting at the 8th byte of the file. The `dd` command didn't initially work for me because `linuxdeploy` was still trying to run the Qt plugin without extracting it first. To force _all_ AppImages used in the container to be extracted first, I replaced the `--appimage-extract-and-run` for `linuxdeploy` with the environment variable `APPIMAGE_EXTRACT_AND_RUN=1`.

This all required a little bit of refactoring.....and I made the ELF file detection a bit more targeted.
codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (1efcde5) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |
| [src/briefcase/platforms/linux/appimage.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9wbGF0Zm9ybXMvbGludXgvYXBwaW1hZ2UucHk=) | `97.45% <100.00%> (+0.02%)` | :arrow_up: |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (1efcde5) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 1efcde5 differs from pull request most recent head 6c4063a. Consider uploading reports for the commit 6c4063a to get more accurate results

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |
| [src/briefcase/platforms/linux/appimage.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9wbGF0Zm9ybXMvbGludXgvYXBwaW1hZ2UucHk=) | `97.45% <100.00%> (+0.02%)` | :arrow_up: |

codecov[bot]: # [Codecov](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) Report
> Merging [#829](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (6c4063a) into [main](https://codecov.io/gh/beeware/briefcase/commit/7a95f4bcdecea7f5a6aadca3d967875a8b07f3e3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) (7a95f4b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

| [Impacted Files](https://codecov.io/gh/beeware/briefcase/pull/829?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware) | Coverage Δ | |
|---|---|---|
| [src/briefcase/integrations/linuxdeploy.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9pbnRlZ3JhdGlvbnMvbGludXhkZXBsb3kucHk=) | `100.00% <100.00%> (ø)` | |
| [src/briefcase/platforms/linux/appimage.py](https://codecov.io/gh/beeware/briefcase/pull/829/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=beeware#diff-c3JjL2JyaWVmY2FzZS9wbGF0Zm9ybXMvbGludXgvYXBwaW1hZ2UucHk=) | `97.45% <100.00%> (+0.02%)` | :arrow_up: |


Patch:
diff --git a/changes/829.bugfix.rst b/changes/829.bugfix.rst
new file mode 100644
index 00000000..526d77c4
--- /dev/null
+++ b/changes/829.bugfix.rst
@@ -0,0 +1,1 @@
+All ``linuxdeploy`` plugins are made executable and ELF headers for AppImage plugins are patched for use in ``Docker``.
diff --git a/src/briefcase/integrations/linuxdeploy.py b/src/briefcase/integrations/linuxdeploy.py
index 7f57423d..fd86771b 100644
--- a/src/briefcase/integrations/linuxdeploy.py
+++ b/src/briefcase/integrations/linuxdeploy.py
@@ -13,6 +13,7 @@ from briefcase.exceptions import (
     NetworkFailure,
 )
 
+ELF_HEADER_IDENT = bytes.fromhex("7F454C46")
 ELF_PATCH_OFFSET = 0x08
 ELF_PATCH_ORIGINAL_BYTES = bytes.fromhex("414902")
 ELF_PATCH_PATCHED_BYTES = bytes.fromhex("000000")
@@ -47,16 +48,25 @@ class LinuxDeployBase:
     def install(self):
         """Download and install linuxdeploy or plugin."""
         try:
-            download_path = self.command.download_url(
+            self.command.download_url(
                 url=self.download_url,
                 download_path=self.file_path,
             )
         except requests_exceptions.ConnectionError as e:
             raise NetworkFailure(f"download {self.full_name}") from e
 
-        with self.command.input.wait_bar(f"Installing {self.full_name}..."):
-            self.command.os.chmod(download_path, 0o755)
-            if self.file_name.endswith("AppImage"):
+        self.prepare_executable()
+
+    def prepare_executable(self):
+        """Update linuxdeploy and its plugins to allow execution.
+
+        All files must be made executable to run or for linuxdeploy to
+        use them as plugins while building the AppImage. ELF files need
+        special "magic" bytes zeroed to run properly in Docker.
+        """
+        with self.command.input.wait_bar(f"Installing {self.file_name}..."):
+            self.command.os.chmod(self.file_path / self.file_name, 0o755)
+            if self.is_elf_file():
                 self.patch_elf_header()
 
     @classmethod
@@ -96,9 +106,18 @@ class LinuxDeployBase:
         self.uninstall()
         self.install()
 
+    def is_elf_file(self):
+        """Returns True if the file is an ELF object file.
+
+        The header for an ELF object file always starts with 0x7F454C46;
+        this is 0x7fELF if you interpret the last three bytes as ASCII.
+        """
+        with (self.file_path / self.file_name).open("r+b") as file:
+            return file.read(len(ELF_HEADER_IDENT)) == ELF_HEADER_IDENT
+
     def patch_elf_header(self):
-        """Patch the ELF header of the AppImage to ensure it's always
-        executable.
+        """Patch the ELF header of the AppImage to ensure it can successfully
+        run in all contexts.
 
         This patch is necessary on Linux hosts that use AppImageLauncher.
         AppImages use a modified ELF binary header starting at offset 0x08
@@ -122,13 +141,13 @@ class LinuxDeployBase:
                 appimage.write(ELF_PATCH_PATCHED_BYTES)
                 appimage.flush()
                 appimage.seek(0)
-                self.command.logger.info("Patched ELF header of linuxdeploy AppImage.")
+                self.command.logger.info(f"Patched ELF header for {self.file_name}.")
             # Else if the header is the patched value, do nothing.
             elif (
                 appimage.read(len(ELF_PATCH_ORIGINAL_BYTES)) == ELF_PATCH_PATCHED_BYTES
             ):
                 self.command.logger.info(
-                    "ELF header of linuxdeploy AppImage is already patched."
+                    f"ELF header for {self.file_name} is already patched."
                 )
             else:
                 # We should only get here if the file at the AppImage patch doesn't have
@@ -227,6 +246,8 @@ class LinuxDeployLocalFilePlugin(LinuxDeployPluginBase):
                 "Is the path correct?"
             )
 
+        self.prepare_executable()
+
 
 class LinuxDeployURLPlugin(LinuxDeployPluginBase):
     full_name = "user-provided linuxdeploy plugin from URL"
@@ -308,7 +329,7 @@ class LinuxDeploy(LinuxDeployBase):
          * A URL
          * A local file.
 
-        This definition can be preceeded by environment variables that must
+        This definition can be preceded by environment variables that must
         exist in the environment. For example, a plugin definition of:
 
             DEPLOY_GTK_VERSION=3 FOO='bar whiz' gtk
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index 6abb0eb4..54bdf344 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -199,6 +199,14 @@ class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
                 # so this effectively silences a bunch of warnings that can't
                 # be easily resolved by the end user.
                 env["DISABLE_COPYRIGHT_FILES_DEPLOYMENT"] = "1"
+                # AppImages do not run natively within a Docker container. This
+                # treats the AppImage like a self-extracting executable. Using
+                # this environment variable instead of --appimage-extract-and-run
+                # is necessary to ensure AppImage plugins are extracted as well.
+                env["APPIMAGE_EXTRACT_AND_RUN"] = "1"
+                # Explicitly declare target architecture as the current architecture.
+                # This can be used by some linuxdeploy plugins.
+                env["ARCH"] = self.host_arch
 
                 # Find all the .so files in app and app_packages,
                 # so they can be passed in to linuxdeploy to have their
@@ -215,20 +223,19 @@ class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
                 for plugin in plugins:
                     additional_args.extend(["--plugin", plugin])
 
-                # Build the app image. We use `--appimage-extract-and-run`
-                # because AppImages won't run natively inside Docker.
+                # Build the AppImage.
                 with self.dockerize(app) as docker:
                     docker.run(
                         [
                             self.linuxdeploy.file_path / self.linuxdeploy.file_name,
-                            "--appimage-extract-and-run",
-                            f"--appdir={self.appdir_path(app)}",
-                            "-d",
+                            "--appdir",
+                            os.fsdecode(self.appdir_path(app)),
+                            "--desktop-file",
                             os.fsdecode(
                                 self.appdir_path(app)
                                 / f"{app.bundle}.{app.app_name}.desktop"
                             ),
-                            "-o",
+                            "--output",
                             "appimage",
                         ]
                         + additional_args,


Test patch:
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py b/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
index abab7fd9..4bbd4fe3 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployBase__patch_elf_binary.py
@@ -94,3 +94,15 @@ def test_patch_linuxdeploy_elf_header_bad_appimage(linuxdeploy, tmp_path):
     # Create a linuxdeploy wrapper, then patch the elf header
     with pytest.raises(CorruptToolError):
         linuxdeploy = linuxdeploy.patch_elf_header()
+
+
+def test_patch_linuxdeploy_elf_header_empty_appimage(linuxdeploy, tmp_path):
+    """If file is empty, raise an error."""
+    appimage_path = tmp_path / "plugin" / "linuxdeploy-dummy-wonky.AppImage"
+
+    # Mock an bad linuxdeploy AppImage
+    create_mock_appimage(appimage_path=appimage_path, mock_appimage_kind="empty")
+
+    # Create a linuxdeploy wrapper, then patch the elf header
+    with pytest.raises(CorruptToolError):
+        linuxdeploy = linuxdeploy.patch_elf_header()
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py b/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
index 7b457988..afe2412a 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployBase__upgrade.py
@@ -64,7 +64,7 @@ def test_upgrade_exists(linuxdeploy, mock_command, tmp_path):
         download_path=tmp_path / "plugin",
     )
     # The downloaded file will be made executable
-    mock_command.os.chmod.assert_called_with("new-downloaded-file", 0o755)
+    mock_command.os.chmod.assert_called_with(appimage_path, 0o755)
 
 
 def test_upgrade_does_not_exist(linuxdeploy, mock_command):
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeployBase__verify.py b/tests/integrations/linuxdeploy/test_LinuxDeployBase__verify.py
index 37923678..b1011577 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeployBase__verify.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeployBase__verify.py
@@ -92,7 +92,7 @@ def test_verify_does_not_exist(mock_command, tmp_path):
         download_path=tmp_path / "tools" / "somewhere",
     )
     # The downloaded file will be made executable
-    mock_command.os.chmod.assert_called_with("new-downloaded-file", 0o755)
+    mock_command.os.chmod.assert_called_with(appimage_path, 0o755)
 
     # The build command retains the path to the downloaded file.
     assert linuxdeploy.file_path == appimage_path.parent
@@ -115,7 +115,7 @@ def test_verify_does_not_exist_non_appimage(mock_command, tmp_path):
         download_path=tmp_path / "tools" / "somewhere",
     )
     # The downloaded file will be made executable
-    mock_command.os.chmod.assert_called_with("new-downloaded-file", 0o755)
+    mock_command.os.chmod.assert_called_with(tool_path, 0o755)
 
     # The tool content hasn't been altered
     with tool_path.open("r") as f:
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py b/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py
new file mode 100644
index 00000000..160476c9
--- /dev/null
+++ b/tests/integrations/linuxdeploy/test_LinuxDeploy__is_elf_file.py
@@ -0,0 +1,50 @@
+import pytest
+
+from briefcase.integrations.linuxdeploy import LinuxDeployBase
+from tests.integrations.linuxdeploy.utils import create_mock_appimage
+
+
+@pytest.fixture
+def linuxdeploy(mock_command, tmp_path):
+    class LinuxDeployDummy(LinuxDeployBase):
+        name = "dummy-plugin"
+
+        @property
+        def file_name(self):
+            return "linuxdeploy-dummy-wonky.AppImage"
+
+        @property
+        def download_url(self):
+            return "https://example.com/path/to/linuxdeploy-dummy-wonky.AppImage"
+
+        @property
+        def file_path(self):
+            return tmp_path / "plugin"
+
+    return LinuxDeployDummy(mock_command)
+
+
+def test_is_elf_header_positive_detection(linuxdeploy, tmp_path):
+    """File with an ELF Header is identified as an ELF file."""
+    create_mock_appimage(
+        appimage_path=tmp_path / "plugin" / "linuxdeploy-dummy-wonky.AppImage"
+    )
+    assert linuxdeploy.is_elf_file() is True
+
+
+def test_is_elf_header_negative_detection(linuxdeploy, tmp_path):
+    """File without an ELF Header is not identified as an ELF file."""
+    create_mock_appimage(
+        appimage_path=tmp_path / "plugin" / "linuxdeploy-dummy-wonky.AppImage",
+        mock_appimage_kind="corrupt",
+    )
+    assert linuxdeploy.is_elf_file() is False
+
+
+def test_is_elf_header_empty_file(linuxdeploy, tmp_path):
+    """Empty file is not identified as an ELF file without any errors."""
+    create_mock_appimage(
+        appimage_path=tmp_path / "plugin" / "linuxdeploy-dummy-wonky.AppImage",
+        mock_appimage_kind="empty",
+    )
+    assert linuxdeploy.is_elf_file() is False
diff --git a/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py b/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
index 6a4d5729..0c508701 100644
--- a/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
+++ b/tests/integrations/linuxdeploy/test_LinuxDeploy__verify_plugins.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from unittest.mock import MagicMock
 
 import pytest
 
@@ -178,6 +179,7 @@ def test_plugin_env(
     expected_env,
 ):
     """All linuxdeploy installs are managed."""
+    linuxdeploy.plugins["gtk"].is_elf_file = MagicMock(return_value=False)
 
     plugins = linuxdeploy.verify_plugins(
         [plugin_declaration],
@@ -194,7 +196,7 @@ def test_plugin_env(
     reason="Windows paths can't be passed to linuxdeploy",
 )
 def test_complex_plugin_config(linuxdeploy, mock_command, tmp_path):
-    """A comple plugin configuration can be verified."""
+    """A complex plugin configuration can be verified."""
     # Define multiple plugins, of different types, each with different environments
 
     # Three tools are obtained by downloading.
@@ -222,8 +224,8 @@ def test_complex_plugin_config(linuxdeploy, mock_command, tmp_path):
                 tmp_path
                 / "tools"
                 / "linuxdeploy_plugins"
-                / "sometool"
-                / "f3355f8e631ffc1abbb7afd37b36315f7846182ca2276c481fb9a43a7f4d239f"
+                / "network"
+                / "f93c7e6d04425e4ed7e533655d9898c4984f104711f4ffd3b3966cfc92500b2d"
                 / "linuxdeploy-plugin-network.sh"
             )(url, download_path)
         else:
diff --git a/tests/integrations/linuxdeploy/utils.py b/tests/integrations/linuxdeploy/utils.py
index cab098da..ac7e7100 100644
--- a/tests/integrations/linuxdeploy/utils.py
+++ b/tests/integrations/linuxdeploy/utils.py
@@ -25,6 +25,7 @@ def create_mock_appimage(appimage_path: Path, mock_appimage_kind: str = "origina
         "original": bytes.fromhex("7f454c46020101004149020000000000"),
         "patched": bytes.fromhex("7f454c46020101000000000000000000"),
         "corrupt": bytes.fromhex("%030x" % randrange(16**30)),
+        "empty": b"",
     }
 
     appimage_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index bc736474..70318f65 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -134,11 +134,11 @@ def test_build_appimage(build_command, first_app, tmp_path):
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "data" / "tools" / "linuxdeploy-wonky.AppImage"),
-            "--appimage-extract-and-run",
-            f"--appdir={app_dir}",
-            "-d",
+            "--appdir",
+            os.fsdecode(app_dir),
+            "--desktop-file",
             os.fsdecode(app_dir / "com.example.first-app.desktop"),
-            "-o",
+            "--output",
             "appimage",
             "--deploy-deps-only",
             os.fsdecode(app_dir / "usr" / "app" / "support"),
@@ -151,6 +151,8 @@ def test_build_appimage(build_command, first_app, tmp_path):
             "PATH": "/usr/local/bin:/usr/bin:/path/to/somewhere",
             "VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
+            "APPIMAGE_EXTRACT_AND_RUN": "1",
+            "ARCH": "wonky",
         },
         cwd=os.fsdecode(tmp_path / "project" / "linux"),
         text=True,
@@ -203,11 +205,11 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path):
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "data" / "tools" / "linuxdeploy-wonky.AppImage"),
-            "--appimage-extract-and-run",
-            f"--appdir={app_dir}",
-            "-d",
+            "--appdir",
+            os.fsdecode(app_dir),
+            "--desktop-file",
             os.fsdecode(app_dir / "com.example.first-app.desktop"),
-            "-o",
+            "--output",
             "appimage",
             "--deploy-deps-only",
             os.fsdecode(app_dir / "usr" / "app" / "support"),
@@ -225,6 +227,8 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path):
             "DEPLOY_GTK_VERSION": "3",
             "VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
+            "APPIMAGE_EXTRACT_AND_RUN": "1",
+            "ARCH": "wonky",
         },
         cwd=os.fsdecode(tmp_path / "project" / "linux"),
         text=True,
@@ -233,6 +237,16 @@ def test_build_appimage_with_plugin(build_command, first_app, tmp_path):
         stderr=subprocess.STDOUT,
         bufsize=1,
     )
+    # Local plugin marked executable
+    build_command.os.chmod.assert_any_call(
+        tmp_path
+        / "project"
+        / "linux"
+        / "appimage"
+        / "First App"
+        / "linuxdeploy-plugin-something.sh",
+        0o755,
+    )
     # Binary is marked executable
     build_command.os.chmod.assert_called_with(
         tmp_path / "project" / "linux" / "First_App-0.0.1-wonky.AppImage", 0o755
@@ -258,11 +272,11 @@ def test_build_failure(build_command, first_app, tmp_path):
     build_command._subprocess.Popen.assert_called_with(
         [
             os.fsdecode(tmp_path / "data" / "tools" / "linuxdeploy-wonky.AppImage"),
-            "--appimage-extract-and-run",
-            f"--appdir={app_dir}",
-            "-d",
+            "--appdir",
+            os.fsdecode(app_dir),
+            "--desktop-file",
             os.fsdecode(app_dir / "com.example.first-app.desktop"),
-            "-o",
+            "--output",
             "appimage",
             "--deploy-deps-only",
             os.fsdecode(app_dir / "usr" / "app" / "support"),
@@ -275,6 +289,8 @@ def test_build_failure(build_command, first_app, tmp_path):
             "PATH": "/usr/local/bin:/usr/bin:/path/to/somewhere",
             "VERSION": "0.0.1",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT": "1",
+            "APPIMAGE_EXTRACT_AND_RUN": "1",
+            "ARCH": "wonky",
         },
         cwd=os.fsdecode(tmp_path / "project" / "linux"),
         text=True,
@@ -316,13 +332,17 @@ def test_build_appimage_in_docker(build_command, first_app, tmp_path):
             "VERSION=0.0.1",
             "--env",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT=1",
+            "--env",
+            "APPIMAGE_EXTRACT_AND_RUN=1",
+            "--env",
+            "ARCH=wonky",
             f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
             "/home/brutus/.cache/briefcase/tools/linuxdeploy-wonky.AppImage",
-            "--appimage-extract-and-run",
-            "--appdir=/app/appimage/First App/First App.AppDir",
-            "-d",
+            "--appdir",
+            "/app/appimage/First App/First App.AppDir",
+            "--desktop-file",
             "/app/appimage/First App/First App.AppDir/com.example.first-app.desktop",
-            "-o",
+            "--output",
             "appimage",
             "--deploy-deps-only",
             "/app/appimage/First App/First App.AppDir/usr/app/support",
@@ -405,13 +425,17 @@ def test_build_appimage_with_plugins_in_docker(build_command, first_app, tmp_pat
             "VERSION=0.0.1",
             "--env",
             "DISABLE_COPYRIGHT_FILES_DEPLOYMENT=1",
+            "--env",
+            "APPIMAGE_EXTRACT_AND_RUN=1",
+            "--env",
+            "ARCH=wonky",
             f"briefcase/com.example.first-app:py3.{sys.version_info.minor}",
             "/home/brutus/.cache/briefcase/tools/linuxdeploy-wonky.AppImage",
-            "--appimage-extract-and-run",
-            "--appdir=/app/appimage/First App/First App.AppDir",
-            "-d",
+            "--appdir",
+            "/app/appimage/First App/First App.AppDir",
+            "--desktop-file",
             "/app/appimage/First App/First App.AppDir/com.example.first-app.desktop",
-            "-o",
+            "--output",
             "appimage",
             "--deploy-deps-only",
             "/app/appimage/First App/First App.AppDir/usr/app/support",
@@ -431,6 +455,16 @@ def test_build_appimage_with_plugins_in_docker(build_command, first_app, tmp_pat
         stderr=subprocess.STDOUT,
         bufsize=1,
     )
+    # Local plugin marked executable
+    build_command.os.chmod.assert_any_call(
+        tmp_path
+        / "project"
+        / "linux"
+        / "appimage"
+        / "First App"
+        / "linuxdeploy-plugin-something.sh",
+        0o755,
+    )
     # Binary is marked executable
     build_command.os.chmod.assert_called_with(
         tmp_path / "project" / "linux" / "First_App-0.0.1-wonky.AppImage", 0o755