Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support relocatable builds of Qt{5,6} #306

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 87 additions & 39 deletions src/appimagetool/appdirtool.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,10 @@ func AppDirDeploy(path string) {
qtVersionDetected = 4
}

qtPrefixPathRequiresPatch := true

if qtVersionDetected > 0 {
handleQt(appdir, qtVersionDetected)
qtPrefixPathRequiresPatch = handleQt(appdir, qtVersionDetected)
}

fmt.Println("")
Expand Down Expand Up @@ -311,11 +313,11 @@ func AppDirDeploy(path string) {
handleNvidia()

for _, lib := range allELFs {

deployElf(lib, appdir, err)
patchRpathsInElf(appdir, libraryLocationsInAppDir, lib)

if strings.Contains(lib, fmt.Sprintf("libQt%dCore.so.%d", qtVersionDetected, qtVersionDetected)) {
if qtPrefixPathRequiresPatch &&
strings.Contains(lib, fmt.Sprintf("libQt%dCore.so.%d", qtVersionDetected, qtVersionDetected)) {
fmt.Println("Patching Qt prefix path in " + lib)
patchQtPrfxpath(appdir, lib, libraryLocationsInAppDir, ldLinux)
}
Expand Down Expand Up @@ -511,7 +513,12 @@ func patchQtPrfxpath(appdir helpers.AppDir, lib string, libraryLocationsInAppDir
f.Seek(0, 0)
// Search from the beginning of the file
search := []byte("qt_prfxpath=")
offset := ScanFile(f, search) + int64(len(search))
prfxpathPos := ScanFile(f, search)
if prfxpathPos < 0 {
helpers.PrintError("Could not find offset for " + string(search), errors.New("no " + string(search) + " token in binary"))
os.Exit(1)
}
offset := prfxpathPos + int64(len(search))
log.Println("Offset of qt_prfxpath:", offset)
/*
What does qt_prfxpath=. actually mean on a Linux system? Where is "."?
Expand Down Expand Up @@ -1248,26 +1255,22 @@ func getCopyrightFile(path string) (string, error) {
}

// Let's see in how many lines of code we can re-implement the guts of linuxdeployqt
func handleQt(appdir helpers.AppDir, qtVersion int) {
func handleQt(appdir helpers.AppDir, qtVersion int) bool {

qtPrefixPathRequiresPatch := true

if qtVersion >= 5 {

// Actually the libQt5Core.so.5/libQt6Core.so.6 contains (always?) qt_prfxpath=... which tells us the location in which 'plugins/' is located
// When libQt5Core.so.5/libQt6Core.so.6 is built non-relocatable, it contains qt_prfxpath=...
// This tells us the path in which 'plugins/' is located

library, err := findLibrary(fmt.Sprintf("libQt%dCore.so.%d", qtVersion, qtVersion))
if err != nil {
helpers.PrintError(fmt.Sprintf("Could not find libQt%dCore.so.%d", qtVersion, qtVersion), err)
os.Exit(1)
}

f, err := os.Open(library)
defer f.Close()
if err != nil {
helpers.PrintError(fmt.Sprintf("Could not open libQt%dCore.so.%d", qtVersion, qtVersion), err)
os.Exit(1)
}

qtPrfxpath := getQtPrfxpath(f, err, qtVersion)
qtPrfxpath, qtPrefixPathRequiresPatch := getQtPrfxpath(library, qtVersion)

if qtPrfxpath == "" {
log.Println("Got empty qtPrfxpath, exiting")
Expand Down Expand Up @@ -1315,8 +1318,8 @@ func handleQt(appdir helpers.AppDir, qtVersion int) {
}
}

// platform plugin context - required for special characters - included if libQt5Gui.so.5/libQt6.Gui.so.6 is included
// similar to https://github.com/probonopd/linuxdeployqt/blob/42e51ea7c7a572a0aa1a21fc47d0f80032809d9d/tools/linuxdeployqt/shared.cpp#L1229
// platform plugin context - required for special characters - included if libQt5Gui.so.5/libQt6.Gui.so.6 is included
// similar to https://github.com/probonopd/linuxdeployqt/blob/42e51ea7c7a572a0aa1a21fc47d0f80032809d9d/tools/linuxdeployqt/shared.cpp#L1229
for _, lib := range allELFs {
if strings.HasSuffix(lib, fmt.Sprintf("libQt%dGui.so.%d", qtVersion, qtVersion)) == true {
if helpers.Exists(qtPrfxpath + "/plugins/platforminputcontexts/") {
Expand Down Expand Up @@ -1427,7 +1430,7 @@ func handleQt(appdir helpers.AppDir, qtVersion int) {
qmlImportScanners := helpers.FilesWithSuffixInDirectoryRecursive(qtPrfxpath, "qmlimportscanner")
if len(qmlImportScanners) < 1 {
log.Println("qmlimportscanner not found, skipping QML deployment") // TODO: Exit if we have qml files and qmlimportscanner is not there
return
return qtPrefixPathRequiresPatch
} else {
log.Println("Found qmlimportscanner:", qmlImportScanners[0])
}
Expand Down Expand Up @@ -1488,16 +1491,38 @@ func handleQt(appdir helpers.AppDir, qtVersion int) {
}
}
}
}

func getQtPrfxpath(f *os.File, err error, qtVersion int) string {
return qtPrefixPathRequiresPatch
}

// If the user has set $QTDIR, use that instead of the one from qt_prfxpath in the library
qtPrefixEnv := os.Getenv("QTDIR")
if qtPrefixEnv != "" {
log.Println("Using $QTDIR:", qtPrefixEnv)
return qtPrefixEnv
func getQtPrfxpath(library string, qtVersion int) (string, bool) {
// Some notes on Qt behavior:
// https://doc.qt.io/qt-5/qt-conf.html
// https://doc.qt.io/qt-6/qt-conf.html
// qt.conf can be used to override default hardcoded paths
// qt6.conf can be used when Qt6 and Qt5 are deployed side-by-side

// https://doc.qt.io/qt-5/qlibraryinfo.html
// https://doc.qt.io/qt-6/qlibraryinfo.html
// Query paths at runtime using QLibraryInfo::location ( Qt5 ) and QLibraryInfo::path ( Qt6 )

// Qt source files relevant to this:
// qtbase/cmake/QtBuildRepoHelpers.cmake -- Configures prerequisites for QT_BUILD_INTERNALS_RELOCATABLE_INSTALL_PREFIX
// qtbase/cmake/QtBuildInternalsExtra.cmake.in -- Configures QT_BUILD_INTERNALS_RELOCATABLE_INSTALL_PREFIX ( relative path to Qt%d directory for relocatable library )
// qtbase/cmake/QtQmakeHelpers.cmake -- has function qt_generate_qconfig_cpp
// qtbase/src/corelib/CMakeLists.txt -- Configures qconfig.cpp.in
// qtbase/src/corelib/global/qconfig.cpp.in -- string constant with qt_prfxpath=
// qtbase/src/corelib/global/qlibraryinfo.h -- Class that handles Qt paths
// qtbase/src/corelib/global/qlibraryinfo.cpp -- "

// TODO IDEA: Use AppRun to generate qt.conf at application start?

f, err := os.Open(library)
if err != nil {
helpers.PrintError(fmt.Sprintf("Could not open libQt%dCore.so.%d", qtVersion, qtVersion), err)
os.Exit(1)
}
defer f.Close()

f.Seek(0, 0)
// Search from the beginning of the file
Expand All @@ -1508,21 +1533,44 @@ func getQtPrfxpath(f *os.File, err error, qtVersion int) string {
// From the current location in the file, search to the next 0x00 byte
f.Seek(offset, 0)
length := ScanFile(f, search)
log.Println("Length of value of qt_prfxpath:", length)
// Now that we know where in the file the information is, go get it
f.Seek(offset, 0)
buf := make([]byte, length)
// Make a buffer that is exactly as long as the range we want to read
_, err = io.ReadFull(f, buf)
if err != nil {
helpers.PrintError("Unable to read qt_prfxpath", err)
os.Exit(1)

qt_prfxpath := ""
qtPrefixPathRequiresPatch := true

// When length is 0, Qt has been built as relocatable
if length == 0 {
qtPrefixPathRequiresPatch = false
// Directory should be in ../Qt${qtVersion}
// TODO: Check if this is true for relocatable binaries in Qt5
qt_prfxpath = filepath.Dir(library) + fmt.Sprintf("/../Qt%d", qtVersion)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not this case on Arch Linux.
I have /usr/lib/libQt6Core.so but directory on /usr/lib/qt6. And qt_prfxpath is really empty.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: Before applying this patch, it cannot work on Arch Linux either. So it is not a regression.

Copy link
Author

@kevle kevle Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for testing it, I'll see if I can fix it.

This appears to be the place where the path is configured on Arch:
https://github.com/archlinux/svntogit-packages/blob/5b5e6ab5ccc29af668f41d05ec1fb8e9f8f99595/qt6-base/repos/extra-x86_64/PKGBUILD#L48

log.Println("Using Qt prefix path:", qt_prfxpath)
} else {
log.Println("Length of value of qt_prfxpath:", length)

// Now that we know where in the file the information is, go get it
f.Seek(offset, 0)
buf := make([]byte, length)
// Make a buffer that is exactly as long as the range we want to read
_, err = io.ReadFull(f, buf)
if err != nil {
helpers.PrintError("Unable to read qt_prfxpath", err)
os.Exit(1)
}
qt_prfxpath = strings.TrimSpace(string(buf))
log.Println("qt_prfxpath:", qt_prfxpath)
if qt_prfxpath == "" {
log.Println("Could not get qt_prfxpath from", library)
}
}
qt_prfxpath := strings.TrimSpace(string(buf))
log.Println("qt_prfxpath:", qt_prfxpath)
if qt_prfxpath == "" {
log.Println("Could not get qt_prfxpath")
return ""

// If the user has set $QTDIR or $QT_ROOT_DIR, use that instead of the one from qt_prfxpath in the library
qtPrefixEnv := os.Getenv("QTDIR")
if qtPrefixEnv == "" {
qtPrefixEnv = os.Getenv("QT_ROOT_DIR")
}
if qtPrefixEnv != "" {
log.Println("Using QTDIR or QT_ROOT_DIR:", qtPrefixEnv)
qt_prfxpath = qtPrefixEnv
}

// Special case:
Expand All @@ -1546,7 +1594,7 @@ func getQtPrfxpath(f *os.File, err error, qtVersion int) string {
}
}

return qt_prfxpath
return qt_prfxpath, qtPrefixPathRequiresPatch
}

// ScanFile returns the offset of the first occurrence of a []byte in a file from the current position,
Expand Down
Loading