AppImage / AppImageKit

Package desktop applications as AppImages that run on common Linux-based operating systems, such as RHEL, CentOS, openSUSE, SLED, Ubuntu, Fedora, debian and derivatives. Join #AppImage on irc.libera.chat
http://appimage.org
Other
8.67k stars 554 forks source link

Add support for checking for base dependencies #968

Open probonopd opened 5 years ago

probonopd commented 5 years ago

@Conan-Kudo suggested in https://twitter.com/Det_Conan_Kudo/status/1137997295490875393:

When I made AppImages, I actually made the user experience better by adding support for checking for base dependencies I didn't want to ship using rpm itself. It was easy and didn't require root access. It helped inform the user whether or not the AppImage would work.

I considered this equivalent to the base runtime checks a lot of Windows portable applications do to make sure they run on the version of Windows the user is trying to run them on. Doing so made the user experience much better and saner for everyone involved.

But I shouldn't have to write those checks. Those should be built into how AppImages are made. That's a foundational property that the AppImage developers don't want to acknowledge is necessary for AppImages to be reasonably successful.

Sounds like a good idea. Are we talking about runtime or AppImage creation build time checks?

Both. Build-time checks allow you to compose a list of base dependencies you need to check for. You obviously need to check them at runtime before executing the program contained in the AppImage in order to gracefully fail.

I like it very much. Do you still have your old code around and would you be willing to contribute it to the project?

Not anymore, unfortunately. It's been a fair number of years, and I've lost the backup chain in between switching computers over the years that had it. It's not hard to write such checks with rpm in a distro-neutral way, though.

At build time, you're more or less doing the equivalent of the rpm library dependency generator: https://github.com/rpm-software-management/rpm/blob/master/fileattrs/elf.attr … You'd filter out the runtime requires to check for based on what libraries you bundle, so you must check all ELF content in the AppImage to make the list.

Because of the nature of RPM dependencies, even if you build your AppImage on CentOS 6 or CentOS 7, you'll be able to generate a list that works across all RPM distributions, since it's not based on package names, but the virtual names that are common to all. Then at run-time, you'd go through your list and do something like: $ rpm --quiet --query --whatprovides "$DEP". If the return code is zero, then it is on the system. If it non-zero, then it isn't.

Do you see a way to do the same that does not involve querying the package manager, but checking the actual files on disk? Because not every distribution is using rpm...

I sort of hand-waved that away and fell back to normal failure modes for AppImages on non-rpm based distros because of that. But often times the virtual names contain the filenames of libraries, so you could do a filename check if you know where libraries are installed...

For example: libc.so.6(GLIBC_2.29)(64bit) contains libc.so.6 and "(64bit)", which tells you that you should look for libc.so .6 in /usr/lib64 or equivalent directory. You can then run the elfdeps program to see if it supports it.

So if you know you need a particular capability, you can check by running the following and checking if the output has a matching virtual name: $ /usr/lib/rpm/elfdeps --provides /usr/lib64/libc.so.6

And voila, no rpmdb or rpm-managed system required, you're still able to do dependency checks, even on Debian or Arch systems, as long as elfdeps works. :)

probonopd commented 5 years ago

Wdyt @TheAssassin @azubieta?

I vaguely remember that MuseScore also is doing some run-time dependency checking, correct @shoogle?

shoogle commented 5 years ago

That's right. MuseScore's AppRun has an accompanying portable-utils script that does runtime dependency checks.

Download MuseScore's AppImage and run it with the check-depends option.

./MuseScore*.AppImage check-depends

It then prints out a dependency report like this:

Distro: Ubuntu 18.04
Architecture: x86_64
Kernel: X.Y.Z

In package only: 2
  libfoo.so
  libbar.so

System only: 1
  libc.so

Provided by both: 2
  libdeb.so
  libX.so

Provided by neither: 1
  libbaz.so

Extra: (in package but unlinked. Possibly needed by plugins) 2
  libQtPluginA.so
  libQtPluginB.so

What the headings mean:

shoogle commented 5 years ago

