probonopd / linuxdeployqt

Makes Linux applications self-contained by copying in the libraries and plugins that the application uses, and optionally generates an AppImage. Can be used for Qt and other applications
Other
2.22k stars 411 forks source link

Bundle apps' dynamic dependencies #237

Open TheAssassin opened 6 years ago

TheAssassin commented 6 years ago

A long time problem we've been looking for a solution for is how to bundle libraries opened by dlopen() calls.

A naive attempt would be to just run the app via strace and look for libraries opened by the application. However, apps depending on e.g., system components (the desktop environment for example, or the sound system), or expect a parameter, and otherwise showing a help text. In all these cases, it's impossible to detect all the libraries opened by the software, and even worse, those applications might alter the system, which is undesirable, too.

Instead of executing an application with strace, I thought it should suffice to read the executable or library file, extract all strings, filter those with given patterns (^.+\.so\..+$ worked well for the beginning, we should also use ^.+\.so$), and bundle those files.

I made a first attempt with strings and grep:

> strings /usr/lib/x86_64-linux-gnu/libSDL2-2.0.so | grep -oP '^.*\.so[\.\d]*''
libasound.so.2
libm.so.6
libdl.so.2
libpulse.so.0
libsndio.so.6.1
libX11.so.6
libXext.so.6
libXcursor.so.1
libXinerama.so.1
libXi.so.6
libXrandr.so.2
libXss.so.1
libXxf86vm.so.1
libwayland-egl.so.1
libwayland-client.so.0
libwayland-cursor.so.0
libxkbcommon.so.0
libpthread.so.0
librt.so.1
libc.so.6
libSDL2-2.0.so.0
libGLESv2.so.2
libGLES_CM.so.1
libGL.so.1
libEGL.so.1
libGLESv1_CM.so.1
libmirclient.so.9
libxkbcommon.so.0
libdbus-1.so.3
libudev.so.1
libudev.so.0

The solution isn't perfect, though. Some tools might just include library names in help texts, etc., but I'd expect it to work well, given the results of a couple of libraries I tried with this method. Some libaries might not be a hard dependency, but in many cases, bundling one library too much is better than bundling too few. Also, our excludelist is pretty solid, so I wouldn't worry too much about this.

As this is a bit speculative, I would offer this functionality only when the user specifies a special parameter, e.g., -bundle-dynamic-libraries.

What do you think?

hallarempt commented 6 years ago

Or maybe just add those libraries as a parameter to -bundle-dynamic-libraries, like extra-plugins? It would also be good if those parameters could just work on every file in a certain directory, I think.

TheAssassin commented 6 years ago

Well, I'd rather have it bundle them automatically. When bundling dynamic dependencies, you need to check every library, executable, etc. for the library names. Packagers don't know much about most of those libraries in my experience, so they can't compile a complete list of libraries that need to be bundled.

hallarempt commented 6 years ago

True.

VioletGiraffe commented 5 years ago

I have an application with a bunch of dynamically loaded plugins, and I can't find a way to bundle the plugins with the application. Without plugins, my app is very limited in its functions. The plugins don't even have any special dependencies not already bundled for the main executable. Is there a way to bundle the specified .so files into AppImage?

probonopd commented 5 years ago

In case we are talking about Qt plugins,

-extra-plugins=<list>    : List of extra plugins which should be deployed,
                              separated by comma.

sounds like what you need. The plugins deployed are from the Qt installation pointed out by qmake -v. You can deploy entire plugin directories, a specific directory or a mix of both.

Usage examples:

    -extra-plugins=sqldrivers/libqmsql.so,iconengines/libqsvgicon.so
    -extra-plugins=sqldrivers,iconengines/libqsvgicon.so
    -extra-plugins=sqldrivers,iconengines,mediaservice,gamepads
VioletGiraffe commented 5 years ago

@probonopd : no, quite the opposite. They are my own plugins built from my code. A bunch of .so files located in the same dir with the main executable. My application loads them at runtime with dlopen(). At first I thought -extra-plugins is what I need, but it can't be used for bundling arbitrary non-qt shared libraries.

