├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── src ├── appdeployer.go ├── bindeployer.go ├── blacklist.go ├── main.go ├── main_test.go ├── qtdeployer.go ├── qttranslations.go └── utils.go └── tests ├── TestApp ├── TestApp.pro ├── delete.svg ├── deployment.pri ├── main.cpp ├── main.qml └── qml.qrc ├── TestLib ├── TestLib.pro ├── testlib.cpp ├── testlib.h └── testlib_global.h ├── deploy_and_run.sh └── deploy_and_run_blacklist.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | dist: trusty 3 | 4 | language: go 5 | 6 | os: 7 | - linux 8 | 9 | git: 10 | depth: 3 11 | 12 | env: 13 | - QUICKBUILD=gotests 14 | - DEPLOY_TESTS=deploytests 15 | 16 | before_install: 17 | - if [ -n "$DEPLOY_TESTS" ]; then sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y; fi 18 | - if [ -n "$DEPLOY_TESTS" ]; then sudo add-apt-repository ppa:beineri/opt-qt551-trusty -y; fi 19 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get update -qq; fi 20 | 21 | install: 22 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get -y install gcc-5 g++-5; fi 23 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get install -qq qt55base; source /opt/qt55/bin/qt55-env.sh; fi 24 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get -y install binutils xpra; fi 25 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get install -y gdb; fi 26 | - if [ -n "$DEPLOY_TESTS" ]; then sudo apt-get -y install qt55base qt55quickcontrols qt55svg qt55declarative; fi 27 | - if [ -n "$DEPLOY_TESTS" ]; then sudo unlink /usr/bin/g++ && sudo ln -s /usr/bin/g++-5 /usr/bin/g++; fi 28 | - export CXX="g++-5" CC="gcc-5" 29 | 30 | before_script: 31 | - export COMPILER=g++-5 32 | - if [ -n "$DEPLOY_TESTS" ]; then g++ --version; fi 33 | - if [ -n "$DEPLOY_TESTS" ]; then wget http://ftp.de.debian.org/debian/pool/main/p/patchelf/patchelf_0.8-2_amd64.deb; fi 34 | - if [ -n "$DEPLOY_TESTS" ]; then sudo dpkg -i patchelf_0.8-2_amd64.deb; fi 35 | - ulimit -c unlimited -S # enable core dumps 36 | 37 | script: 38 | - if [ -n "$DEPLOY_TESTS" ]; then xpra start :42; fi # start the display to have it ready before needed 39 | - export DISPLAY=:42 # for the Qml app to start 40 | - cd src/ 41 | - go build -o linuxdeploy 42 | - if [ -n "$QUICKBUILD" ]; then go test -v; fi 43 | - if [ -n "$DEPLOY_TESTS" ]; then cd ../tests/TestLib; fi 44 | - if [ -n "$DEPLOY_TESTS" ]; then qmake "CONFIG+=debug" TestLib.pro; fi 45 | - if [ -n "$DEPLOY_TESTS" ]; then make; fi 46 | - if [ -n "$DEPLOY_TESTS" ]; then cd ../TestApp; fi 47 | - if [ -n "$DEPLOY_TESTS" ]; then qmake "CONFIG+=debug" TestApp.pro; fi 48 | - if [ -n "$DEPLOY_TESTS" ]; then make; fi 49 | - if [ -n "$DEPLOY_TESTS" ]; then cd ../../; fi 50 | - if [ -n "$DEPLOY_TESTS" ]; then tests/deploy_and_run.sh; fi 51 | - if [ -n "$DEPLOY_TESTS" ]; then tests/deploy_and_run_blacklist.sh; fi 52 | 53 | after_success: 54 | - if [ -n "$DEPLOY_TESTS" ]; then cat src/linuxdeploy.log; fi 55 | - if [ -n "$DEPLOY_TESTS" ]; then cat src/linuxdeploy_blacklist.log; fi 56 | 57 | after_failure: 58 | - if [ -n "$DEPLOY_TESTS" ]; then cat src/linuxdeploy.log; fi 59 | - if [ -n "$DEPLOY_TESTS" ]; then cat src/linuxdeploy_blacklist.log; fi 60 | - if [ -n "$DEPLOY_TESTS" ]; then cd src/TestApp.AppDir1; fi 61 | - if [ -n "$DEPLOY_TESTS" ]; then for i in $(find ./ -maxdepth 1 -name 'core*' -print); do gdb $(pwd)/TestApp core* -ex "thread apply all bt" -ex "set pagination 0" -batch; done; fi 62 | - if [ -n "$DEPLOY_TESTS" ]; then cd ../TestApp.AppDir2; fi 63 | - if [ -n "$DEPLOY_TESTS" ]; then for i in $(find ./ -maxdepth 1 -name 'core*' -print); do gdb $(pwd)/TestApp core* -ex "thread apply all bt" -ex "set pagination 0" -batch; done; fi 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Basic architecture overview 2 | 3 | The whole deployment process consists of several pipelines: libraries inspection, files copying, RPATH patching, binaries stripping, special Qt libraries handling and others. 4 | Each pipeline is represented by an appropriate `channel` which is being handled in it's own goroutine. Libraries and files are passed over from one pipeline to the other after being processed (see details on chart below). 5 | 6 | `AppDeployer` is a top-level entity to orchestrate the whole deployment. It kicks-off the process by calling `processMainExe()` and starting processing of all other pipelines like `processCopyTasks()`, `processStripTasks()` and others. 7 | 8 | Another important place is deploying all Qt dependencies. `QtDeployer` as a part of `AppDeployer` is responsible for this. It handles plugins, qml imports and libraries separately in the `processQtLibTasks()` and `deployQmlImports()`. 9 | Also `libQt5Core` needs to have hardcoded paths patched which is implemented in the `patchQtCore()` method. Qt environment is derived from the `qmake` output which is parsed in the beginning if Qt is in the dependencies or specified via `-qmake` param. 10 | 11 | AppImage format is supported in a way of creating `AppRun` link, `.DirIcon` file and correct `.desktop` file (icon path without extension, Exec command and others). This is all handled in the `AppDeployer` respective methods which are called after copying the main exe file. 12 | 13 | ## Pipelines 14 | 15 | +-------+ +--------+ +---------+ +---------+ 16 | ---> | LDD +---> | Copy +---> | RPATH +---> | Strip | 17 | +---+---+ +---+----+ +----+----+ +---------+ 18 | ^ ^ ^ 19 | | | | 20 | | v | 21 | | +---+----+ | 22 | +---------+ Qt +----------+ 23 | +---^----+ 24 | | 25 | | 26 | +--------v--------+ 27 | | Qt dependencies | 28 | +-----------------+ 29 | 30 | So initially main exe is fed to [**LDD** pipeline] which extracts the dependencies (and dependencies of dependencies) and passes them to the [**Copy** pipeline]. The latter copies files from their origin to the deployment directory in a proper manner (e.g. libs to `lib/` directory). Ordinary libraries are then passed to [**RPATH** pipeline] and Qt libraries are passed to [**Qt** pipeline]. 31 | 32 | [**RPATH** pipeline] fixes `RPATH` for libs to be `$ORIGIN:$ORIGIN/path/to/libs` and passes files over to [**Strip** pipeline] if needed (if `-strip` was in the cmdline options). [**Qt** pipeline] inspects required [**Qt dependencies**] for each library plus Qml imports and Qt Translations. These dependencies are processed in a way that ordinary files are being passed back to [**Copy** pipeline], new libraries back to the [**LDD** pipeline] and processed libraries - to the [**RPATH** pipeline]. 33 | 34 | After all pipelines are done, blacklisted libraries are removed from the deployment destination. 35 | 36 | ## How to contribute 37 | 38 | - [Fork](http://help.github.com/forking/) linuxdeploy repository on GitHub 39 | - Clone your fork locally 40 | - Configure the upstream repo (`git remote add upstream git@github.com:Ribtoks/linuxdeploy.git`) 41 | - Create local branch (`git checkout -b your_feature`) 42 | - Work on your feature 43 | - Build and Run tests (`go tests -v`) 44 | - Push the branch to GitHub (`git push origin your_feature`) 45 | - Send a [pull request](https://help.github.com/articles/using-pull-requests) on GitHub 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Taras Kushnir 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linuxdeploy 2 | A tool for deploying standalone Linux applications 3 | 4 | [![Build Status](https://travis-ci.org/ribtoks/linuxdeploy.svg?branch=master)](https://travis-ci.org/ribtoks/linuxdeploy) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8cb388236da9407095fe73130a1ffa4b)](https://www.codacy.com/app/ribtoks/linuxdeploy) 6 | 7 | # Description 8 | **linuxdeploy** inspects the executable file and deploys it alongside with all the dependencies to a specified location. Afterwards RPATH is fixed correctly so the deployed executable only uses deployed libraries. Main use-case for this tool is _deploying Qt applications on Linux without pain in the format of AppImage_, however your mileage may vary. 9 | 10 | Find more developers documentation in the [CONTRIBUTING.md](https://github.com/Ribtoks/linuxdeploy/blob/master/CONTRIBUTING.md) 11 | 12 | # Build 13 | 14 | As easy as: 15 | 16 | cd src 17 | go build -o linuxdeploy 18 | 19 | # Dependencies 20 | 21 | You have to have in your `PATH`: 22 | 23 | * `ldd` (checking dso dependencies) 24 | * [patchelf](https://anonscm.debian.org/cgit/collab-maint/patchelf.git/) (patching `RPATH` in binaries) 25 | * `strip` (optionally to remove debug symbols from binaries) 26 | 27 | # Usage 28 | 29 | ## Simple usage 30 | 31 | Most simple usage of this tool: 32 | 33 | linuxdeploy -exe /path/to/myexe -appdir myexe.AppDir -icon /path/to/icon 34 | -gen-desktop -default-blacklist -out appimage 35 | 36 | appimagetool --verbose -n myexe.AppDir "myexe.AppImage" 37 | 38 | These commands will deploy application `myexe` and it's dependencies to the directory `./myexe.AppDir/` packing in the AppImage-compatible structure. Afterwards AppImage is generated with an [AppImageTool](https://github.com/probonopd/AppImageKit). 39 | 40 | ## Deploying Qt 41 | 42 | **linuxdeploy** is capable of deploying all Qt's dependencies of your app: libraries, private widgets, QML imports and translations. Optionally you can specify path to the `qmake` executable and **linuxdeploy** will derive Qt Environment from it. You can specify additional directories to search for qml imports using a repeatable `-qmldir` switch. 43 | 44 | ## Other features 45 | 46 | Usually when creating AppImage you don't need to deploy _all_ the libraries (like _libstdc++_ or _libdbus_). **linuxdeploy** supports ignore list as a command-line parameter `-blacklist`. It is path to a file with an ignore per line where ignore is a prefix of the library to skip (e.g. if you need to ignore _libstdc++.so.6_ you can have a line _libstdc++_ in the blacklist file). Also you have a default blacklist which can be checked out in the `src/blacklist.go` file and can be added with `-default-blacklist` cmdline switch. 47 | 48 | **linuxdeploy** can also generate a desktop file in the deployment directory. Also it will fill-in information about icon and AppRun link in case you're deploying AppImage. 49 | 50 | Every binary deployed (original exe and dependent libs) can be stripped if you specify cmdline switch `-strip`. 51 | 52 | ## Command line switches: 53 | 54 | -exe string 55 | Path to the executable to deploy 56 | -appdir string 57 | Path to the destination deployment directory or AppDir (if 'type' is appimage) 58 | -libs value 59 | Additional libraries search paths (repeatable) 60 | -qmake string 61 | Path to qmake 62 | -qmldir value 63 | Additional QML imports dir (repeatable) 64 | -blacklist string 65 | Path to the additional libraries blacklist file (default "libs.blacklist") 66 | -default-blacklist 67 | Add default blacklist 68 | -gen-desktop 69 | Generate desktop file 70 | -icon string 71 | Path the exe's icon (used for desktop file) 72 | -log string 73 | Path to the logfile (default "linuxdeploy.log") 74 | -out string 75 | Type of the generated output (default "appimage") 76 | -overwrite 77 | Overwrite output if present 78 | -stdout 79 | Log to stdout and to logfile 80 | -strip 81 | Run strip on binaries 82 | 83 | # Known issues 84 | 85 | The only working `patchelf` right now is from the [Debian's repository](https://anonscm.debian.org/cgit/collab-maint/patchelf.git/). [Vanilla patchelf](https://github.com/NixOS/patchelf) damages `libQt5Core.so` library. 86 | 87 | # Disclaimer 88 | 89 | I wrote this tool because [linuxdeployqt](https://github.com/probonopd/linuxdeployqt/) ~was too buggy for me~ did not work well for me. Now this implementation successfully deploys [more or less complex desktop Qt/Qml app](https://github.com/ribtoks/xpiks) and works a lot faster then the former. 90 | 91 | Pull Requests and feedback are more than welcome! Please check out [CONTRIBUTING.md](https://github.com/Ribtoks/linuxdeploy/blob/master/CONTRIBUTING.md) for more details and developers documentation on the internals. 92 | -------------------------------------------------------------------------------- /src/appdeployer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "os" 20 | "strings" 21 | "sync" 22 | "path/filepath" 23 | "fmt" 24 | "bufio" 25 | ) 26 | 27 | const ( 28 | LDD_DEPENDENCY_FLAG Bitmask = 1 << iota // check ldd deps for given item 29 | FIX_RPATH_FLAG // fix rpath for qt-related libs/plugins 30 | DEPLOY_ONLY_LIBRARIES_FLAG 31 | ) 32 | 33 | const ( 34 | LDD_AND_RPATH_FLAG = LDD_DEPENDENCY_FLAG | FIX_RPATH_FLAG 35 | LIBRARIES_AND_RPATH_FLAG = FIX_RPATH_FLAG | DEPLOY_ONLY_LIBRARIES_FLAG 36 | ) 37 | 38 | type DeployRequest struct { 39 | sourcePath string // relative or absolute path of file to process 40 | sourceRoot string // if empty then sourcePath is absolute path 41 | targetPath string // target *relative* path 42 | flags Bitmask // deployment flags 43 | } 44 | 45 | func (dp *DeployRequest) FullPath() string { 46 | if len(dp.sourceRoot) == 0 { 47 | return dp.sourcePath 48 | } else { 49 | return filepath.Join(dp.sourceRoot, dp.sourcePath) 50 | } 51 | } 52 | 53 | func (dp *DeployRequest) Basename() string { 54 | return filepath.Base(dp.sourcePath) 55 | } 56 | 57 | func (dp *DeployRequest) SourceDir() string { 58 | return filepath.Dir(dp.sourcePath) 59 | } 60 | 61 | func (dp *DeployRequest) RequiresRPathFix() bool { 62 | return dp.flags.HasFlag(FIX_RPATH_FLAG) 63 | } 64 | 65 | func (dp *DeployRequest) IsLddDependency() bool { 66 | return dp.flags.HasFlag(LDD_DEPENDENCY_FLAG) 67 | } 68 | 69 | type AppDeployer struct { 70 | waitGroup sync.WaitGroup 71 | processedLibs map[string]bool 72 | 73 | libsChannel chan *DeployRequest 74 | copyChannel chan *DeployRequest 75 | stripChannel chan string 76 | rpathChannel chan string 77 | qtChannel chan string 78 | 79 | qtDeployer *QtDeployer 80 | additionalLibPaths []string 81 | destinationRoot string 82 | targetExePath string 83 | destinationExePath string 84 | iconFilename string 85 | } 86 | 87 | func (ad *AppDeployer) DeployApp() { 88 | if err := ad.qtDeployer.queryQtEnv(); err != nil { 89 | log.Println(err) 90 | } 91 | 92 | ad.waitGroup.Add(1) 93 | go ad.processMainExe() 94 | 95 | go ad.processCopyTasks() 96 | go ad.processFixRPathTasks() 97 | go ad.processStripTasks() 98 | go ad.processQtLibTasks() 99 | 100 | blacklist := generateLibsBlacklist() 101 | 102 | log.Printf("Waiting for tasks processing to finish") 103 | ad.waitGroup.Wait() 104 | log.Printf("Tasks have been processed") 105 | 106 | close(ad.libsChannel) 107 | close(ad.copyChannel) 108 | close(ad.qtChannel) 109 | close(ad.rpathChannel) 110 | close(ad.stripChannel) 111 | 112 | var wg sync.WaitGroup 113 | wg.Add(1) 114 | go ad.deployQtTranslations(filepath.Join(ad.destinationRoot, "translations"), &wg) 115 | 116 | err := cleanupBlacklistedLibs(ad.LibsPath(), blacklist) 117 | if err != nil { log.Printf("Error while removing blacklisted libs: %v", err) } 118 | 119 | wg.Wait() 120 | } 121 | 122 | func (ad *AppDeployer) LibsPath() string { 123 | return filepath.Join(ad.destinationRoot, "lib") 124 | } 125 | 126 | func (ad *AppDeployer) addLibTask(sourceRoot, sourcePath, targetPath string, flags Bitmask) { 127 | ad.waitGroup.Add(1) 128 | go func() { 129 | ad.libsChannel <- &DeployRequest{ 130 | sourceRoot: sourceRoot, 131 | sourcePath: sourcePath, 132 | targetPath: targetPath, 133 | flags: flags, 134 | } 135 | }() 136 | } 137 | 138 | func (ad *AppDeployer) addCopyTask(sourceRoot, sourcePath, targetPath string, flags Bitmask) { 139 | ad.waitGroup.Add(1) 140 | go func() { 141 | ad.copyChannel <- &DeployRequest{ 142 | sourceRoot: sourceRoot, 143 | sourcePath: sourcePath, 144 | targetPath: targetPath, 145 | flags: flags, 146 | } 147 | }() 148 | } 149 | 150 | func (ad *AppDeployer) accountLibrary(libpath string) { 151 | log.Printf("Processed library %v", libpath) 152 | ad.processedLibs[libpath] = true 153 | } 154 | 155 | func (ad *AppDeployer) isLibraryDeployed(libpath string) bool { 156 | _, ok := ad.processedLibs[libpath] 157 | return ok 158 | } 159 | 160 | func (ad *AppDeployer) processMainExe() { 161 | defer ad.waitGroup.Done() 162 | 163 | go ad.copyMainExe() 164 | 165 | dependencies, err := ad.findLddDependencies(filepath.Base(ad.targetExePath), ad.targetExePath) 166 | if err != nil { log.Fatal(err) } 167 | 168 | for _, dependPath := range dependencies { 169 | if !ad.isLibraryDeployed(dependPath) { 170 | ad.addLibTask("", dependPath, "lib", LDD_AND_RPATH_FLAG) 171 | } else { 172 | log.Printf("Dependency seems to be processed: %v", dependPath) 173 | } 174 | } 175 | 176 | go ad.processLibTasks() 177 | 178 | log.Println("Main exe processing finished") 179 | } 180 | 181 | func (ad *AppDeployer) copyMainExe() { 182 | destinationPath := filepath.Join(ad.destinationRoot, filepath.Base(ad.targetExePath)) 183 | ensureDirExists(destinationPath) 184 | 185 | err := copyFile(ad.targetExePath, destinationPath) 186 | if err != nil { 187 | log.Fatal("Error while copying main exe [%v] to [%v]: %v", ad.targetExePath, destinationPath, err) 188 | } 189 | 190 | ad.destinationExePath = destinationPath 191 | log.Printf("Destination path of main exe is %v", destinationPath) 192 | 193 | ad.addFixRPathTask(destinationPath) 194 | 195 | if generateAppImg() { 196 | ad.createAppLink() 197 | } 198 | 199 | ad.copyIcon() 200 | 201 | if *generateDesktopFlag { 202 | ad.generateDesktopFile() 203 | } 204 | } 205 | 206 | func (ad *AppDeployer) createAppLink() { 207 | appname := filepath.Base(ad.destinationExePath) 208 | symlinkPath := filepath.Join(ad.destinationRoot, "AppRun") 209 | err := os.Symlink(appname, symlinkPath) 210 | if err != nil { 211 | log.Printf("Error creating symlink: %v", err) 212 | } 213 | } 214 | 215 | func (ad *AppDeployer) copyIcon() { 216 | if len(*iconPathFlag) == 0 { return } 217 | 218 | if _, err := os.Stat(*iconPathFlag); err != nil { 219 | log.Printf("Cannot process icon %v: %v", *iconPathFlag, err) 220 | return 221 | } 222 | 223 | iconFilename := filepath.Base(*iconPathFlag) 224 | iconDestinationPath := filepath.Join(ad.destinationRoot, iconFilename) 225 | err := copyFile(*iconPathFlag, iconDestinationPath) 226 | if err != nil { 227 | log.Printf("Error while copying icon %v", err) 228 | } 229 | 230 | if generateAppImg() { 231 | // copy icon as .DirIcon too 232 | err := copyFile(*iconPathFlag, filepath.Join(ad.destinationRoot, ".DirIcon")) 233 | if err != nil { 234 | log.Printf("Error while creating AppDir icon %v", err) 235 | } 236 | } 237 | 238 | ad.iconFilename = iconFilename 239 | } 240 | 241 | func (ad *AppDeployer) generateDesktopFile() { 242 | exeFilename := filepath.Base(ad.destinationExePath) 243 | desktopFilepath := filepath.Join(ad.destinationRoot, fmt.Sprintf("%s.desktop", exeFilename)) 244 | 245 | desktopFile, err := os.OpenFile(desktopFilepath, os.O_CREATE | os.O_RDWR | os.O_TRUNC, 0777) 246 | if err != nil { 247 | log.Printf("Failed to create desktop file: %v", err) 248 | return 249 | } 250 | 251 | writer := bufio.NewWriter(desktopFile) 252 | defer desktopFile.Close() 253 | 254 | fmt.Fprintln(writer, "[Desktop Entry]") 255 | fmt.Fprintln(writer, "Type=Application") 256 | fmt.Fprintf(writer, "Name=%s\n", exeFilename) 257 | 258 | if generateAppImg() { 259 | fmt.Fprintln(writer, "Exec=./AppRun %F") 260 | if len(ad.iconFilename) > 0 { 261 | extensionStartIndex := strings.LastIndex(ad.iconFilename, ".") 262 | iconBasename := ad.iconFilename[:extensionStartIndex] 263 | fmt.Fprintf(writer, "Icon=%s\n", iconBasename) 264 | } 265 | } else { 266 | fmt.Fprintf(writer, "Exec=%s\n", exeFilename) 267 | if len(ad.iconFilename) > 0 { 268 | fmt.Fprintf(writer, "Icon=%s\n", ad.iconFilename) 269 | } 270 | } 271 | 272 | fmt.Fprintln(writer, "Terminal=false") 273 | fmt.Fprintln(writer, "StartupNotify=true") 274 | fmt.Fprintln(writer, "Encoding=UTF-8") 275 | 276 | writer.Flush() 277 | 278 | log.Println("Desktop file generated") 279 | } 280 | 281 | func (ad *AppDeployer) addFixRPathTask(fullpath string) { 282 | ad.waitGroup.Add(1) 283 | go func() { 284 | ad.rpathChannel <- fullpath 285 | }() 286 | } 287 | 288 | func (ad *AppDeployer) addQtLibTask(fullpath string) { 289 | if !ad.qtDeployer.qtEnvironmentSet { 290 | log.Println("Qt environment is not set!") 291 | return 292 | } 293 | 294 | ad.waitGroup.Add(1) 295 | go func() { 296 | ad.qtChannel <- fullpath 297 | }() 298 | } 299 | 300 | // copies everything without inspection 301 | func (ad *AppDeployer) copyRecursively(sourceRoot, sourcePath, targetPath string) error { 302 | // rescue agains premature finish of the main loop 303 | ad.waitGroup.Add(1) 304 | defer ad.waitGroup.Done() 305 | 306 | var emptyFlags Bitmask = 0 307 | 308 | rootpath := filepath.Join(sourceRoot, sourcePath) 309 | log.Printf("Copying recursively %v into %v", rootpath, targetPath) 310 | 311 | err := filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error { 312 | if err != nil { 313 | return err 314 | } 315 | 316 | if !info.Mode().IsRegular() { 317 | return nil 318 | } 319 | 320 | relativePath, err := filepath.Rel(sourceRoot, path) 321 | if err != nil { 322 | log.Println(err) 323 | } 324 | 325 | ad.addCopyTask(sourceRoot, relativePath, targetPath, emptyFlags) 326 | 327 | return nil 328 | }) 329 | 330 | return err 331 | } 332 | 333 | // inspects libraries for dependencies and copies other files 334 | func (ad *AppDeployer) deployRecursively(sourceRoot, sourcePath, targetPath string, flags Bitmask) error { 335 | // rescue agains premature finish of the main loop 336 | ad.waitGroup.Add(1) 337 | defer ad.waitGroup.Done() 338 | 339 | rootpath := filepath.Join(sourceRoot, sourcePath) 340 | log.Printf("Deploying recursively %v in %v to %v", sourcePath, sourceRoot, targetPath) 341 | 342 | onlyLibraries := flags.HasFlag(DEPLOY_ONLY_LIBRARIES_FLAG) 343 | var emptyFlags Bitmask = 0 344 | 345 | err := filepath.Walk(rootpath, func(path string, info os.FileInfo, err error) error { 346 | if err != nil { 347 | return err 348 | } 349 | 350 | if !info.Mode().IsRegular() { 351 | return nil 352 | } 353 | 354 | basename := filepath.Base(path) 355 | isLibrary := strings.HasPrefix(basename, "lib") && strings.Contains(basename, ".so") 356 | 357 | if !isLibrary && onlyLibraries { 358 | return nil 359 | } 360 | 361 | relativePath, err := filepath.Rel(sourceRoot, path) 362 | if err != nil { 363 | log.Println(err) 364 | } 365 | 366 | if isLibrary { 367 | ad.addLibTask(sourceRoot, relativePath, targetPath, flags | LDD_DEPENDENCY_FLAG) 368 | } else { 369 | ad.addCopyTask(sourceRoot, relativePath, targetPath, emptyFlags) 370 | } 371 | 372 | return nil 373 | }) 374 | 375 | return err 376 | } 377 | -------------------------------------------------------------------------------- /src/bindeployer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "os/exec" 20 | "path/filepath" 21 | "strings" 22 | "os" 23 | "fmt" 24 | ) 25 | 26 | func (ad *AppDeployer) processLibTasks() { 27 | if _, err := exec.LookPath("ldd"); err != nil { 28 | log.Fatal("ldd cannot be found!") 29 | } 30 | 31 | for request := range ad.libsChannel { 32 | ad.processLibTask(request) 33 | ad.waitGroup.Done() 34 | } 35 | 36 | log.Println("Libraries processing finished") 37 | } 38 | 39 | func (ad *AppDeployer) processLibTask(request *DeployRequest) { 40 | libpath := request.FullPath() 41 | 42 | if ad.canSkipLibrary(libpath) { 43 | log.Printf("Skipping library: %v", libpath) 44 | return 45 | } 46 | 47 | log.Printf("Processing library: %v", libpath) 48 | 49 | dependencies, err := ad.findLddDependencies(request.Basename(), libpath) 50 | if err != nil { 51 | log.Printf("Error while dependency check for %v: %v", libpath, err) 52 | return 53 | } 54 | 55 | ad.accountLibrary(libpath) 56 | 57 | ad.waitGroup.Add(1) 58 | go func(copyRequest *DeployRequest) { 59 | ad.copyChannel <- copyRequest 60 | }(request) 61 | 62 | flags := request.flags 63 | // fix rpath of all the libs 64 | //flags.ClearFlag(FIX_RPATH_FLAG) 65 | flags.AddFlag(LDD_DEPENDENCY_FLAG) 66 | 67 | for _, dependPath := range dependencies { 68 | if !ad.isLibraryDeployed(dependPath) { 69 | ad.addLibTask("", dependPath, "lib", flags) 70 | } 71 | } 72 | } 73 | 74 | func (ad *AppDeployer) canSkipLibrary(libpath string) bool { 75 | canSkip := false 76 | if strings.HasPrefix(libpath, "linux-vdso.so") { 77 | canSkip = true 78 | } else if ad.isLibraryDeployed(libpath) { 79 | canSkip = true 80 | } 81 | 82 | return canSkip 83 | } 84 | 85 | func (ad *AppDeployer) findLddDependencies(basename, filepath string) ([]string, error) { 86 | log.Printf("Inspecting %v", filepath) 87 | 88 | out, err := exec.Command("ldd", filepath).Output() 89 | if err != nil { return nil, err } 90 | 91 | dependencies := make([]string, 0, 10) 92 | 93 | output := string(out) 94 | lines := strings.Split(output, "\n") 95 | for _, line := range lines { 96 | line = strings.TrimSpace(line) 97 | libname, libpath, err := parseLddOutputLine(line) 98 | 99 | if err != nil { 100 | log.Printf("Cannot parse ldd line: %v", line) 101 | continue 102 | } 103 | 104 | if len(libpath) == 0 { 105 | libpath = ad.resolveLibrary(libname) 106 | } 107 | 108 | log.Printf("[%v]: depends on %v from ldd [%v]", basename, libpath, line) 109 | dependencies = append(dependencies, libpath) 110 | } 111 | 112 | return dependencies, nil 113 | } 114 | 115 | func (ad *AppDeployer) addAdditionalLibPath(libpath string) { 116 | log.Printf("Adding addition libpath: %v", libpath) 117 | foundPath := libpath 118 | var err error 119 | 120 | if !filepath.IsAbs(foundPath) { 121 | if foundPath, err = filepath.Abs(foundPath); err == nil { 122 | log.Printf("Trying to resolve libpath to: %v", foundPath) 123 | 124 | if _, err = os.Stat(foundPath); os.IsNotExist(err) { 125 | exeDir := filepath.Dir(ad.targetExePath) 126 | foundPath = filepath.Join(exeDir, libpath) 127 | log.Printf("Trying to resolve libpath to: %v", foundPath) 128 | } 129 | } 130 | } 131 | 132 | if _, err := os.Stat(foundPath); os.IsNotExist(err) { 133 | log.Printf("Cannot find library path: %v", foundPath) 134 | return 135 | } 136 | 137 | log.Printf("Resolved additional libpath to: %v", foundPath) 138 | ad.additionalLibPaths = append(ad.additionalLibPaths, foundPath) 139 | } 140 | 141 | func (ad *AppDeployer) resolveLibrary(libname string) (foundPath string) { 142 | foundPath = libname 143 | 144 | for _, extraLibPath := range ad.additionalLibPaths { 145 | possiblePath := filepath.Join(extraLibPath, libname) 146 | 147 | if _, err := os.Stat(possiblePath); err == nil { 148 | foundPath = possiblePath 149 | break 150 | } 151 | } 152 | 153 | log.Printf("Resolving library %v to %v", libname, foundPath) 154 | return foundPath 155 | } 156 | 157 | func (ad *AppDeployer) processCopyTasks() { 158 | copiedFiles := make(map[string]bool) 159 | 160 | for copyRequest := range ad.copyChannel { 161 | ad.processCopyTask(copiedFiles, copyRequest) 162 | ad.waitGroup.Done() 163 | } 164 | 165 | log.Printf("Copy tasks processing finished") 166 | } 167 | 168 | func (ad *AppDeployer) processCopyTask(copiedFiles map[string]bool, copyRequest *DeployRequest) { 169 | var destinationPath, destinationPrefix string 170 | 171 | if len(copyRequest.sourceRoot) == 0 { 172 | // absolute path 173 | destinationPrefix = copyRequest.targetPath 174 | } else { 175 | destinationPrefix = filepath.Join(copyRequest.targetPath, copyRequest.SourceDir()) 176 | } 177 | 178 | sourcePath := copyRequest.FullPath() 179 | destinationPath = filepath.Join(ad.destinationRoot, destinationPrefix, filepath.Base(copyRequest.sourcePath)) 180 | 181 | if _, ok := copiedFiles[destinationPath]; ok { 182 | log.Printf("File %v has already been copied", sourcePath) 183 | return 184 | } 185 | 186 | ensureDirExists(destinationPath) 187 | err := copyFile(sourcePath, destinationPath) 188 | 189 | if err != nil { 190 | log.Printf("Error while copying [%v] to [%v]: %v", sourcePath, destinationPath, err) 191 | return 192 | } 193 | 194 | copiedFiles[destinationPath] = true 195 | log.Printf("Copied [%v] to [%v]", sourcePath, destinationPath) 196 | isQtLibrary := false 197 | 198 | if copyRequest.IsLddDependency() { 199 | libraryBasename := filepath.Base(destinationPath) 200 | libname := strings.ToLower(libraryBasename) 201 | 202 | if strings.HasPrefix(libname, "libqt") { 203 | ad.addQtLibTask(destinationPath) 204 | isQtLibrary = true 205 | } 206 | } 207 | 208 | if !isQtLibrary && copyRequest.RequiresRPathFix() { 209 | ad.addFixRPathTask(destinationPath) 210 | } 211 | } 212 | 213 | func (ad *AppDeployer) processFixRPathTasks() { 214 | patchelfAvailable := true 215 | 216 | if _, err := exec.LookPath("patchelf"); err != nil { 217 | log.Printf("Patchelf cannot be found!") 218 | patchelfAvailable = false 219 | } 220 | 221 | destinationRoot := ad.destinationRoot 222 | fixedFiles := make(map[string]bool) 223 | 224 | for fullpath := range ad.rpathChannel { 225 | if patchelfAvailable { 226 | if _, ok := fixedFiles[fullpath]; !ok { 227 | fixRPath(fullpath, destinationRoot) 228 | fixedFiles[fullpath] = true 229 | } else { 230 | log.Printf("RPATH has been already fixed for %v", fullpath) 231 | } 232 | } 233 | 234 | ad.addStripTask(fullpath) 235 | 236 | ad.waitGroup.Done() 237 | } 238 | 239 | log.Printf("RPath change requests processing finished") 240 | } 241 | 242 | func fixRPath(fullpath, destinationRoot string) { 243 | libdir := filepath.Dir(fullpath) 244 | relativePath, err := filepath.Rel(libdir, destinationRoot) 245 | if err != nil { 246 | log.Println(err) 247 | return 248 | } 249 | 250 | rpath := fmt.Sprintf("$ORIGIN:$ORIGIN/%s/lib/", relativePath) 251 | log.Printf("Changing RPATH for %v to %v", fullpath, rpath) 252 | 253 | cmd := exec.Command("patchelf", "--set-rpath", rpath, fullpath) 254 | if err = cmd.Run(); err != nil { 255 | log.Println(err) 256 | } 257 | } 258 | 259 | func (ad *AppDeployer) addStripTask(fullpath string) { 260 | if *stripFlag { 261 | ad.waitGroup.Add(1) 262 | go func() { 263 | ad.stripChannel <- fullpath 264 | }() 265 | } 266 | } 267 | 268 | func (ad *AppDeployer) processStripTasks() { 269 | stripAvailable := true 270 | 271 | if _, err := exec.LookPath("strip"); err != nil { 272 | log.Printf("Strip cannot be found!") 273 | stripAvailable = false 274 | } 275 | 276 | strippedBinaries := make(map[string]bool) 277 | 278 | for fullpath := range ad.stripChannel { 279 | if stripAvailable { 280 | if _, ok := strippedBinaries[fullpath]; !ok { 281 | if err := stripBinary(fullpath); err == nil { 282 | strippedBinaries[fullpath] = true 283 | } 284 | } else { 285 | log.Printf("%v has been already stripped", fullpath) 286 | } 287 | } 288 | 289 | ad.waitGroup.Done() 290 | } 291 | 292 | log.Printf("Strip requests processing finished") 293 | } 294 | 295 | func stripBinary(fullpath string) error { 296 | log.Printf("Running strip on %v", fullpath) 297 | 298 | out, err := exec.Command("strip", "--strip-debug", "--verbose", fullpath).CombinedOutput() 299 | if err != nil { 300 | log.Printf("Error while stripping %v: %v", fullpath, out) 301 | } else { 302 | log.Printf("Stripped %v: %v", fullpath, out); 303 | } 304 | 305 | return err 306 | } 307 | -------------------------------------------------------------------------------- /src/blacklist.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "bufio" 20 | "os" 21 | "strings" 22 | "path/filepath" 23 | ) 24 | 25 | func DefaultBlacklist() []string { 26 | blacklist := []string { 27 | "libcom_err.so", 28 | "libcrypt.so", 29 | "libdl.so", 30 | "libexpat.so", 31 | "libfontconfig.so", 32 | "libgcc_s.so", 33 | "libglib-2.0.so", 34 | "libgpg-error.so", 35 | "libgssapi_krb5.so", 36 | "libgssapi.so", 37 | "libhcrypto.so", 38 | "libheimbase.so", 39 | "libheimntlm.so", 40 | "libhx509.so", 41 | "libICE.so", 42 | "libidn.so", 43 | "libk5crypto.so", 44 | "libkeyutils.so", 45 | "libkrb5.so", 46 | "libkrb5.so", 47 | "libkrb5support.so", 48 | // "liblber-2.4.so.2", # needed for debian wheezy 49 | // "libldap_r-2.4.so.2", # needed for debian wheezy 50 | "libm.so", 51 | "libp11-kit.so", 52 | "libpcre.so", 53 | "libpthread.so", 54 | "libresolv.so", 55 | "libroken.so", 56 | "librt.so", 57 | "libsasl2.so", 58 | "libSM.so", 59 | "libusb-1.0.so", 60 | "libuuid.so", 61 | "libwind.so", 62 | "libz.so", 63 | 64 | //Delete potentially dangerous libraries 65 | "libstdc", 66 | "libgobject", 67 | "libc.so", 68 | 69 | "libdbus-1.so", 70 | 71 | // Fix the "libGL error" messages 72 | "libGL.so", 73 | "libdrm.so", 74 | } 75 | 76 | return blacklist 77 | } 78 | 79 | func generateLibsBlacklist() []string { 80 | blacklist, err := parseBlacklistFile(*blacklistFileFlag) 81 | if err != nil { log.Printf("Error while parsing blacklist: %v", err) } 82 | 83 | if *defaultBlackListFlag { 84 | defaultBlacklist := DefaultBlacklist() 85 | blacklist = append(blacklist, defaultBlacklist...) 86 | } 87 | 88 | return blacklist 89 | } 90 | 91 | func parseBlacklistFile(filepath string) ([]string, error) { 92 | log.Printf("Parsing blacklist file %v", filepath) 93 | 94 | file, err := os.Open(filepath) 95 | if err != nil { return nil, err } 96 | 97 | defer file.Close() 98 | 99 | blacklist := make([]string, 0, 10) 100 | 101 | scanner := bufio.NewScanner(file) 102 | for scanner.Scan() { 103 | item := strings.TrimSpace(scanner.Text()) 104 | 105 | if strings.HasPrefix(item, "#") { continue } 106 | blacklist = append(blacklist, strings.ToLower(item)) 107 | } 108 | 109 | log.Printf("Parsed %v blacklisted libraries", len(blacklist)) 110 | 111 | // check for errors 112 | if err = scanner.Err(); err != nil { 113 | return blacklist, err 114 | } 115 | 116 | return blacklist, nil 117 | } 118 | 119 | func cleanupBlacklistedLibs(libdirpath string, blacklist []string) error { 120 | if len(blacklist) == 0 { 121 | log.Printf("No libraries blacklisted") 122 | return nil 123 | } 124 | 125 | log.Println("Removing blacklisted libraries...") 126 | 127 | err := filepath.Walk(libdirpath, func(path string, info os.FileInfo, err error) error { 128 | if err != nil { 129 | return err 130 | } 131 | 132 | if !info.Mode().IsRegular() { 133 | return nil 134 | } 135 | 136 | basename := strings.ToLower(filepath.Base(path)) 137 | 138 | for _, blackLib := range blacklist { 139 | if strings.HasPrefix(basename, blackLib) { 140 | log.Printf("Removing blacklisted library [%v] with match on [%v]", path, blackLib) 141 | os.Remove(path) 142 | break 143 | } 144 | } 145 | 146 | return nil 147 | }) 148 | 149 | return err 150 | } 151 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "flag" 21 | "os" 22 | "os/exec" 23 | "io" 24 | "errors" 25 | "path/filepath" 26 | ) 27 | 28 | type stringsParam []string 29 | 30 | func (s *stringsParam) String() string { 31 | return fmt.Sprintf("%s", *s) 32 | } 33 | 34 | func (s *stringsParam) Set(value string) error { 35 | *s = append(*s, value) 36 | return nil 37 | } 38 | 39 | var ( 40 | qmlImports stringsParam 41 | librariesDirs stringsParam 42 | currentExeFullPath string 43 | ) 44 | 45 | // flags 46 | var ( 47 | outTypeFlag = flag.String("out", "appimage", "Type of the generated output") 48 | blacklistFileFlag = flag.String("blacklist", "libs.blacklist", "Path to the additional libraries blacklist file") 49 | defaultBlackListFlag = flag.Bool("default-blacklist", false, "Add default blacklist") 50 | generateDesktopFlag = flag.Bool("gen-desktop", false, "Generate desktop file") 51 | logPathFlag = flag.String("log", "linuxdeploy.log", "Path to the logfile") 52 | stdoutFlag = flag.Bool("stdout", false, "Log to stdout and to logfile") 53 | exePathFlag = flag.String("exe", "", "Path to the executable") 54 | iconPathFlag = flag.String("icon", "", "Path the exe's icon (used for desktop file)") 55 | appDirPathFlag = flag.String("appdir", "", "Path to the AppDir (if 'type' is appimage)") 56 | overwriteFlag = flag.Bool("overwrite", false, "Overwrite output if present") 57 | qmakePathFlag = flag.String("qmake", "", "Path to qmake") 58 | stripFlag = flag.Bool("strip", false, "Run strip on binaries") 59 | ) 60 | 61 | const ( 62 | appName = "linuxdeploy" 63 | ) 64 | 65 | func init() { 66 | flag.Var(&qmlImports, "qmldir", "QML imports dir") 67 | flag.Var(&librariesDirs, "libs", "Additional libraries search paths") 68 | } 69 | 70 | func main() { 71 | err := parseFlags() 72 | if err != nil { 73 | flag.PrintDefaults() 74 | log.Fatal(err.Error()) 75 | } 76 | 77 | logfile, err := setupLogging() 78 | if err == nil { 79 | defer logfile.Close() 80 | } 81 | 82 | currentExeFullPath = executablePath() 83 | log.Println("Current exe path is", currentExeFullPath) 84 | 85 | appDirPath := resolveAppDir() 86 | os.RemoveAll(appDirPath) 87 | os.MkdirAll(appDirPath, os.ModePerm) 88 | log.Printf("Created directory %v", appDirPath) 89 | 90 | appDeployer := &AppDeployer{ 91 | processedLibs: make(map[string]bool), 92 | libsChannel: make(chan *DeployRequest), 93 | copyChannel: make(chan *DeployRequest), 94 | rpathChannel: make(chan string), 95 | stripChannel: make(chan string), 96 | qtChannel: make(chan string), 97 | 98 | qtDeployer: &QtDeployer{ 99 | qmakePath: resolveQMake(), 100 | qmakeVars: make(map[string]string), 101 | deployedQmlImports: make(map[string]bool), 102 | qtEnv: make(map[QMakeKey]string), 103 | qmlImportDirs: qmlImports, 104 | privateWidgetsDeployed: false, 105 | qtEnvironmentSet: false, 106 | translationsRequired: make(map[string]bool), 107 | }, 108 | 109 | additionalLibPaths: make([]string, 0, 10), 110 | destinationRoot: appDirPath, 111 | targetExePath: resolveTargetExe(), 112 | } 113 | 114 | for _, libpath := range librariesDirs { 115 | appDeployer.addAdditionalLibPath(libpath) 116 | } 117 | 118 | appDeployer.DeployApp() 119 | } 120 | 121 | func parseFlags() error { 122 | flag.Parse() 123 | 124 | _, err := os.Stat(*exePathFlag) 125 | if os.IsNotExist(err) { return err } 126 | 127 | if len(*outTypeFlag) > 0 && (*outTypeFlag != "appimage") { return errors.New(appName + " only supports appimage type at this time") } 128 | 129 | appDirInfo, err := os.Stat(*appDirPathFlag) 130 | if err == nil && appDirInfo.IsDir() { 131 | if !(*overwriteFlag) { 132 | return errors.New("AppDir already exists. Please set overwrite flag to overwrite it") 133 | } 134 | } 135 | 136 | return nil 137 | } 138 | 139 | func setupLogging() (f *os.File, err error) { 140 | f, err = os.OpenFile(*logPathFlag, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666) 141 | if err != nil { 142 | fmt.Println("error opening file: %v", *logPathFlag) 143 | return nil, err 144 | } 145 | 146 | if *stdoutFlag { 147 | mw := io.MultiWriter(os.Stdout, f) 148 | log.SetOutput(mw) 149 | } else { 150 | log.SetOutput(f) 151 | } 152 | 153 | log.Println("------------------------------") 154 | log.Println(appName + " log started") 155 | 156 | return f, err 157 | } 158 | 159 | func resolveAppDir() string { 160 | foundPath := *appDirPathFlag 161 | var err error 162 | 163 | if !filepath.IsAbs(foundPath) { 164 | if foundPath, err = filepath.Abs(foundPath); err != nil { 165 | foundPath = *appDirPathFlag 166 | } 167 | } 168 | 169 | return foundPath 170 | } 171 | 172 | func resolveTargetExe() string { 173 | foundPath := *exePathFlag 174 | var err error 175 | 176 | if !filepath.IsAbs(foundPath) { 177 | if foundPath, err = filepath.Abs(foundPath); err != nil { 178 | foundPath = *exePathFlag 179 | } 180 | } 181 | 182 | return foundPath 183 | } 184 | 185 | func resolveQMake() string { 186 | var err error 187 | currentPath := *qmakePathFlag 188 | if len(currentPath) == 0 { currentPath = "qmake" } 189 | 190 | if _, err = os.Stat(currentPath); os.IsNotExist(err) { 191 | if currentPath, err = exec.LookPath("qmake"); err != nil { 192 | if currentPath, err = exec.LookPath("qmake-qt5"); err != nil { 193 | if currentPath, err = exec.LookPath("qmake-qt4"); err != nil { 194 | return "" 195 | } 196 | } 197 | } 198 | } 199 | 200 | return currentPath 201 | } 202 | 203 | func generateAppImg() bool { 204 | return *outTypeFlag == "appimage" 205 | } 206 | -------------------------------------------------------------------------------- /src/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | "bytes" 6 | ) 7 | 8 | func TestBasicBytesReplace(t *testing.T) { 9 | buffer := []byte("somename=somevalue\x00") 10 | varname := []byte("somename=") 11 | replacement := []byte("test") 12 | 13 | expectedResult := []byte("somename=test\x00\x00\x00\x00\x00\x00") 14 | 15 | replaceInBuffer(buffer, varname, replacement) 16 | 17 | if bytes.Compare(buffer, expectedResult) != 0 { 18 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 19 | } 20 | } 21 | 22 | func TestNotFoundKeyBytesReplace(t *testing.T) { 23 | originalString := "somename=somevalue\x00" 24 | buffer := []byte(originalString) 25 | varname := []byte("somename1=") 26 | replacement := []byte("test") 27 | 28 | expectedResult := []byte(originalString) 29 | 30 | replaceInBuffer(buffer, varname, replacement) 31 | 32 | if bytes.Compare(buffer, expectedResult) != 0 { 33 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 34 | } 35 | } 36 | 37 | func TestNotFoundZeroEndReplace(t *testing.T) { 38 | originalString := "somename=somevaluexs" 39 | buffer := []byte(originalString) 40 | varname := []byte("somename=") 41 | replacement := []byte("test") 42 | 43 | expectedResult := []byte(originalString) 44 | 45 | replaceInBuffer(buffer, varname, replacement) 46 | 47 | if bytes.Compare(buffer, expectedResult) != 0 { 48 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 49 | } 50 | } 51 | 52 | func TestBasicPaddedReplace(t *testing.T) { 53 | buffer := []byte("otherStartsomename=somevalue\x00otherEnd\x00") 54 | varname := []byte("somename=") 55 | replacement := []byte("test") 56 | 57 | expectedResult := []byte("otherStartsomename=test\x00\x00\x00\x00\x00\x00otherEnd\x00") 58 | 59 | replaceInBuffer(buffer, varname, replacement) 60 | 61 | if bytes.Compare(buffer, expectedResult) != 0 { 62 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 63 | } 64 | } 65 | 66 | func TestZeroLengthValueReplace(t *testing.T) { 67 | originalString := "otherStartsomename=\x00otherEnd\x00" 68 | buffer := []byte(originalString) 69 | varname := []byte("somename=") 70 | replacement := []byte("test") 71 | 72 | expectedResult := []byte(originalString) 73 | 74 | replaceInBuffer(buffer, varname, replacement) 75 | 76 | if bytes.Compare(buffer, expectedResult) != 0 { 77 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 78 | } 79 | } 80 | 81 | func TestSmallerValueThanReplacementReplace(t *testing.T) { 82 | originalString := "otherStartsomename=tes\x00otherEnd\x00" 83 | buffer := []byte(originalString) 84 | varname := []byte("somename=") 85 | replacement := []byte("test") 86 | 87 | expectedResult := []byte(originalString) 88 | 89 | replaceInBuffer(buffer, varname, replacement) 90 | 91 | if bytes.Compare(buffer, expectedResult) != 0 { 92 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 93 | } 94 | } 95 | 96 | func TestReplaceToTheSameValue(t *testing.T) { 97 | originalString := "otherStart\x00somename=test\x00otherEnd\x00" 98 | buffer := []byte(originalString) 99 | varname := []byte("somename=") 100 | replacement := []byte("test") 101 | 102 | expectedResult := []byte(originalString) 103 | 104 | replaceInBuffer(buffer, varname, replacement) 105 | 106 | if bytes.Compare(buffer, expectedResult) != 0 { 107 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 108 | } 109 | } 110 | 111 | func TestReplaceToZeroLength(t *testing.T) { 112 | buffer := []byte("otherStart\x00somename=test\x00otherEnd\x00") 113 | varname := []byte("somename=") 114 | replacement := []byte("") 115 | 116 | expectedResult := []byte("otherStart\x00somename=\x00\x00\x00\x00\x00otherEnd\x00") 117 | 118 | replaceInBuffer(buffer, varname, replacement) 119 | 120 | if bytes.Compare(buffer, expectedResult) != 0 { 121 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 122 | } 123 | } 124 | 125 | func TestReplaceOnlyFirstMatch(t *testing.T) { 126 | originalString := "otherStart\x00somename=somevalue\x00otherEnd\x00somename=another" 127 | buffer := []byte(originalString) 128 | varname := []byte("somename=") 129 | replacement := []byte("test") 130 | 131 | expectedResult := []byte("otherStart\x00somename=test\x00\x00\x00\x00\x00\x00otherEnd\x00somename=another") 132 | 133 | replaceInBuffer(buffer, varname, replacement) 134 | 135 | if bytes.Compare(buffer, expectedResult) != 0 { 136 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 137 | } 138 | } 139 | 140 | func TestInSliceOfSlice(t *testing.T) { 141 | originalString := "otherStart\x00somename=somevalue\x00otherEnd\x00somename=another\x00" 142 | buffer := []byte(originalString) 143 | varname := []byte("somename=") 144 | replacement := []byte("test") 145 | 146 | expectedResult := []byte("otherStart\x00somename=somevalue\x00otherEnd\x00somename=test\x00\x00\x00\x00") 147 | 148 | replaceInBuffer(buffer[20:], varname, replacement) 149 | 150 | if bytes.Compare(buffer, expectedResult) != 0 { 151 | t.Fatalf("Expected %v but got %v", expectedResult, buffer) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/qtdeployer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "strings" 20 | "os" 21 | "os/exec" 22 | "io/ioutil" 23 | "errors" 24 | "path/filepath" 25 | "encoding/json" 26 | ) 27 | 28 | type QMakeKey int 29 | 30 | const ( 31 | QT_INSTALL_PREFIX QMakeKey = iota 32 | QT_INSTALL_ARCHDATA 33 | QT_INSTALL_DATA 34 | QT_INSTALL_DOCS 35 | QT_INSTALL_HEADERS 36 | QT_INSTALL_LIBS 37 | QT_INSTALL_LIBEXECS 38 | QT_INSTALL_BINS 39 | QT_INSTALL_TESTS 40 | QT_INSTALL_PLUGINS 41 | QT_INSTALL_IMPORTS 42 | QT_INSTALL_QML 43 | QT_INSTALL_TRANSLATIONS 44 | QT_INSTALL_CONFIGURATION 45 | QT_INSTALL_EXAMPLES 46 | QT_INSTALL_DEMOS 47 | QT_HOST_PREFIX 48 | QT_HOST_DATA 49 | QT_HOST_BINS 50 | QT_HOST_LIBS 51 | QMAKE_VERSION 52 | QT_VERSION 53 | ) 54 | 55 | type QtDeployer struct { 56 | qmakePath string 57 | qmakeVars map[string]string 58 | deployedQmlImports map[string]bool 59 | qtEnv map[QMakeKey]string 60 | qmlImportDirs []string 61 | privateWidgetsDeployed bool 62 | qtEnvironmentSet bool 63 | translationsRequired map[string]bool 64 | } 65 | 66 | func (qd *QtDeployer) queryQtEnv() error { 67 | log.Printf("Querying qmake environment using %v", qd.qmakePath) 68 | if len(qd.qmakePath) == 0 { return errors.New("QMake has not been resolved") } 69 | 70 | out, err := exec.Command(qd.qmakePath, "-query").Output() 71 | if err != nil { return err } 72 | 73 | output := string(out) 74 | // TODO: probably switch to bytes.Split for better performance 75 | lines := strings.Split(output, "\n") 76 | 77 | for _, line := range lines { 78 | line = strings.TrimSpace(line) 79 | if len(line) == 0 { continue } 80 | parts := strings.Split(line, ":") 81 | 82 | if len(parts) != 2 { 83 | log.Printf("Unexpected qmake output: %v", line) 84 | continue 85 | } 86 | 87 | qd.qmakeVars[parts[0]] = parts[1] 88 | } 89 | 90 | qd.parseQtVars() 91 | log.Println("Parsed qmake output: %v", qd.qtEnv) 92 | qd.qtEnvironmentSet = true 93 | return nil 94 | } 95 | 96 | func (qd *QtDeployer) parseQtVars() { 97 | qd.qtEnv[QT_INSTALL_PREFIX], _ = qd.qmakeVars["QT_INSTALL_PREFIX"] 98 | qd.qtEnv[QT_INSTALL_ARCHDATA], _ = qd.qmakeVars["QT_INSTALL_ARCHDATA"] 99 | qd.qtEnv[QT_INSTALL_DATA], _ = qd.qmakeVars["QT_INSTALL_DATA"] 100 | qd.qtEnv[QT_INSTALL_DOCS], _ = qd.qmakeVars["QT_INSTALL_DOCS"] 101 | qd.qtEnv[QT_INSTALL_HEADERS], _ = qd.qmakeVars["QT_INSTALL_HEADERS"] 102 | qd.qtEnv[QT_INSTALL_LIBS], _ = qd.qmakeVars["QT_INSTALL_LIBS"] 103 | qd.qtEnv[QT_INSTALL_LIBEXECS], _ = qd.qmakeVars["QT_INSTALL_LIBEXECS"] 104 | qd.qtEnv[QT_INSTALL_BINS], _ = qd.qmakeVars["QT_INSTALL_BINS"] 105 | qd.qtEnv[QT_INSTALL_PLUGINS], _ = qd.qmakeVars["QT_INSTALL_PLUGINS"] 106 | qd.qtEnv[QT_INSTALL_IMPORTS], _ = qd.qmakeVars["QT_INSTALL_IMPORTS"] 107 | qd.qtEnv[QT_INSTALL_QML], _ = qd.qmakeVars["QT_INSTALL_QML"] 108 | qd.qtEnv[QT_INSTALL_TRANSLATIONS], _ = qd.qmakeVars["QT_INSTALL_TRANSLATIONS"] 109 | qd.qtEnv[QT_INSTALL_CONFIGURATION], _ = qd.qmakeVars["QT_INSTALL_CONFIGURATION"] 110 | qd.qtEnv[QT_HOST_PREFIX], _ = qd.qmakeVars["QT_HOST_PREFIX"] 111 | qd.qtEnv[QT_HOST_DATA], _ = qd.qmakeVars["QT_HOST_DATA"] 112 | qd.qtEnv[QT_HOST_BINS], _ = qd.qmakeVars["QT_HOST_BINS"] 113 | qd.qtEnv[QT_HOST_LIBS], _ = qd.qmakeVars["QT_HOST_LIBS"] 114 | qd.qtEnv[QMAKE_VERSION], _ = qd.qmakeVars["QMAKE_VERSION"] 115 | qd.qtEnv[QT_VERSION], _ = qd.qmakeVars["QT_VERSION"] 116 | } 117 | 118 | func (qd *QtDeployer) BinPath() string { 119 | return qd.qtEnv[QT_INSTALL_BINS] 120 | } 121 | 122 | func (qd *QtDeployer) PluginsPath() string { 123 | return qd.qtEnv[QT_INSTALL_PLUGINS] 124 | } 125 | 126 | func (qd *QtDeployer) LibExecsPath() string { 127 | return qd.qtEnv[QT_INSTALL_LIBEXECS] 128 | } 129 | 130 | func (qd *QtDeployer) DataPath() string { 131 | return qd.qtEnv[QT_INSTALL_DATA] 132 | } 133 | 134 | func (qd *QtDeployer) TranslationsPath() string { 135 | return qd.qtEnv[QT_INSTALL_TRANSLATIONS] 136 | } 137 | 138 | func (qd *QtDeployer) QmlPath() string { 139 | return qd.qtEnv[QT_INSTALL_QML] 140 | } 141 | 142 | func (qd *QtDeployer) accountQmlImport(path string) { 143 | qd.deployedQmlImports[path] = true 144 | } 145 | 146 | func (qd *QtDeployer) isQmlImportDeployed(path string) (deployed bool) { 147 | // TODO: also check directory hierarchy? 148 | _, deployed = qd.deployedQmlImports[path] 149 | return deployed 150 | } 151 | 152 | func (ad *AppDeployer) processQtLibTasks() { 153 | if !ad.qtDeployer.qtEnvironmentSet { 154 | log.Printf("Qt Environment is not initialized") 155 | return 156 | } 157 | 158 | go ad.deployQmlImports() 159 | 160 | for libraryPath := range ad.qtChannel { 161 | ad.processQtLibTask(libraryPath) 162 | // rpath should be changed for all qt libs 163 | ad.addFixRPathTask(libraryPath) 164 | 165 | ad.waitGroup.Done() 166 | } 167 | 168 | log.Printf("Qt libraries processing finished") 169 | } 170 | 171 | func (ad *AppDeployer) processQtLibTask(libraryPath string) { 172 | libraryBasename := filepath.Base(libraryPath) 173 | libname := strings.ToLower(libraryBasename) 174 | 175 | if !strings.HasPrefix(libname, "libqt") { log.Fatal("Can only accept Qt libraries") } 176 | log.Printf("Inspecting Qt lib: %v", libraryBasename) 177 | 178 | ad.qtDeployer.accountQtLibrary(libname) 179 | 180 | deployFlags := LDD_DEPENDENCY_FLAG | DEPLOY_ONLY_LIBRARIES_FLAG | FIX_RPATH_FLAG 181 | 182 | if strings.HasPrefix(libname, "libqt5gui") { 183 | ad.addQtPluginTask("platforms/libqxcb.so") 184 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "imageformats", "plugins", deployFlags) 185 | } else 186 | if strings.HasPrefix(libname, "libqt5svg") { 187 | ad.addQtPluginTask("iconengines/libqsvgicon.so") 188 | } else 189 | if strings.HasPrefix(libname, "libqt5printsupport") { 190 | ad.addQtPluginTask("printsupport/libcupsprintersupport.so") 191 | } else 192 | if strings.HasPrefix(libname, "libqt5opengl") || 193 | strings.HasPrefix(libname, "libqt5xcbqpa") { 194 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "xcbglintegrations", "plugins", deployFlags) 195 | } else 196 | if strings.HasPrefix(libname, "libqt5network") { 197 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "bearer", "plugins", deployFlags) 198 | } else 199 | if strings.HasPrefix(libname, "libqt5sql") { 200 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "sqldrivers", "plugins", deployFlags) 201 | } else 202 | if strings.HasPrefix(libname, "libqt5multimedia") { 203 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "mediaservice", "plugins", deployFlags) 204 | ad.deployRecursively(ad.qtDeployer.PluginsPath(), "audio", "plugins", deployFlags) 205 | } else 206 | if strings.HasPrefix(libname, "libqt5webenginecore") { 207 | ad.addCopyQtDepTask(ad.qtDeployer.LibExecsPath(), "QtWebEngineProcess", "libexecs") 208 | ad.copyRecursively(ad.qtDeployer.DataPath(), "resources", ".") 209 | ad.copyRecursively(ad.qtDeployer.TranslationsPath(), "qtwebengine_locales", "translations") 210 | } else 211 | if strings.HasPrefix(libname, "libqt5core") { 212 | ad.patchQtCore(libraryPath) 213 | } 214 | } 215 | 216 | // copies one file 217 | func (ad *AppDeployer) addCopyQtDepTask(sourceRoot, sourcePath, targetPath string) error { 218 | path := filepath.Join(sourceRoot, sourcePath) 219 | log.Printf("Copy once %v into %v", path, targetPath) 220 | relativePath, err := filepath.Rel(sourceRoot, path) 221 | if err != nil { 222 | log.Println(err) 223 | } 224 | 225 | ad.waitGroup.Add(1) 226 | go func() { 227 | ad.copyChannel <- &DeployRequest{ 228 | sourceRoot: sourceRoot, 229 | sourcePath: relativePath, 230 | targetPath: targetPath, 231 | flags: FIX_RPATH_FLAG, 232 | } 233 | }() 234 | 235 | return err 236 | } 237 | 238 | func (ad *AppDeployer) addQtPluginTask(relpath string) { 239 | log.Printf("Deploying additional Qt plugin: %v", relpath) 240 | ad.addLibTask(ad.qtDeployer.PluginsPath(), relpath, "plugins", LDD_AND_RPATH_FLAG) 241 | } 242 | 243 | func (ad *AppDeployer) deployQmlImports() error { 244 | // rescue agains premature finish of the main loop 245 | ad.waitGroup.Add(1) 246 | defer ad.waitGroup.Done() 247 | 248 | log.Printf("Processing QML imports from %v", ad.qtDeployer.qmlImportDirs) 249 | 250 | scannerPath := filepath.Join(ad.qtDeployer.BinPath(), "qmlimportscanner") 251 | 252 | if _, err := os.Stat(scannerPath); err != nil { 253 | if scannerPath, err = exec.LookPath("qmlimportscanner"); err != nil { 254 | log.Printf("Cannot find qmlimportscanner") 255 | return err 256 | } 257 | } 258 | 259 | log.Printf("QML import scanner: %v", scannerPath) 260 | 261 | args := make([]string, 0, 10) 262 | for _, qmldir := range ad.qtDeployer.qmlImportDirs { 263 | args = append(args, "-rootPath") 264 | args = append(args, qmldir) 265 | } 266 | 267 | args = append(args, "-importPath") 268 | args = append(args, ad.qtDeployer.QmlPath()) 269 | 270 | out, err := exec.Command(scannerPath, args...).Output() 271 | if err != nil { 272 | log.Printf("QML import scanner failed with %v", err) 273 | return err 274 | } 275 | 276 | err = ad.processQmlImportsJson(out) 277 | return err 278 | } 279 | 280 | type QmlImport struct { 281 | Classname string `json:"classname,omitempty"` 282 | Name string `json:"name,omitempty"` 283 | Path string `json:"path,omitempty"` 284 | Plugin string `json:"plugin,omitempty"` 285 | ImportType string `json:"type,omitempty"` 286 | Version string `json:"version,omitempty"` 287 | } 288 | 289 | func (ad *AppDeployer) processQmlImportsJson(jsonRaw []byte) (err error) { 290 | log.Printf("Parsing QML imports") 291 | 292 | var qmlImports []QmlImport 293 | err = json.Unmarshal(jsonRaw, &qmlImports) 294 | if err != nil { return err } 295 | log.Printf("Parsed %v imports", len(qmlImports)) 296 | 297 | sourceRoot := ad.qtDeployer.QmlPath() 298 | 299 | for _, qmlImport := range qmlImports { 300 | relativePath, err := filepath.Rel(sourceRoot, qmlImport.Path) 301 | 302 | if err != nil || len(qmlImport.Name) == 0 { 303 | log.Printf("Skipping import %v", qmlImport) 304 | continue 305 | } 306 | 307 | if qmlImport.ImportType != "module" { 308 | log.Printf("Skipping non-module import %v", qmlImport) 309 | continue 310 | } 311 | 312 | if len(qmlImport.Path) == 0 { 313 | log.Printf("Skipping import without path %v", qmlImport) 314 | continue 315 | } 316 | 317 | if ad.qtDeployer.isQmlImportDeployed(qmlImport.Path) { 318 | log.Printf("Skipping already deployed QML import %v", qmlImport.Path) 319 | continue 320 | } 321 | 322 | if (qmlImport.Name == "QtQuick.Controls") && !ad.qtDeployer.privateWidgetsDeployed { 323 | ad.qtDeployer.privateWidgetsDeployed = true 324 | log.Printf("Deploying private widgets for QtQuick.Controls") 325 | ad.deployRecursively(sourceRoot, "QtQuick/PrivateWidgets", "qml", FIX_RPATH_FLAG) 326 | } 327 | 328 | log.Printf("Deploying QML import %v", qmlImport.Path) 329 | ad.qtDeployer.accountQmlImport(qmlImport.Path) 330 | ad.deployRecursively(sourceRoot, relativePath, "qml", FIX_RPATH_FLAG) 331 | } 332 | 333 | return nil 334 | } 335 | 336 | func (ad *AppDeployer) patchQtCore(libraryPath string) { 337 | // rescue agains premature finish of the main loop 338 | ad.waitGroup.Add(1) 339 | defer ad.waitGroup.Done() 340 | 341 | log.Printf("About to patch libQt5Core at path %v", libraryPath) 342 | err := patchQtCore(libraryPath) 343 | if err != nil { 344 | log.Printf("QtCore patching failed! %v", err) 345 | } else { 346 | log.Println("QtCore patching finished") 347 | } 348 | } 349 | 350 | func patchQtCore(path string) error { 351 | fi, err := os.Stat(path) 352 | if err != nil { return err } 353 | 354 | originalMode := fi.Mode() 355 | 356 | contents, err := ioutil.ReadFile(path) 357 | if err != nil { return err } 358 | 359 | // this list originates from https://github.com/probonopd/linuxdeployqt 360 | replaceVariable(contents, "qt_prfxpath=", "."); 361 | replaceVariable(contents, "qt_adatpath=", "."); 362 | replaceVariable(contents, "qt_docspath=", "doc"); 363 | replaceVariable(contents, "qt_hdrspath=", "include"); 364 | replaceVariable(contents, "qt_libspath=", "lib"); 365 | replaceVariable(contents, "qt_lbexpath=", "libexec"); 366 | replaceVariable(contents, "qt_binspath=", "bin"); 367 | replaceVariable(contents, "qt_plugpath=", "plugins"); 368 | replaceVariable(contents, "qt_impspath=", "imports"); 369 | replaceVariable(contents, "qt_qml2path=", "qml"); 370 | replaceVariable(contents, "qt_datapath=", "."); 371 | replaceVariable(contents, "qt_trnspath=", "translations"); 372 | replaceVariable(contents, "qt_xmplpath=", "examples"); 373 | replaceVariable(contents, "qt_demopath=", "demos"); 374 | replaceVariable(contents, "qt_tstspath=", "tests"); 375 | replaceVariable(contents, "qt_hpfxpath=", "."); 376 | replaceVariable(contents, "qt_hbinpath=", "bin"); 377 | replaceVariable(contents, "qt_hdatpath=", "."); 378 | replaceVariable(contents, "qt_stngpath=", "."); // e.g., /opt/qt53/etc/xdg; does it load Trolltech.conf from there? 379 | 380 | /* Qt on Arch Linux comes with more hardcoded paths 381 | * https://github.com/probonopd/linuxdeployqt/issues/98 382 | replaceVariable(contents, "lib/qt/libexec", "libexec"); 383 | replaceVariable(contents, "lib/qt/plugins", "plugins"); 384 | replaceVariable(contents, "lib/qt/imports", "imports"); 385 | replaceVariable(contents, "lib/qt/qml", "qml"); 386 | replaceVariable(contents, "lib/qt", ""); 387 | replaceVariable(contents, "share/doc/qt", "doc"); 388 | replaceVariable(contents, "include/qt", "include"); 389 | replaceVariable(contents, "share/qt", ""); 390 | replaceVariable(contents, "share/qt/translations", "translations"); 391 | replaceVariable(contents, "share/doc/qt/examples", "examples"); 392 | */ 393 | 394 | err = ioutil.WriteFile(path, contents, originalMode) 395 | return err 396 | } 397 | -------------------------------------------------------------------------------- /src/qttranslations.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "strings" 20 | "path/filepath" 21 | "fmt" 22 | "os" 23 | "os/exec" 24 | "sync" 25 | ) 26 | 27 | // list of modules from windeployqt 28 | const ( 29 | QtBluetoothModule int64 = 1 << iota 30 | QtCLuceneModule 31 | QtConcurrentModule 32 | QtCoreModule 33 | QtDeclarativeModule 34 | QtDesignerComponents 35 | QtDesignerModule 36 | QtGuiModule 37 | QtCluceneModule 38 | QtHelpModule 39 | QtMultimediaModule 40 | QtMultimediaWidgetsModule 41 | QtMultimediaQuickModule 42 | QtNetworkModule 43 | QtNfcModule 44 | QtOpenGLModule 45 | QtPositioningModule 46 | QtPrintSupportModule 47 | QtQmlModule 48 | QtQuickModule 49 | QtQuickParticlesModule 50 | QtScriptModule 51 | QtScriptToolsModule 52 | QtSensorsModule 53 | QtSerialPortModule 54 | QtSqlModule 55 | QtSvgModule 56 | QtTestModule 57 | QtWidgetsModule 58 | _ // QtWinExtrasModule 59 | QtXmlModule 60 | QtXmlPatternsModule 61 | QtWebKitModule 62 | QtWebKitWidgetsModule 63 | QtQuickWidgetsModule 64 | QtWebSocketsModule 65 | QtEnginioModule 66 | QtWebEngineCoreModule 67 | QtWebEngineModule 68 | QtWebEngineWidgetsModule 69 | QtQmlToolingModule 70 | Qt3DCoreModule 71 | Qt3DRendererModule 72 | Qt3DQuickModule 73 | Qt3DQuickRendererModule 74 | Qt3DInputModule 75 | QtLocationModule 76 | QtWebChannelModule 77 | QtTextToSpeechModule 78 | QtSerialBusModule 79 | ) 80 | 81 | var moduleToTranslationMap = map[string]string { 82 | /*QtBluetoothModule:*/ "qt5bluetooth": "", 83 | /*QtCLuceneModule:*/"qt5clucene": "qt_help", 84 | /*QtConcurrentModule:*/ "qt5concurrent": "qtbase", 85 | /*QtCoreModule:*/ "qt5core": "qtbase", 86 | /*QtDeclarativeModule:*/ "qt5declarative": "qtquick1", 87 | /*QtDesignerComponents:*/ "qt5designercomponents": "", 88 | /*QtDesignerModule:*/ "qt5designer": "", 89 | /*QtGuiModule:*/ "qt5gui": "qtbase", 90 | /*QtHelpModule:*/ "qt5help": "qt_help", 91 | /*QtMultimediaModule:*/ "qt5multimedia": "qtmultimedia", 92 | /*QtMultimediaWidgetsModule:*/ "qt5multimediawidgets": "qtmultimedia", 93 | /*QtMultimediaQuickModule:*/ "qt5multimediaquick_p": "qtmultimedia", 94 | /*QtNetworkModule:*/ "qt5network": "qtbase", 95 | /*QtNfcModule:*/ "qt5nfc": "", 96 | /*QtOpenGLModule:*/ "qt5opengl": "", 97 | /*QtPositioningModule:*/ "qt5positioning": "", 98 | /*QtPrintSupportModule:*/ "qt5printsupport": "", 99 | /*QtQmlModule:*/ "qt5qml": "qtdeclarative", 100 | /*QtQuickModule:*/ "qt5quick": "qtdeclarative", 101 | /*QtQuickParticlesModule:*/ "qt5quickparticles": "", 102 | /*QtScriptModule:*/ "qt5script": "qtscript", 103 | /*QtScriptToolsModule:*/ "qt5scripttools": "qtscript", 104 | /*QtSensorsModule:*/ "qt5sensors": "", 105 | /*QtSerialPortModule:*/ "qt5serialport": "qtserialport", 106 | /*QtSqlModule:*/ "qt5sql": "qtbase", 107 | /*QtSvgModule:*/ "qt5svg": "", 108 | /*QtTestModule:*/ "qt5test": "", 109 | /*QtWidgetsModule:*/ "qt5widgets": "qtbase", 110 | // QtWinExtrasModule:*/ "qt5winextras": "", 111 | /*QtXmlModule:*/ "qt5xml": "qtbase", 112 | /*QtXmlPatternsModule:*/ "qt5xmlpatterns": "qtxmlpatterns", 113 | /*QtWebKitModule:*/ "qt5webkit": "qtwebengine", 114 | /*QtWebKitWidgetsModule:*/ "qt5webkitwidgets": "", 115 | /*QtQuickWidgetsModule:*/ "qt5quickwidgets": "", 116 | /*QtWebSocketsModule:*/ "qt5websockets": "qtwebsockets", 117 | /*QtEnginioModule:*/ "enginio": "", 118 | /*QtWebEngineCoreModule:*/ "qt5webenginecore": "", 119 | /*QtWebEngineModule:*/ "qt5webengine": "", 120 | /*QtWebEngineWidgetsModule:*/ "qt5webenginewidgets": "", 121 | /*QtQmlToolingModule:*/ "qt5qmltooling": "qmltooling", 122 | /*Qt3DCoreModule:*/ "qt53dcore": "", 123 | /*Qt3DRendererModule:*/ "qt53drenderer": "", 124 | /*Qt3DQuickModule:*/ "qt53dquick": "", 125 | /*Qt3DQuickRendererModule:*/ "qt53dquickrenderer": "", 126 | /*Qt3DInputModule:*/ "qt53dinput": "", 127 | /*QtLocationModule:*/ "qt5location": "", 128 | /*QtWebChannelModule:*/ "qt5webchannel": "", 129 | /*QtTextToSpeechModule:*/ "qt5texttospeech": "", 130 | /*QtSerialBusModule:*/ "qt5serialbus": "", 131 | } 132 | 133 | func (qd *QtDeployer) accountQtLibrary(libname string) { 134 | extensionIndex := strings.LastIndex(libname, ".so") 135 | if extensionIndex == -1 { return } 136 | 137 | libprefix := libname[3:extensionIndex] 138 | if translation, ok := moduleToTranslationMap[libprefix]; ok { 139 | if len(translation) > 0 { 140 | qd.translationsRequired[translation] = true 141 | log.Printf("Accounted translation %v for lib %v", translation, libname) 142 | } 143 | } else { 144 | log.Printf("Translations unknown for module: %v", libname) 145 | } 146 | } 147 | 148 | func (ad *AppDeployer) deployQtTranslations(translationsRoot string, mainWaitGroup *sync.WaitGroup) { 149 | defer mainWaitGroup.Done() 150 | if !ad.qtDeployer.qtEnvironmentSet { return } 151 | 152 | qtTranslationsPath := ad.qtDeployer.TranslationsPath() 153 | 154 | languages := retrieveAvailableLanguages(qtTranslationsPath) 155 | if len(languages) == 0 { return } 156 | 157 | lconvertPath := filepath.Join(ad.qtDeployer.BinPath(), "lconvert") 158 | 159 | if _, err := os.Stat(lconvertPath); err != nil { 160 | if lconvertPath, err = exec.LookPath("lconvert"); err != nil { 161 | log.Printf("Cannot find lconvert") 162 | return 163 | } 164 | } 165 | 166 | log.Printf("Required translations: %v", ad.qtDeployer.translationsRequired) 167 | ensureDirExists(filepath.Join(translationsRoot, "dummyfile")) 168 | 169 | var wg sync.WaitGroup 170 | 171 | for _, lang := range languages { 172 | wg.Add(1) 173 | go ad.deployLanguage(lang, lconvertPath, translationsRoot, &wg) 174 | } 175 | 176 | wg.Wait() 177 | log.Printf("Translations generations finished") 178 | } 179 | 180 | func (ad *AppDeployer) deployLanguage(lang, lconvertPath, translationsRoot string, wg *sync.WaitGroup) { 181 | defer wg.Done() 182 | 183 | qtTranslationsPath := ad.qtDeployer.TranslationsPath() 184 | 185 | arguments := make([]string, 0, 10) 186 | // generate combined translation files for each language 187 | outputFile := fmt.Sprintf("qt_%s.qm", lang) 188 | outputFilepath := filepath.Join(translationsRoot, outputFile) 189 | 190 | arguments = append(arguments, "-o", outputFilepath) 191 | 192 | for module, _ := range ad.qtDeployer.translationsRequired { 193 | trFile := fmt.Sprintf("%s_%s.qm", module, lang) 194 | trFilepath := filepath.Join(qtTranslationsPath, trFile) 195 | arguments = append(arguments, trFilepath) 196 | } 197 | 198 | log.Printf("Launching lconvert with arguments %v", arguments) 199 | 200 | err := exec.Command(lconvertPath, arguments...).Run() 201 | if err != nil { 202 | log.Printf("lconvert failed with %v", err) 203 | } else { 204 | log.Printf("Generated translations file %v", outputFile) 205 | } 206 | } 207 | 208 | func retrieveAvailableLanguages(translationsRoot string) []string { 209 | log.Printf("Translations: checking available languages in %v", translationsRoot) 210 | 211 | languages := make([]string, 0, 10) 212 | 213 | err := filepath.Walk(translationsRoot, func(path string, info os.FileInfo, err error) error { 214 | if err != nil { 215 | return err 216 | } 217 | 218 | if !info.Mode().IsRegular() { 219 | return nil 220 | } 221 | 222 | basename := strings.ToLower(filepath.Base(path)) 223 | 224 | if !strings.HasPrefix(basename, "qtbase_") || !strings.HasSuffix(basename, ".qm") { 225 | return nil 226 | } 227 | 228 | // language between qtbase_ and .qm 229 | lastIndex := len(basename) - 3 230 | lang := basename[7:lastIndex] 231 | languages = append(languages, lang) 232 | 233 | return nil 234 | }) 235 | 236 | if err != nil { log.Printf("Error while searching translations: %v", err) } 237 | log.Printf("Found qt translations languages %v", languages) 238 | 239 | return languages 240 | } 241 | -------------------------------------------------------------------------------- /src/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is a part of linuxdeploy - tool for 3 | * creating standalone applications for Linux 4 | * 5 | * Copyright (C) 2017 Taras Kushnir 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the MIT License. 9 | 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "os/exec" 20 | "log" 21 | "io" 22 | "path" 23 | "errors" 24 | "strings" 25 | "bytes" 26 | ) 27 | 28 | type Bitmask uint32 29 | 30 | func (f Bitmask) HasFlag(flag Bitmask) bool { return f & flag != 0 } 31 | func (f *Bitmask) AddFlag(flag Bitmask) { *f |= flag } 32 | func (f *Bitmask) ClearFlag(flag Bitmask) { *f &= ^flag } 33 | func (f *Bitmask) ToggleFlag(flag Bitmask) { *f ^= flag } 34 | 35 | func executablePath() string { 36 | fullpath, _ := exec.LookPath(os.Args[0]) 37 | return fullpath 38 | } 39 | 40 | func copyFile(src, dst string) (err error) { 41 | log.Printf("About to copy file %v to %v", src, dst) 42 | 43 | fi, err := os.Stat(src) 44 | if err != nil { return err } 45 | sourceMode := fi.Mode() 46 | 47 | in, err := os.Open(src) 48 | if err != nil { 49 | log.Printf("Failed to open source: %v", err) 50 | return err 51 | } 52 | 53 | defer in.Close() 54 | 55 | out, err := os.OpenFile(dst, os.O_RDWR | os.O_TRUNC | os.O_CREATE, sourceMode) 56 | if err != nil { 57 | log.Printf("Failed to create destination: %v", err) 58 | return 59 | } 60 | 61 | defer func() { 62 | cerr := out.Close() 63 | if err == nil { 64 | err = cerr 65 | } 66 | }() 67 | 68 | if _, err = io.Copy(out, in); err != nil { 69 | return 70 | } 71 | 72 | err = out.Sync() 73 | return 74 | } 75 | 76 | func ensureDirExists(fullpath string) (err error) { 77 | log.Printf("Ensure directory exists for file %v", fullpath) 78 | dirpath := path.Dir(fullpath) 79 | err = os.MkdirAll(dirpath, os.ModePerm) 80 | if err != nil { 81 | log.Printf("Failed to create directory %v", dirpath) 82 | } 83 | 84 | return err 85 | } 86 | 87 | func parseLddOutputLine(line string) (string, string, error) { 88 | if len(line) == 0 { return "", "", errors.New("Empty") } 89 | 90 | var libpath, libname string 91 | 92 | if strings.Contains(line, " => ") { 93 | parts := strings.Split(line, " => ") 94 | 95 | if len(parts) != 2 { 96 | return "", "", errors.New("Wrong format") 97 | } 98 | 99 | libname = strings.TrimSpace(parts[0]) 100 | 101 | if parts[1] == "not found" { return parts[0], "", nil } 102 | 103 | lastUseful := strings.LastIndex(parts[1], "(0x") 104 | if lastUseful != -1 { 105 | libpath = strings.TrimSpace(parts[1][:lastUseful]) 106 | } 107 | } else { 108 | log.Printf("Skipping ldd line: %v", line) 109 | return "", "", errors.New("Not with =>") 110 | } 111 | 112 | return libname, libpath, nil 113 | } 114 | 115 | func replaceInBuffer(buffer, key, replacement []byte) { 116 | index := bytes.Index(buffer, key) 117 | if index == -1 { 118 | log.Printf("Not found \"%s\" %v when replacing", key, key) 119 | return 120 | } 121 | 122 | nextIndex := len(key) + index 123 | log.Printf("Start of %s found at %v", key, nextIndex) 124 | 125 | endIndex := bytes.IndexByte(buffer[nextIndex:], byte(0)) 126 | if endIndex == -1 { 127 | log.Printf("End not found for %s (%v) when replacing", key, key) 128 | return 129 | } 130 | 131 | log.Printf("Replacement End found at %v", endIndex + nextIndex) 132 | 133 | if endIndex < len(replacement) { 134 | log.Printf("Cannot exceed length when replacing %s", key) 135 | return 136 | } 137 | 138 | i := nextIndex 139 | j := 0 140 | replacementSize := len(replacement) 141 | endIndex += nextIndex 142 | 143 | log.Printf("Replacement previous value is %s", buffer[nextIndex:endIndex]) 144 | 145 | for (i < endIndex) && (j < replacementSize) { 146 | buffer[i] = replacement[j] 147 | j++ 148 | i++ 149 | } 150 | 151 | // pad with zeroes 152 | for i < endIndex { 153 | buffer[i] = byte(0) 154 | i++ 155 | } 156 | 157 | log.Printf("Replaced \"%s\" %v to \"%s\" %v", key, key, replacement, replacement) 158 | } 159 | 160 | func replaceVariable(buffer []byte, varname, varvalue string) { 161 | replaceInBuffer(buffer, []byte(varname), []byte(varvalue)) 162 | } 163 | -------------------------------------------------------------------------------- /tests/TestApp/TestApp.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = app 2 | 3 | QT += qml quick svg 4 | CONFIG += c++11 5 | 6 | INCLUDEPATH += $$PWD/../TestLib/ 7 | LIBS += -L$$PWD/../TestLib/build-Debug/ 8 | LIBS += -L$$PWD/../TestLib/ 9 | LIBS += -ltestlib 10 | 11 | SOURCES += main.cpp 12 | RESOURCES += qml.qrc 13 | 14 | # Additional import path used to resolve QML modules in Qt Creator's code model 15 | QML_IMPORT_PATH = 16 | 17 | # Default rules for deployment. 18 | include(deployment.pri) 19 | -------------------------------------------------------------------------------- /tests/TestApp/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/TestApp/deployment.pri: -------------------------------------------------------------------------------- 1 | unix:!android { 2 | isEmpty(target.path) { 3 | qnx { 4 | target.path = /tmp/$${TARGET}/bin 5 | } else { 6 | target.path = /opt/$${TARGET}/bin 7 | } 8 | export(target.path) 9 | } 10 | INSTALLS += target 11 | } 12 | 13 | export(INSTALLS) 14 | -------------------------------------------------------------------------------- /tests/TestApp/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QGuiApplication app(argc, argv); 10 | 11 | QQmlApplicationEngine engine; 12 | QQmlContext *rootContext = engine.rootContext(); 13 | int magicNumber = TestLib().getMagicNumber(); 14 | rootContext->setContextProperty("magicNumber", magicNumber); 15 | 16 | engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); 17 | 18 | std::cout << "Loaded main.qml" << std::endl; 19 | 20 | return app.exec(); 21 | } 22 | -------------------------------------------------------------------------------- /tests/TestApp/main.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Window 2.2 3 | 4 | Window { 5 | visible: true 6 | width: 600 7 | height: 400 8 | 9 | MouseArea { 10 | anchors.fill: parent 11 | onClicked: { 12 | Qt.quit(); 13 | } 14 | } 15 | 16 | Image { 17 | source: "qrc:/delete.svg" 18 | sourceSize.width: 400 19 | sourceSize.height: 400 20 | anchors.centerIn: parent 21 | } 22 | 23 | Text { 24 | text: qsTr("Hello World") + magicNumber 25 | anchors.centerIn: parent 26 | color: "#ffffff" 27 | } 28 | 29 | Timer { 30 | interval: 2000 31 | repeat: false 32 | onTriggered: Qt.quit() 33 | running: true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/TestApp/qml.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | main.qml 4 | delete.svg 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/TestLib/TestLib.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2017-05-29T20:07:50 4 | # 5 | #------------------------------------------------- 6 | 7 | QT -= core gui 8 | 9 | TARGET = testlib 10 | TEMPLATE = lib 11 | 12 | DEFINES += TESTLIB_LIBRARY 13 | 14 | SOURCES += testlib.cpp 15 | 16 | HEADERS += testlib.h\ 17 | testlib_global.h 18 | 19 | unix { 20 | target.path = /usr/lib 21 | INSTALLS += target 22 | } 23 | -------------------------------------------------------------------------------- /tests/TestLib/testlib.cpp: -------------------------------------------------------------------------------- 1 | #include "testlib.h" 2 | 3 | TestLib::TestLib() 4 | { 5 | } 6 | 7 | int TestLib::getMagicNumber() { 8 | return 42; 9 | } 10 | -------------------------------------------------------------------------------- /tests/TestLib/testlib.h: -------------------------------------------------------------------------------- 1 | #ifndef TESTLIB_H 2 | #define TESTLIB_H 3 | 4 | #include "testlib_global.h" 5 | 6 | class TESTLIBSHARED_EXPORT TestLib 7 | { 8 | 9 | public: 10 | TestLib(); 11 | 12 | public: 13 | int getMagicNumber(); 14 | }; 15 | 16 | #endif // TESTLIB_H 17 | -------------------------------------------------------------------------------- /tests/TestLib/testlib_global.h: -------------------------------------------------------------------------------- 1 | #ifndef TESTLIB_GLOBAL_H 2 | #define TESTLIB_GLOBAL_H 3 | 4 | #ifdef _WIN32 5 | # if defined(LIBAVTHUMBNAILER_LIBRARY) 6 | # define TESTLIBSHARED_EXPORT __declspec(dllexport) 7 | # else 8 | # define TESTLIBSHARED_EXPORT __declspec(dllimport) 9 | # endif 10 | #else 11 | # define TESTLIBSHARED_EXPORT 12 | #endif 13 | 14 | #endif // TESTLIB_GLOBAL_H 15 | -------------------------------------------------------------------------------- /tests/deploy_and_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd src 4 | 5 | APP_DIR=TestApp.AppDir1 6 | 7 | ./linuxdeploy -exe ../tests/TestApp/TestApp -appdir $APP_DIR -overwrite -libs ../tests/TestLib/ -qmldir ../tests/TestApp/ -log linuxdeploy.log 8 | 9 | rc=$?; if [[ $rc != 0 ]]; then exit 1; fi 10 | 11 | until xset -q 12 | do 13 | echo "Waiting for X server to start..." 14 | sleep 1; 15 | done 16 | 17 | echo "Deployed all needed libraries. Trying to launch..." 18 | 19 | pushd $APP_DIR 20 | 21 | unset QT_PLUGIN_PATH 22 | unset LD_LIBRARY_PATH 23 | unset QTDIR 24 | 25 | export QML_IMPORT_TRACE=1 26 | export QT_DEBUG_PLUGINS=1 27 | 28 | LD_DEBUG=libs ./TestApp 29 | rc=$? 30 | 31 | echo "Application finished with code $rc." 32 | 33 | popd # appdir 34 | 35 | popd # src 36 | 37 | if [[ $rc != 0 ]]; then exit 1; fi 38 | -------------------------------------------------------------------------------- /tests/deploy_and_run_blacklist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd src 4 | 5 | APP_DIR=TestApp.AppDir2 6 | 7 | ./linuxdeploy -exe ../tests/TestApp/TestApp -default-blacklist -appdir $APP_DIR -overwrite -libs ../tests/TestLib/ -qmldir ../tests/TestApp/ -log linuxdeploy_blacklist.log 8 | 9 | rc=$?; if [[ $rc != 0 ]]; then exit 1; fi 10 | 11 | until xset -q 12 | do 13 | echo "Waiting for X server to start..." 14 | sleep 1; 15 | done 16 | 17 | echo "Deployed all needed libraries. Trying to launch..." 18 | 19 | pushd $APP_DIR 20 | 21 | unset QT_PLUGIN_PATH 22 | unset LD_LIBRARY_PATH 23 | unset QTDIR 24 | 25 | export QML_IMPORT_TRACE=1 26 | export QT_DEBUG_PLUGINS=1 27 | 28 | LD_DEBUG=libs ./TestApp 29 | rc=$? 30 | 31 | echo "Application finished with code $rc." 32 | 33 | popd # appdir 34 | 35 | popd # src 36 | 37 | if [[ $rc != 0 ]]; then exit 1; fi 38 | --------------------------------------------------------------------------------