The idea is that the developer runs the check-depends on all target systems and then bundles libraries that appeared under the "Provided by neither" heading on any system. At this point the application should run, but may crash when you try to run a plugin. You can work out which extra libraries are required by making an educated guess based on:

Also, if the AppImage crashes on a user's machine we can ask them to create a report like this:

./MuseScore*.AppImage check-depends > dependency-report.txt

They can attach this report to their issue or bug report.

shoogle commented 5 years ago

FYI, the dependency lists are populated as follows.

  1. The AppDir is scanned (using find) for all ELF executables and libraries.
    • These "bundled binaries" could belong under any of the following headings:
      • In package only, Provided by both or Extra
  2. One-by-one, each bundled binary is checked for dependencies as follows:
    1. The binary is copied to /tmp.
    2. It is scanned by ldd-recursive (a bundled script).
      • ldd-recursive checks for dependencies, and dependencies of dependencies, etc.
      • Since the binary is in /tmp, ldd-recursive will only find system dependencies.
        • These could be System only or Provided by both.
      • Anything not provided by the system will be reported as "not found".
        • These could be In package only or Provided by neither.
    3. The binary is deleted from /tmp and the next one copied and checked.
  3. We compare the list of bundled binaries from (1) with the list of system libaries and missing libaries from (2) to work out the intersection of the Venn diagrams for "Package" and "System".
  4. Anything that was listed as bundled in (1), but not reported by ldd-recursive as either found or not found in (2), is "Extra".

The process takes longer than it should because find reports every file in the AppDir as an executable. This is because all files are given execute permission when runtime.c mounts the AppImage. (I suppose we could do something a bit cleverer to detect executables, such as look for magic bits or ELF headers.)

probonopd commented 5 years ago

Thanks for the detailed explanation @shoogle.

all files are given execute permission when runtime.c mounts the AppImage

Sounds like a bug?

probonopd commented 5 years ago

At AppImage build time we can run e.g., appimagelint. I wonder whether this can be used to save some data which we can check against at runtime, to me faster and more efficient at runtime.

shoogle commented 5 years ago

I think the ideal solution is to detect when the AppImage's payload application crashes (as opposed to a normal exit) and then intercept the error message and try to replace it with something human-readable. I know this is something @TheAssassin was contemplating for AppImageLauncher, but I don't know how feasible it is. It might involve source changes for downstream projects.

An easier solution might be to embed linuxdeploy's dependency checking logic into the AppImages it creates. Users would have to trigger the check manually by providing a command line option, such as --appimage-check-depends.

TheAssassin commented 5 years ago

I think the ideal solution is to detect when the AppImage's payload application crashes (as opposed to a normal exit) and then intercept the error message and try to replace it with something human-readable. I know this is something @TheAssassin was contemplating for AppImageLauncher, but I don't know how feasible it is. It might involve source changes for downstream projects.

It's doable to some extent, of course you can only provide somewhat generic error messages to the users (without having the apps in the AppImages collaborate with AIL), but it's doable.

shoogle commented 5 years ago

Sounds like a bug?

Maybe not actually. I think ISO 9660 (which MuseScore is still using until this PR gets merged) doesn't store file permissions, so runtime.c has no choice but to give everything execute permission.

I wonder whether this can be used to save some data which we can check against at runtime, to [be?] faster and more efficient at runtime.

If these are one-off checks that are called manually then slowness is not necessarily a problem. You could just do what MuseScore does and ship ldd-recursive with a few lines of Bash. It's arguably a good thing if the checking is done by different code to the bundling, as this acts as a form of CI test.

However, if you wanted to do something in advance, you could store a dependency tree for all bundled binaries. The main executable is the root of the tree, and each branch is a bundled library. The leaf nodes are libraries with no dependencies, or the word "system" to indicate that the libary is on the exclude list. Performing the run-time check basically completes the tree by looking to see whether those system libraries are present, and whether their dependencies (which may be different on each system) are present, etc. Other executables would have their own tree.

probonopd commented 5 years ago

ISO 9660

Yes, let's hope the type 2 patch gets merged soon ;-)

probonopd commented 5 years ago

Possibly the best thing to do would be to try launching the payload application, and only if this fails, start a (time-consuming) "investigation" of what went wrong.