probonopd commented 5 years ago

In this case, copy your .so files into the AppDir (e.g., in usr/lib/) and supply them to linuxdeployqt like this:

linuxdeployqt \
  -executable=appdir/usr/lib/mylib.so \
  -executable=appdir/usr/lib/myotherlib.so \
  appdir/usr/share/*desktop -appimage

This will make sure that the extra libraries can be loaded and that their dependencies, too, will be deployed.

(Edited above to reflect the missing =.)

VioletGiraffe commented 5 years ago

@probonopd: oh, so easy! Could definitely use this bit of info in the main README. Thanks!

probonopd commented 5 years ago

Can you send a pull request for the README please? Thank you @VioletGiraffe

VioletGiraffe commented 5 years ago

@probonopd: I would, as soon as I figure out how it all actually works. I don't have an AppDir, I'm using linuxdeployqt with -appimage flag to create a bundle image directly with no extra hassle. I have a binary, let's call it AppBinary, I have a .desktop file that refers to it, and I have a bunch of .so files in the same directory with the main binary. So I'm doing the following, with the current dir being the main executable directory:

~/Downloads/linuxdeployqt -executable libshared1.so -executable libshared2.so install/app.desktop -appimage

And I'm getting an error:

ERROR: Unknown argument: "libshared.so"

Can you please advise?

probonopd commented 5 years ago

Sorry I missed the =.

Arrange your files like this:

appdir/usr/bin/AppBinary
appdir/usr/share/app.desktop
appdir/usr/share/icons/hicolor/512x512/app.png
appdir/usr/lib/libshared1.so
appdir/usr/lib/libshared2.so

Ideally,

qmake CONFIG+=release PREFIX=/usr
make -j$(nproc)
make INSTALL_ROOT=appdir -j$(nproc) install

does this for you. This structure is called an "FHS-like" AppDir.

Then, run

linuxdeployqt \
  -executable=appdir/usr/lib/mylib.so \
  -executable=appdir/usr/lib/myotherlib.so \
  appdir/usr/share/*desktop -appimage
VioletGiraffe commented 5 years ago

Do I really need to create the AppDir structure manually? The whole reason of running linuxdeployqt -appimage is to cut down on the manual work. I already have the shared libraries right here, I'm specifying them with -executable, can't linuxdeployqt pick them up without me having to copy them elsewhere, wasting CPU time, electricity, disk space, and my time writing the scripts to do all this?

I tried this way: linuxdeployqt -executable libshared1.so -executable libshared2.so installation/share/applications/myapp.desktop -appimage Got this error:

ERROR: Unknown argument: "installation/share/applications/myapp.desktop"

probonopd commented 5 years ago

Your build script (Makefile, QMake file, CMake file) should be able to create the FHS-like structure. If it can't do that, then yes, you need to do it manually. Please see the README.md.

VioletGiraffe commented 5 years ago

I tried as you suggested (almost - I don't see the point of using /usr/ subdir, surely it can't be required?):

~/Downloads/linuxdeployqt-6-x86_64.AppImage -executable=installation/lib/libshared1.so.1.0 -executable==installation/lib/libshared2.so.1.0 installation/share/applications/myapp.desktop -appimage

Same error:

ERROR: Unknown argument: "installation/share/applications/myapp.desktop"

What's wrong?

VioletGiraffe commented 5 years ago

By the way, these dependencies are static, they are linked at compile time with the -l linker flag despite being shared libraries. I'm not loading them with dlopen(). Isn't linuxdeployqt and/or appimagetool supposed to pick them up automatically? They're not being bundled even with -bundle-everything.

probonopd commented 5 years ago

Looks like by deviating from https://github.com/probonopd/linuxdeployqt#using-linuxdeployqt-with-travis-ci you are making it harder for yourself than need be. Please just follow the example more or less 1:1. Which project are you working on? If you like, I can send you a pull request.

VioletGiraffe commented 5 years ago

@probonopd, unfortunately, it's not an open-source project. But I don't understand where I'm deviating from the manual you've linked to (and I've read it all over by now, I think)! I moved my bundle directory one level down into /usr/ in case that's required, and I specified the desktop file using *.desktop syntax. No change. What am I doing wrong??? I'm about to start pulling my hair out (and there's very little left from my previous encounters with Linux).

VioletGiraffe commented 5 years ago

New milestone: after copying shared libraries into /usr/lib/ manually and removing all -executable= from command line, I finally get an AppImage, but it prints this when I try to run it:

execv error: No such file or directory

probonopd commented 5 years ago

Can you post a full linuxdeployqt log?

VioletGiraffe commented 5 years ago

@probonopd : here's the output with verbose=3, is that what you meant? ldq.log

probonopd commented 5 years ago

Please post the log when it fails. This one succeeded.

VioletGiraffe commented 5 years ago

@probonopd : launching this appimage failed with "execv error: No such file or directory". Surely, it's not supposed to happen?

probonopd commented 5 years ago

That is not supposed to happen indeed. Can you mail me the AppImage for analysis? probono at puredarwin dot org.

VioletGiraffe commented 5 years ago

@probonopd: done, e-mail sent. Thanks in advance!

VioletGiraffe commented 5 years ago

Did you get a chance to look into it? I desperately want to bundle my application into AppImage (it seems to be the only way to deploy applications on Linux without major headache), but I need to add dynamically loaded plugins and last time I tried, we couldn't make it work.

dirkmittler commented 4 years ago

@VioletGiraffe

Hello.

I hope that I'm not overstepping my license, by posting an opinion on the problem you seem to have encountered. But I think that your problem has largely been misidentified in this thread. The question isn't so much, How can the dynamically-loaded libraries be bundled, as much as, Why isn't the program finding them?

The application can be designed in HFS mode or not so, where HFS mode is the mode, that has the large, well-structured directory-layout. The following link describes how to switch:

Not Populating? Use CMake to help.

Alternatively, a blank AppDir can first be initialized with 'linuxdeploy', so that afterward, it can be processed with 'linuxdeployqt'.

It would be perfectly possible to bundle the libraries in non-HFS mode, by creating a folder named, say, ${APPDIR}/etc, and placing the libraries there. But I think that the key to solving your problem can be found here:

I think this applies equally in HFS mode as it does in non-HFS mode.

Long story short, your program decides at run-time, where to find the libraries. How can it distinguish between paths that belong to the AppDir, and paths that belong to the host machine? The AppDir is mounted at a (SquashFS) mountpoint. The program has some way to read in the value of the environment variable ${APPDIR}. In non-HFS mode, append '/etc' and then the name of the library to that. In HFS mode, append '/usr/lib/', or, whichever other directory you placed the libs in. Or, have I misunderstood the problem again?

Just remember, in HFS mode, to give the 'linuxdeployqt' tool the '-bundle-non-qt-libs' option.

#include <dlfcn.h>
#include <QString>
#include <QByteArray>

QString libname = QString("libshared1.so");
QString libpath = QString::fromLocal8Bit(qgetenv("APPDIR")) += "/usr/lib/" += libname;

QByteArray libpath_ba = libpath.toLocal8Bit();
void *handle = dlopen(libpath_ba.data(), RTLD_LAZY);

Dirk

VioletGiraffe commented 4 years ago

@dirkmittler Hello Dirk, thank you for the insight and a bunch of useful information. Indeed, it is just as important to find the libraries as it is to deploy them into the AppImage. However, with the latest versions of linuxdeployqt the problem has gone, I can successfully bundle extra .so files by specifying -executable=. I did not have to modify the path at which the main application looks for the plugins - I'm successfully using qApp->applicationDirPath() on all platforms regardless of whether it runs from AppImage container or if I'm just debugging the application from IDE without bundling.

probonopd commented 4 years ago

This is how it is intended to work @VioletGiraffe.