├── src ├── ui │ ├── first-run.h │ ├── resources.qrc │ ├── settings_main.cpp │ ├── settings_dialog.h │ ├── CMakeLists.txt │ ├── settings_dialog.cpp │ ├── remove.ui │ ├── remove_main.cpp │ ├── settings_dialog.ui │ ├── first-run.cpp │ ├── first-run.ui │ ├── update_main.cpp │ └── main.cpp ├── cli │ ├── logging │ │ ├── CMakeLists.txt │ │ └── logging.h │ ├── commands │ │ ├── CMakeLists.txt │ │ ├── IntegrateCommand.h │ │ ├── CommandFactory.h │ │ ├── Command.h │ │ ├── CommandFactory.cpp │ │ ├── exceptions.h │ │ └── IntegrateCommand.cpp │ ├── CMakeLists.txt │ └── cli_main.cpp ├── trashbin │ ├── CMakeLists.txt │ ├── trashbin.h │ └── trashbin.cpp ├── fswatcher │ ├── CMakeLists.txt │ ├── filesystemwatcher.h │ └── filesystemwatcher.cpp ├── i18n │ ├── CMakeLists.txt │ ├── translationmanager.h │ └── translationmanager.cpp ├── daemon │ ├── CMakeLists.txt │ ├── worker.h │ ├── main.cpp │ └── worker.cpp ├── shared │ ├── CMakeLists.txt │ └── shared.h ├── fusefs │ ├── CMakeLists.txt │ ├── fs.h │ ├── error.h │ └── main.cpp └── CMakeLists.txt ├── resources ├── doc │ └── screenshot.png ├── icons │ ├── hicolor │ │ ├── 128x128 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 160x160 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 16x16 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 192x192 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 256x256 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 32x32 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 384x384 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 512x512 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ ├── 64x64 │ │ │ └── apps │ │ │ │ └── AppImageLauncher.png │ │ └── scalable │ │ │ └── apps │ │ │ └── AppImageLauncher.svg │ ├── generate-icons.sh │ └── AppImageLauncher_inkscape.svg ├── binfmt.d │ └── appimage.conf.in ├── appimagelauncher-lite.desktop ├── appimagelauncherd.service.in ├── appimagelaunchersettings.desktop ├── appimagelauncher.desktop ├── appimagelauncherfs.service.in ├── install-scripts │ ├── post-uninstall.in │ └── post-install.in ├── AppImageLauncher.1.in ├── mime │ └── packages │ │ └── appimage.xml ├── CMakeLists.txt └── appimagelauncher-lite-AppRun.sh ├── i18n ├── desktopfiles.zh_Hans.json ├── desktopfiles.en.json ├── desktopfiles.es.json ├── desktopfiles.pt.json ├── desktopfiles.ru.json ├── desktopfiles.de.json ├── desktopfiles.pt_PT.json ├── desktopfiles.fr.json ├── auto-translate.py └── CMakeLists.txt ├── .gitignore ├── .idea └── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── .gitmodules ├── cmake ├── modules │ ├── FindINotify.cmake │ └── FindFUSE.cmake ├── cpack_source.cmake ├── cpack_general.cmake ├── scripts.cmake ├── reproducible_builds.cmake ├── toolchains │ └── i386-linux-gnu.cmake ├── cpack_rpm.cmake ├── versioning.cmake ├── cpack_deb.cmake └── install.cmake ├── travis ├── Dockerfile.build-bionic ├── Dockerfile.build-cosmic ├── Dockerfile.build-xenial ├── travis-docker.sh ├── Dockerfile.build-cosmic-i386-cross ├── Dockerfile.build-bionic-i386-cross ├── Dockerfile.build-xenial-i386-cross ├── travis-build.sh └── build-lite.sh ├── LICENSE.txt ├── lib └── CMakeLists.txt ├── .travis.yml ├── CMakeLists.txt └── README.md /src/ui/first-run.h: -------------------------------------------------------------------------------- 1 | void showFirstRunDialog(); 2 | -------------------------------------------------------------------------------- /resources/doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/doc/screenshot.png -------------------------------------------------------------------------------- /i18n/desktopfiles.zh_Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "从系统中移除 AppImage", 3 | "Desktop Action update/Name": "更新 AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/desktopfiles.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Remove AppImage from system", 3 | "Desktop Action update/Name": "Update AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-debug*/ 2 | build*/ 3 | *.AppImage* 4 | *.deb* 5 | *.tar* 6 | cmake/GIT_COMMIT 7 | squashfs-root/ 8 | *.qm 9 | resources/l10n/* 10 | -------------------------------------------------------------------------------- /i18n/desktopfiles.es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Eliminar Appimage del Sistema", 3 | "Desktop Action update/Name": "Actualizar Appimage" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/desktopfiles.pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Remover AppImage do sistema", 3 | "Desktop Action update/Name": "Actualizar AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/desktopfiles.ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Удалить AppImage из системы", 3 | "Desktop Action update/Name": "Обновить AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/desktopfiles.de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "AppImage vom System entfernen", 3 | "Desktop Action update/Name": "AppImage aktualisieren" 4 | } 5 | -------------------------------------------------------------------------------- /i18n/desktopfiles.pt_PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Remover AppImage do sistema", 3 | "Desktop Action update/Name": "Actualizar AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /src/cli/logging/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(cli_logging INTERFACE) 2 | set_property(TARGET cli_logging PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}) 3 | -------------------------------------------------------------------------------- /i18n/desktopfiles.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Desktop Action remove/Name": "Supprimer l'AppImage du système", 3 | "Desktop Action update/Name": "Mettre à jour l'AppImage" 4 | } 5 | -------------------------------------------------------------------------------- /resources/icons/hicolor/128x128/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/128x128/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/160x160/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/160x160/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/16x16/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/16x16/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/192x192/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/192x192/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/256x256/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/256x256/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/32x32/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/32x32/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/384x384/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/384x384/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/512x512/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/512x512/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /resources/icons/hicolor/64x64/apps/AppImageLauncher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/probonopd/AppImageLauncher/master/resources/icons/hicolor/64x64/apps/AppImageLauncher.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /resources/binfmt.d/appimage.conf.in: -------------------------------------------------------------------------------- 1 | # Launch AppImages with AppImageLauncher 2 | :appimage-type1:M:8:AI\x01::@CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher: 3 | :appimage-type2:M:8:AI\x02::@CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher: 4 | -------------------------------------------------------------------------------- /src/ui/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ../../resources/icons/hicolor/scalable/apps/AppImageLauncher.svg 5 | 6 | 7 | -------------------------------------------------------------------------------- /resources/appimagelauncher-lite.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=appimagelauncher-lite 3 | Exec=appimagelauncher-lite 4 | Icon=AppImageLauncher 5 | Type=Application 6 | X-AppImage-Integrate=false 7 | NoDisplay=true 8 | Categories=Utility; 9 | -------------------------------------------------------------------------------- /src/trashbin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(trashbin STATIC trashbin.cpp trashbin.h) 2 | target_link_libraries(trashbin PUBLIC Qt5::Core libappimage shared) 3 | target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/AppImageUpdate"] 2 | path = lib/AppImageUpdate 3 | url = https://github.com/AppImage/AppImageUpdate.git 4 | [submodule "lib/libappimage"] 5 | path = lib/libappimage 6 | url = https://github.com/AppImage/libappimage.git 7 | -------------------------------------------------------------------------------- /src/fswatcher/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(filesystemwatcher STATIC filesystemwatcher.cpp filesystemwatcher.h) 2 | target_link_libraries(filesystemwatcher PUBLIC Qt5::Core) 3 | target_include_directories(filesystemwatcher PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 4 | -------------------------------------------------------------------------------- /resources/appimagelauncherd.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AppImageLauncher daemon 3 | 4 | [Service] 5 | ExecStart=@CMAKE_INSTALL_PREFIX@/bin/appimagelauncherd 6 | Restart=on-failure 7 | RestartSec=10 8 | 9 | [Install] 10 | WantedBy=default.target 11 | -------------------------------------------------------------------------------- /src/ui/settings_main.cpp: -------------------------------------------------------------------------------- 1 | // libraries 2 | #include 3 | 4 | // local 5 | #include "settings_dialog.h" 6 | 7 | int main(int argc, char** argv) { 8 | QApplication app(argc, argv); 9 | SettingsDialog dialog; 10 | dialog.show(); 11 | 12 | return app.exec(); 13 | } 14 | -------------------------------------------------------------------------------- /src/i18n/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(translationmanager translationmanager.cpp translationmanager.h) 2 | target_link_libraries(translationmanager PUBLIC Qt5::Core Qt5::Widgets) 3 | target_include_directories(translationmanager PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 4 | add_dependencies(translationmanager l10n) 5 | -------------------------------------------------------------------------------- /resources/appimagelaunchersettings.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Exec=AppImageLauncherSettings %f 5 | Name=AppImageLauncher Settings 6 | Icon=AppImageLauncher 7 | Terminal=false 8 | Categories=Utility; 9 | X-AppImage-Integrate=false 10 | StartupWMClass=AppImageLauncher 11 | -------------------------------------------------------------------------------- /src/cli/commands/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(cli_commands STATIC Command.h CommandFactory.cpp CommandFactory.h IntegrateCommand.cpp IntegrateCommand.h exceptions.h) 2 | target_link_libraries(cli_commands PUBLIC Qt5::Core shared cli_logging) 3 | target_include_directories(cli_commands PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 4 | -------------------------------------------------------------------------------- /src/cli/logging/logging.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system headers 4 | #include 5 | 6 | // library headers 7 | #include 8 | #include 9 | 10 | // wrapper for stdout 11 | #define qout() QTextStream(stdout, QIODevice::WriteOnly) 12 | 13 | // wrapper for stderr 14 | #define qerr() QTextStream(stderr, QIODevice::WriteOnly) 15 | -------------------------------------------------------------------------------- /resources/appimagelauncher.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Type=Application 4 | Exec=AppImageLauncher %f 5 | Name=AppImageLauncher 6 | Icon=AppImageLauncher 7 | Terminal=false 8 | Categories=Utility; 9 | MimeType=application/x-iso9660-appimage;application/x-appimage;application/vnd.appimage; 10 | NoDisplay=true 11 | X-AppImage-Integrate=false 12 | StartupWMClass=AppImageLauncher 13 | -------------------------------------------------------------------------------- /src/cli/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_subdirectory(logging) 2 | 3 | add_subdirectory(commands) 4 | 5 | add_executable(ail-cli cli_main.cpp) 6 | target_link_libraries(ail-cli PUBLIC cli_commands cli_logging) 7 | 8 | set_property( 9 | TARGET ail-cli 10 | PROPERTY INSTALL_RPATH ${_rpath} 11 | ) 12 | 13 | install( 14 | TARGETS ail-cli 15 | DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER_CLI 16 | ) 17 | -------------------------------------------------------------------------------- /src/daemon/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # daemon binary 2 | add_executable(appimagelauncherd main.cpp worker.cpp worker.h) 3 | target_link_libraries(appimagelauncherd shared filesystemwatcher PkgConfig::glib libappimage) 4 | set_target_properties(appimagelauncherd PROPERTIES INSTALL_RPATH ${_rpath}) 5 | 6 | install( 7 | TARGETS 8 | appimagelauncherd 9 | RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER 10 | LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER 11 | ) 12 | -------------------------------------------------------------------------------- /src/cli/commands/IntegrateCommand.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // local headers 4 | #include "Command.h" 5 | 6 | namespace appimagelauncher { 7 | namespace cli { 8 | namespace commands { 9 | /** 10 | * Integrates AppImages passed as arguments on the commandline. 11 | */ 12 | class IntegrateCommand : public Command { 13 | void exec(QList arguments) final; 14 | }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cmake/modules/FindINotify.cmake: -------------------------------------------------------------------------------- 1 | find_path(INOTIFY_INCLUDE_DIR sys/inotify.h PATH_SUFFIXES inotify) 2 | find_library(INOTIFY_LIBRARY inotify) 3 | 4 | include(FindPackageHandleStandardArgs) 5 | find_package_handle_standard_args(INotify DEFAULT_MSG INOTIFY_INCLUDE_DIR) 6 | 7 | IF(INOTIFY_FOUND) 8 | set(INotify_INCLUDE_DIRS ${INOTIFY_INCLUDE_DIR}) 9 | 10 | add_library(inotify IMPORTED INTERFACE) 11 | set_property(TARGET inotify PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${INOTIFY_INCLUDE_DIRS}) 12 | ENDIF(INOTIFY_FOUND) 13 | -------------------------------------------------------------------------------- /src/shared/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(shared STATIC shared.h shared.cpp) 2 | target_link_libraries(shared PUBLIC PkgConfig::glib Qt5::Core Qt5::Widgets Qt5::DBus libappimage translationmanager trashbin) 3 | if(ENABLE_UPDATE_HELPER) 4 | target_link_libraries(shared PUBLIC libappimageupdate) 5 | endif() 6 | target_compile_definitions(shared 7 | PRIVATE -DPRIVATE_LIBDIR="${_private_libdir}" 8 | PRIVATE -DCMAKE_PROJECT_SOURCE_DIR="${PROJECT_SOURCE_DIR}" 9 | ) 10 | target_include_directories(shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 11 | -------------------------------------------------------------------------------- /src/cli/commands/CommandFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system headers 4 | #include 5 | 6 | // library headers 7 | #include 8 | 9 | // local headers 10 | #include 11 | 12 | /** 13 | * Creates Commands. 14 | */ 15 | 16 | namespace appimagelauncher { 17 | namespace cli { 18 | namespace commands { 19 | class CommandFactory { 20 | public: 21 | static std::shared_ptr getCommandByName(const QString&); 22 | }; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/appimagelauncherfs.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=AppImageLauncherFS daemon 3 | Before=display-manager.service 4 | 5 | [Service] 6 | Type=exec 7 | ExecStart=@CMAKE_INSTALL_PREFIX@/@CMAKE_INSTALL_BINDIR@/appimagelauncherfs 8 | Restart=always 9 | RestartSec=2 10 | 11 | [Install] 12 | # multi-user.target etc. seem to be buggy on some systems, it may not even exist on some systems 13 | # can be checked with systemctl --user list-units --type target 14 | # default seems to work better -- thanks @smac89 for the hint 15 | WantedBy=default.target 16 | -------------------------------------------------------------------------------- /src/cli/commands/Command.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system headers 4 | #include 5 | 6 | // library headers 7 | #include 8 | #include 9 | 10 | namespace appimagelauncher { 11 | namespace cli { 12 | namespace commands { 13 | /** 14 | * Base class for CLI command implementations. 15 | */ 16 | class Command { 17 | public: 18 | // Run the command. 19 | virtual void exec(QList arguments) = 0; 20 | }; 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/cli/commands/CommandFactory.cpp: -------------------------------------------------------------------------------- 1 | // local headers 2 | #include "CommandFactory.h" 3 | #include "IntegrateCommand.h" 4 | #include "exceptions.h" 5 | 6 | 7 | namespace appimagelauncher { 8 | namespace cli { 9 | namespace commands { 10 | std::shared_ptr CommandFactory::getCommandByName(const QString& commandName) { 11 | if (commandName == "integrate") 12 | return std::shared_ptr(new IntegrateCommand); 13 | 14 | throw CommandNotFoundError(commandName); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/daemon/worker.h: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | 4 | // library includes 5 | #include 6 | 7 | #pragma once 8 | 9 | class Worker : public QObject { 10 | Q_OBJECT 11 | 12 | private: 13 | class PrivateData; 14 | std::shared_ptr d = nullptr; 15 | 16 | public: 17 | Worker(); 18 | 19 | signals: 20 | void startTimer(); 21 | 22 | public slots: 23 | void scheduleForIntegration(const QString& path); 24 | void scheduleForUnintegration(const QString& path); 25 | 26 | public slots: 27 | void executeDeferredOperations(); 28 | 29 | private slots: 30 | void startTimerIfNecessary(); 31 | }; 32 | -------------------------------------------------------------------------------- /src/i18n/translationmanager.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // library includes 4 | #include 5 | #include 6 | #include 7 | 8 | /* 9 | * Installs translations for AppImageLauncher UIs in a Qt application. 10 | * 11 | * You need to keep instances of this alive until the application is terminated. 12 | */ 13 | class TranslationManager { 14 | private: 15 | const QCoreApplication& app; 16 | QList installedTranslators; 17 | 18 | public: 19 | explicit TranslationManager(QCoreApplication& app); 20 | ~TranslationManager(); 21 | 22 | public: 23 | // get translation dir 24 | static QString getTranslationDir(); 25 | }; 26 | -------------------------------------------------------------------------------- /src/trashbin/trashbin.h: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | 4 | #pragma once 5 | 6 | class TrashBin { 7 | private: 8 | class PrivateData; 9 | PrivateData* d; 10 | 11 | public: 12 | TrashBin(); 13 | 14 | public: 15 | QString path(); 16 | 17 | public: 18 | // move AppImage into trash bin directory 19 | bool disposeAppImage(const QString& pathToAppImage); 20 | 21 | // check all AppImages in trash bin whether they can be removed 22 | // this function should be called regularly to make sure the files in the trash bin are cleaned up as soon 23 | // as possible 24 | bool cleanUp(); 25 | }; 26 | -------------------------------------------------------------------------------- /cmake/modules/FindFUSE.cmake: -------------------------------------------------------------------------------- 1 | find_path(FUSE_INCLUDE_DIR fuse.h 2 | /usr/include 3 | /usr/include/x86_64-linux-gnu 4 | /usr/include/i386-linux-gnu 5 | ) 6 | 7 | find_library(FUSE_LIBRARY fuse) 8 | 9 | include(FindPackageHandleStandardArgs) 10 | find_package_handle_standard_args("FUSE" DEFAULT_MSG FUSE_INCLUDE_DIR FUSE_LIBRARY) 11 | 12 | mark_as_advanced(FUSE_INCLUDE_DIR FUSE_LIBRARY) 13 | 14 | message(STATUS "Found FUSE: ${FUSE_LIBRARY} (include dirs: ${FUSE_INCLUDE_DIR})") 15 | 16 | add_library(libfuse IMPORTED SHARED) 17 | set_property(TARGET libfuse PROPERTY INTERFACE_INCLUDE_DIRECTORIES "${FUSE_INCLUDE_DIR}") 18 | set_property(TARGET libfuse PROPERTY IMPORTED_LOCATION "${FUSE_LIBRARY}") 19 | -------------------------------------------------------------------------------- /src/ui/settings_dialog.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system 4 | #include 5 | 6 | // libraries 7 | #include 8 | #include 9 | 10 | 11 | namespace Ui { 12 | class SettingsDialog; 13 | } 14 | 15 | class SettingsDialog : public QDialog { 16 | Q_OBJECT 17 | 18 | public: 19 | explicit SettingsDialog(QWidget* parent = nullptr); 20 | 21 | ~SettingsDialog() override; 22 | 23 | protected slots: 24 | void onChooseAppsDirClicked(); 25 | 26 | void onDialogAccepted(); 27 | 28 | private: 29 | void loadSettings(); 30 | 31 | void saveSettings(); 32 | 33 | void toggleDaemon(); 34 | 35 | Ui::SettingsDialog* ui; 36 | std::shared_ptr settingsFile; 37 | }; 38 | -------------------------------------------------------------------------------- /src/fusefs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # use latest FUSE API 2 | add_definitions(-DFUSE_USE_VERSION=26) 3 | # required by FUSE 4 | add_definitions(-D_FILE_OFFSET_BITS=64) 5 | 6 | if(NOT TARGET libfuse) 7 | find_package(FUSE REQUIRED) 8 | endif() 9 | 10 | set(Boost_USE_STATIC_LIBS ON) 11 | find_package(Boost REQUIRED COMPONENTS filesystem) 12 | 13 | add_executable(appimagelauncherfs main.cpp fs.cpp fs.h error.h) 14 | target_link_libraries(appimagelauncherfs PUBLIC libfuse Boost::filesystem) 15 | 16 | # ISO C++ spawns annoying warnings about string literals 17 | target_compile_options(appimagelauncherfs PRIVATE -Wno-write-strings) 18 | 19 | install( 20 | TARGETS appimagelauncherfs COMPONENT APPIMAGELAUNCHERFS 21 | RUNTIME DESTINATION ${_bindir} 22 | ) 23 | -------------------------------------------------------------------------------- /resources/install-scripts/post-uninstall.in: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo "Removing AppImageLauncher as interpreter for AppImages" 4 | 5 | UPDATE_BINFMTS=update-binfmts 6 | 7 | if ! (type "$UPDATE_BINFMTS" &>/dev/null) && ! (which "$UPDATE_BINFMTS" &>/dev/null); then 8 | UPDATE_BINFMTS=@CMAKE_INSTALL_PREFIX@/lib/appimagelauncher/update-binfmts 9 | 10 | if [ ! -f "$UPDATE_BINFMTS" ]; then 11 | UPDATE_BINFMTS= 12 | fi 13 | fi 14 | 15 | if [ "$UPDATE_BINFMTS" != "" ]; then 16 | (set -x; "$UPDATE_BINFMTS" --package appimage --remove appimage-type1 @CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher) 17 | (set -x; "$UPDATE_BINFMTS" --package appimage --remove appimage-type2 @CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher) 18 | else 19 | (set -x; systemctl restart systemd-binfmt) 20 | fi 21 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | 14 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /resources/icons/generate-icons.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | if ! (which inkscape &>/dev/null); then 6 | echo "Error: this tool requires inkscape" >&2 7 | exit 1 8 | fi 9 | 10 | cd $(dirname "$0") 11 | 12 | # create directory structure 13 | mkdir -p hicolor/scalable/apps 14 | inkscape --export-plain-svg=hicolor/scalable/apps/AppImageLauncher.svg AppImageLauncher_inkscape.svg 15 | 16 | for res in 16 32 64 128 160 192 256 384 512; do 17 | fullres="${res}x${res}" 18 | echo "Generating PNG for resolution $fullres" 19 | 20 | mkdir -p hicolor/"$fullres"/apps 21 | 22 | inkscape -w "$res" -h "$res" -e hicolor/"$fullres"/apps/AppImageLauncher.png AppImageLauncher_inkscape.svg 23 | # rsvg-convert -w "$res" -h "$res" AppImageLauncher_inkscape.svg > hicolor/"$fullres"/apps/AppImageLauncher.png 24 | done 25 | -------------------------------------------------------------------------------- /cmake/cpack_source.cmake: -------------------------------------------------------------------------------- 1 | # source package options 2 | 3 | # ignore files 4 | # requires four backslashes to escape dots etc., probably because the values are evaluated twice, 5 | # once by CMake and then by CPack 6 | set(CPACK_SOURCE_IGNORE_FILES 7 | # warning: the following pattern requires at least 6 characters before the "build" part to work as intended 8 | "([^t][^r][^a][^v][^i][^s])/.*build.*/" 9 | "/.*-prefix/" 10 | "\\\\..*AppImage$" 11 | "\\\\..*AppImage\\\\..*$" 12 | "\\\\..*zs-old.*$" 13 | "\\\\..*tar.*$" 14 | "\\\\..*deb.*$" 15 | "/.git/" 16 | "/.idea/" 17 | ) 18 | 19 | # source tarball filename (without extension) 20 | set(CPACK_SOURCE_PACKAGE_FILE_NAME appimagelauncher-git${APPIMAGELAUNCHER_GIT_COMMIT_DATE_SHORT}.${APPIMAGELAUNCHER_GIT_COMMIT}.source) 21 | 22 | # build .tar.xz archives to get the maximum amount of compression and produce small tarballs 23 | set(CPACK_SOURCE_GENERATOR TXZ) 24 | -------------------------------------------------------------------------------- /i18n/auto-translate.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | import sys 4 | 5 | from xml.etree import ElementTree as ET 6 | 7 | 8 | def copy_sources_to_translations(root: ET.Element): 9 | for context in root.findall("context"): 10 | for message in context.findall("message"): 11 | source = message.find("source") 12 | translation = message.find("translation") 13 | 14 | translation.text = source.text 15 | 16 | if "type" in translation.attrib: 17 | if translation.attrib["type"] in ("", "unfinished"): 18 | del translation.attrib["type"] 19 | 20 | 21 | def main(): 22 | with open(sys.argv[1]) as f: 23 | et = ET.parse(f) 24 | 25 | root = et.getroot() 26 | 27 | copy_sources_to_translations(root) 28 | 29 | with open(sys.argv[1], "wb") as f: 30 | f.write(ET.tostring(root)) 31 | 32 | 33 | if __name__ == "__main__": 34 | main() 35 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-bionic: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM ubuntu:bionic 5 | 6 | RUN apt-get update && \ 7 | apt-get -y --no-install-recommends install qt5-default \ 8 | qtbase5-dev \ 9 | qt5-qmake \ 10 | libcurl4-openssl-dev \ 11 | libfuse-dev \ 12 | desktop-file-utils \ 13 | libglib2.0-dev \ 14 | libcairo2-dev \ 15 | libssl-dev \ 16 | ca-certificates \ 17 | libbsd-dev \ 18 | qttools5-dev-tools \ 19 | \ 20 | gcc \ 21 | g++ \ 22 | cmake \ 23 | make \ 24 | git \ 25 | automake \ 26 | autoconf \ 27 | libtool \ 28 | patch \ 29 | wget \ 30 | vim-common \ 31 | desktop-file-utils \ 32 | pkg-config \ 33 | libarchive-dev \ 34 | libboost-filesystem-dev 35 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-cosmic: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM ubuntu:cosmic 5 | 6 | RUN apt-get update && \ 7 | apt-get -y --no-install-recommends install qt5-default \ 8 | qtbase5-dev \ 9 | qt5-qmake \ 10 | libcurl4-openssl-dev \ 11 | libfuse-dev \ 12 | desktop-file-utils \ 13 | libglib2.0-dev \ 14 | libcairo2-dev \ 15 | libssl-dev \ 16 | ca-certificates \ 17 | libbsd-dev \ 18 | qttools5-dev-tools \ 19 | \ 20 | gcc \ 21 | g++ \ 22 | cmake \ 23 | make \ 24 | git \ 25 | automake \ 26 | autoconf \ 27 | libtool \ 28 | patch \ 29 | wget \ 30 | vim-common \ 31 | desktop-file-utils \ 32 | pkg-config \ 33 | libarchive-dev \ 34 | libboost-filesystem-dev 35 | -------------------------------------------------------------------------------- /src/fswatcher/filesystemwatcher.h: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | 4 | // library includes 5 | #include 6 | #include 7 | #include 8 | 9 | #pragma once 10 | 11 | class FileSystemWatcherError : public std::runtime_error { 12 | public: 13 | FileSystemWatcherError(const QString& message) : std::runtime_error(message.toStdString().c_str()) {}; 14 | }; 15 | 16 | class FileSystemWatcher : public QObject { 17 | Q_OBJECT 18 | 19 | private: 20 | class PrivateData; 21 | std::shared_ptr d; 22 | 23 | public: 24 | explicit FileSystemWatcher(const QString& path); 25 | explicit FileSystemWatcher(const QStringList& paths); 26 | FileSystemWatcher(); 27 | 28 | public: 29 | bool startWatching(); 30 | bool stopWatching(); 31 | 32 | public slots: 33 | void readEvents(); 34 | 35 | public: 36 | QStringList directories(); 37 | 38 | signals: 39 | void fileChanged(QString path); 40 | void fileRemoved(QString path); 41 | }; 42 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-xenial: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM ubuntu:bionic 5 | 6 | RUN apt-get update && \ 7 | apt-get -y --no-install-recommends install qt5-default \ 8 | qtbase5-dev \ 9 | qt5-qmake \ 10 | libcurl4-openssl-dev \ 11 | libfuse-dev \ 12 | desktop-file-utils \ 13 | libglib2.0-dev \ 14 | libcairo2-dev \ 15 | libssl-dev \ 16 | ca-certificates \ 17 | libbsd-dev \ 18 | qttools5-dev-tools \ 19 | \ 20 | rpm \ 21 | gcc \ 22 | g++ \ 23 | cmake \ 24 | make \ 25 | git \ 26 | automake \ 27 | autoconf \ 28 | libtool \ 29 | patch \ 30 | wget \ 31 | vim-common \ 32 | desktop-file-utils \ 33 | pkg-config \ 34 | libarchive-dev \ 35 | libboost-filesystem-dev 36 | -------------------------------------------------------------------------------- /travis/travis-docker.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ "$1" == "" ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | set -x 9 | set -e 10 | 11 | export DOCKER_DIST="$1" 12 | 13 | cd $(readlink -f $(dirname "$0")) 14 | 15 | IMAGE=appimagelauncher-build:"$DOCKER_DIST" 16 | DOCKERFILE=Dockerfile.build-"$DOCKER_DIST" 17 | 18 | if [ ! -f "$DOCKERFILE" ]; then 19 | echo "Error: $DOCKERFILE could not be found" 20 | exit 1 21 | fi 22 | 23 | if [ "$ARCH" == "i386" ]; then 24 | IMAGE=appimagelauncher-build:"$DOCKER_DIST"-i386-cross 25 | DOCKERFILE=Dockerfile.build-"$DOCKER_DIST"-i386-cross 26 | fi 27 | 28 | docker build -t "$IMAGE" -f "$DOCKERFILE" . 29 | 30 | if [[ "$BUILD_LITE" == "" ]]; then 31 | build_script=travis-build.sh 32 | else 33 | build_script=build-lite.sh 34 | fi 35 | 36 | docker run -e ARCH -e TRAVIS_BUILD_NUMBER --rm -it -v $(readlink -f ..):/ws "$IMAGE" \ 37 | bash -xc "export CI=1 && export DEBIAN_DIST=\"$DOCKER_DIST\" && cd /ws && source travis/$build_script" 38 | -------------------------------------------------------------------------------- /resources/install-scripts/post-install.in: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | echo "Installing AppImageLauncher as interpreter for AppImages" 4 | 5 | # as there's no _real_ package that we could use as a dependency to take care of the kernel module, 6 | # we need to make sure that the kernel module is loaded manually 7 | modprobe -v binfmt_misc 8 | 9 | UPDATE_BINFMTS=update-binfmts 10 | 11 | if ! (type "$UPDATE_BINFMTS" &>/dev/null) && ! (which "$UPDATE_BINFMTS" &>/dev/null); then 12 | UPDATE_BINFMTS=@CMAKE_INSTALL_PREFIX@/lib/appimagelauncher/update-binfmts 13 | 14 | if [ ! -f "$UPDATE_BINFMTS" ]; then 15 | UPDATE_BINFMTS= 16 | fi 17 | fi 18 | 19 | if [ "$UPDATE_BINFMTS" != "" ]; then 20 | mkdir -p /var/lib/binfmts 21 | 22 | (set -x; "$UPDATE_BINFMTS" --package appimage --install appimage-type1 @CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher --magic 'AI\x01' --offset 8) 23 | (set -x; "$UPDATE_BINFMTS" --package appimage --install appimage-type2 @CMAKE_INSTALL_PREFIX@/bin/AppImageLauncher --magic 'AI\x02' --offset 8) 24 | else 25 | (set -x; systemctl restart systemd-binfmt) 26 | fi 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2019 TheAssassin 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. 20 | -------------------------------------------------------------------------------- /src/fusefs/fs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | #include 6 | 7 | // library includes 8 | #include 9 | 10 | class AppImageLauncherFS { 11 | private: 12 | class PrivateData; 13 | std::shared_ptr d; 14 | 15 | static std::shared_ptr instance; 16 | 17 | // this class is a singleton 18 | // therefore, no public constructor, no copying, and no public constructor, but a getInstance() method 19 | private: 20 | // constructor doesn't take any arguments 21 | AppImageLauncherFS(); 22 | 23 | // private copy constructor = no copies 24 | AppImageLauncherFS(const AppImageLauncherFS&); 25 | // same goes for the assignment operator 26 | AppImageLauncherFS& operator=(const AppImageLauncherFS&); 27 | 28 | public: 29 | static std::shared_ptr getInstance(); 30 | 31 | public: 32 | // returns calculated mountpoint directory path 33 | std::string mountpoint(); 34 | 35 | // returns a FUSE style list of operations that can be passed to fuse_main etc. 36 | // you should prefer using run() 37 | std::shared_ptr operations() const; 38 | 39 | // runs filesystem with FUSE 40 | // returns exit code received from FUSE 41 | int run(); 42 | }; 43 | -------------------------------------------------------------------------------- /src/cli/commands/exceptions.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system headers 4 | #include 5 | 6 | // library headers 7 | #include 8 | 9 | namespace appimagelauncher { 10 | namespace cli { 11 | namespace commands { 12 | class CliError : public std::runtime_error { 13 | public: 14 | explicit CliError(const QString& message) : std::runtime_error(message.toStdString()) {} 15 | }; 16 | 17 | class CommandNotFoundError : public std::runtime_error { 18 | private: 19 | QString commandName; 20 | 21 | public: 22 | explicit CommandNotFoundError(const QString& commandName) : std::runtime_error( 23 | "No such command available: " + commandName.toStdString()), commandName(commandName) {} 24 | 25 | QString getCommandName() const { 26 | return commandName; 27 | } 28 | }; 29 | 30 | class UsageError : public CliError { 31 | public: 32 | using CliError::CliError; 33 | }; 34 | 35 | class InvalidArgumentsError : public UsageError { 36 | public: 37 | using UsageError::UsageError; 38 | }; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /cmake/cpack_general.cmake: -------------------------------------------------------------------------------- 1 | # general CPack options 2 | 3 | set(PROJECT_VERSION ${APPIMAGELAUNCHER_VERSION}) 4 | set(CPACK_GENERATOR "DEB") 5 | 6 | # make sure to only package APPIMAGELAUNCHER and APPIMAGELAUNCHERFS components 7 | set(CPACK_COMPONENTS_ALL APPIMAGELAUNCHER APPIMAGELAUNCHER_CLI) 8 | if(NOT USE_SYSTEM_APPIMAGELAUNCHERFS) 9 | list(APPEND CPACK_COMPONENTS_ALL APPIMAGELAUNCHERFS) 10 | endif() 11 | # package them all in a single package, otherwise cpack would generate one package per component by default 12 | # https://cmake.org/cmake/help/v3.0/module/CPackComponent.html#variable:CPACK_COMPONENTS_GROUPING 13 | set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE) 14 | 15 | # global options 16 | set(CPACK_PACKAGE_CONTACT "TheAssassin") 17 | set(CPACK_PACKAGE_HOMEPAGE "https://github.com/TheAssassin/AppImageLauncher") 18 | set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_SOURCE_DIR}/README.md") 19 | set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_SOURCE_DIR}/LICENSE.txt") 20 | 21 | # versioning 22 | # it appears setting CPACK_DEBIAN_PACKAGE_VERSION doesn't work, hence setting CPACK_PACKAGE_VERSION 23 | set(CPACK_PACKAGE_VERSION ${APPIMAGELAUNCHER_VERSION}) 24 | 25 | # TODO: insert some useful description 26 | set(CPACK_COMPONENT_APPIMAGELAUNCHER_PACKAGE_DESCRIPTION "AppImageLauncher") 27 | 28 | # find more suitable section for package 29 | set(CPACK_COMPONENT_APPIMAGELAUNCHER_PACKAGE_SECTION misc) 30 | -------------------------------------------------------------------------------- /lib/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # required for pkg-config module's imported target feature 2 | cmake_minimum_required(VERSION 3.6) 3 | 4 | # note for self: include libappimage before AppImageUpdate to use the version here, not the one shipped with AppImageUpdate 5 | # the AppImageUpdate submodule tends to be out of date 6 | if(NOT USE_SYSTEM_LIBAPPIMAGE) 7 | add_subdirectory(libappimage EXCLUDE_FROM_ALL) 8 | endif() 9 | 10 | # used by the update helper 11 | if(ENABLE_UPDATE_HELPER) 12 | add_subdirectory(AppImageUpdate EXCLUDE_FROM_ALL) 13 | endif() 14 | 15 | if(TRAVIS_BUILD) 16 | # TODO: use latest version of CMake once it contains the following change: 17 | # https://gitlab.kitware.com/cmake/cmake/commit/00a9d133fb2838ebb756d684659c5d51f577ede3 18 | # when this is available, the workaround here isn't necessary any more, instead install(TARGETS ...) can be used 19 | # and the BUILD_WITH_INSTALL_RPATH property doesn't need to be set any more 20 | message(WARNING "TRAVIS_BUILD is set to ON, binaries can only be tested after installation!") 21 | set(DEPS_RPATH "\$ORIGIN") 22 | set_property(TARGET libappimageupdate PROPERTY INSTALL_RPATH ${DEPS_RPATH}) 23 | set_property(TARGET libappimageupdate PROPERTY BUILD_WITH_INSTALL_RPATH ON) 24 | set_property(TARGET libappimageupdate-qt PROPERTY INSTALL_RPATH ${DEPS_RPATH}) 25 | set_property(TARGET libappimageupdate-qt PROPERTY BUILD_WITH_INSTALL_RPATH ON) 26 | endif() 27 | -------------------------------------------------------------------------------- /cmake/scripts.cmake: -------------------------------------------------------------------------------- 1 | # borrowed from libappimage 2 | if(NOT COMMAND check_program) 3 | function(check_program) 4 | set(keywords FORCE_PREFIX) 5 | set(oneValueArgs NAME) 6 | cmake_parse_arguments(ARG "${keywords}" "${oneValueArgs}" "" "${ARGN}") 7 | 8 | if(NOT ARG_NAME) 9 | message(FATAL_ERROR "NAME argument required for check_program") 10 | endif() 11 | 12 | if(TOOLS_PREFIX) 13 | set(prefix ${TOOLS_PREFIX}) 14 | endif() 15 | 16 | message(STATUS "Checking for program ${ARG_NAME}") 17 | 18 | string(TOUPPER ${ARG_NAME} name_upper) 19 | 20 | if(prefix) 21 | # try prefixed version first 22 | find_program(${name_upper} ${prefix}${ARG_NAME}) 23 | endif() 24 | 25 | # try non-prefixed version 26 | if(NOT ${name_upper}) 27 | if(TOOLS_PREFIX AND ARG_FORCE_PREFIX) 28 | message(FATAL_ERROR "TOOLS_PREFIX set, but could not find program with prefix in PATH (FORCE_PREFIX is set)") 29 | endif() 30 | 31 | find_program(${name_upper} ${ARG_NAME}) 32 | 33 | if(NOT ${name_upper}) 34 | message(FATAL_ERROR "Could not find required program ${ARG_NAME}.") 35 | endif() 36 | endif() 37 | 38 | message(STATUS "Found program ${ARG_NAME}: ${${name_upper}}") 39 | 40 | mark_as_advanced(${name_upper}) 41 | endfunction() 42 | endif() 43 | -------------------------------------------------------------------------------- /cmake/reproducible_builds.cmake: -------------------------------------------------------------------------------- 1 | # this little snippet makes sure that no absolute paths end up in the binaries built by CMake 2 | # it will replace such paths with relative ones 3 | # see https://reproducible-builds.org/docs/build-path/ for more information 4 | 5 | cmake_minimum_required(VERSION 3.4) 6 | 7 | include(CheckCCompilerFlag) 8 | 9 | if(CMAKE_BUILD_TYPE STREQUAL Release) 10 | message(STATUS "Release build detected, enabling reproducible builds mode") 11 | get_filename_component(abs_source_path ${PROJECT_SOURCE_DIR} ABSOLUTE) 12 | file(RELATIVE_PATH rel_source_path ${PROJECT_BINARY_DIR} ${PROJECT_SOURCE_DIR}) 13 | 14 | set(map_fix ${abs_source_path}=${rel_source_path}) 15 | 16 | # can only add flags when the compiler supports them 17 | # known working compilers: GCC >= 8 18 | foreach(type debug macro) 19 | set(flag_name -f${type}-prefix-map) 20 | set(flags ${flag_name}=${map_fix}) 21 | 22 | check_c_compiler_flag(${flags} ${type}_prefix_map_flag_available) 23 | 24 | if(${type}_prefix_map_flag_available) 25 | set(extra_flags "${extra_flags} ${flags}") 26 | else() 27 | message(WARNING "${flag_name} not available, cannot enable full reproducible builds mode") 28 | endif() 29 | endforeach() 30 | 31 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${extra_flags}") 32 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${extra_flags}") 33 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${extra_flags}") 34 | endif() 35 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-cosmic-i386-cross: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM ubuntu:cosmic 5 | 6 | RUN dpkg --add-architecture i386 && \ 7 | apt-get update && \ 8 | apt-get -y --no-install-recommends install \ 9 | libfuse2:i386 \ 10 | qtbase5-dev:i386 \ 11 | qt5-qmake:i386 \ 12 | qtbase5-dev-tools:i386 \ 13 | libqt5core5a:i386 \ 14 | libqt5gui5:i386 \ 15 | libcurl4-openssl-dev:i386 \ 16 | libssl-dev:i386 \ 17 | libqt5widgets5:i386 \ 18 | librsvg2-bin:i386 \ 19 | libfuse-dev:i386 \ 20 | libcurl4:i386 \ 21 | libcurl4 \ 22 | libbsd-dev:i386 \ 23 | libglib2.0-dev:i386 \ 24 | liblzma-dev:i386 \ 25 | libgtest-dev \ 26 | libcairo-dev:i386 \ 27 | libgl1-mesa-dev:i386 \ 28 | libglu1-mesa-dev:i386 \ 29 | \ 30 | rpm \ 31 | gcc-multilib \ 32 | g++-multilib \ 33 | cmake \ 34 | make \ 35 | git \ 36 | ca-certificates \ 37 | automake \ 38 | autoconf \ 39 | libtool \ 40 | patch \ 41 | wget \ 42 | vim-common \ 43 | desktop-file-utils \ 44 | pkg-config \ 45 | qttools5-dev-tools:i386 \ 46 | qt5-qmake-bin:i386 \ 47 | libarchive-dev:i386 \ 48 | libboost-filesystem-dev:i386 \ 49 | zlib1g:i386 50 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-bionic-i386-cross: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM i386/ubuntu:bionic 5 | 6 | RUN dpkg --add-architecture i386 && \ 7 | apt-get update && \ 8 | apt-get -y --no-install-recommends install \ 9 | libfuse2:i386 \ 10 | qtbase5-dev:i386 \ 11 | qt5-qmake:i386 \ 12 | qtbase5-dev-tools:i386 \ 13 | libqt5core5a:i386 \ 14 | libqt5gui5:i386 \ 15 | libcurl4-openssl-dev:i386 \ 16 | libssl-dev:i386 \ 17 | libqt5widgets5:i386 \ 18 | librsvg2-bin:i386 \ 19 | libfuse-dev:i386 \ 20 | libcurl4:i386 \ 21 | libcurl4 \ 22 | libbsd-dev:i386 \ 23 | libglib2.0-dev:i386 \ 24 | liblzma-dev:i386 \ 25 | libgtest-dev \ 26 | libcairo-dev:i386 \ 27 | libgl1-mesa-dev:i386 \ 28 | libglu1-mesa-dev:i386 \ 29 | \ 30 | rpm \ 31 | gcc-multilib \ 32 | g++-multilib \ 33 | cmake \ 34 | make \ 35 | git \ 36 | ca-certificates \ 37 | automake \ 38 | autoconf \ 39 | libtool \ 40 | patch \ 41 | wget \ 42 | vim-common \ 43 | desktop-file-utils \ 44 | pkg-config \ 45 | qttools5-dev-tools:i386 \ 46 | qt5-qmake-bin:i386 \ 47 | libarchive-dev:i386 \ 48 | libboost-filesystem-dev:i386 \ 49 | zlib1g:i386 50 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | 3 | notifications: 4 | email: false 5 | 6 | services: 7 | - docker 8 | 9 | matrix: 10 | include: 11 | - env: DIST=bionic ARCH=x86_64 12 | name: Native packages for Ubuntu bionic, Debian testing and newer, x86_64 13 | - env: DIST=bionic ARCH=i386 14 | name: Native packages for Ubuntu bionic, Debian testing and newer, i386 15 | - env: DIST=xenial ARCH=x86_64 16 | name: Native packages for Ubuntu xenial, Debian stable etc., x86_64 17 | - env: DIST=xenial ARCH=i386 18 | name: Native packages for Ubuntu xenial, Debian stable etc., i386 19 | - env: DIST=xenial ARCH=x86_64 BUILD_LITE=1 20 | name: AppImageLauncher Lite AppImage x86_64 21 | - env: DIST=xenial ARCH=i386 BUILD_LITE=1 22 | name: AppImageLauncher Lite AppImage i386 23 | 24 | script: 25 | - bash -ex travis/travis-docker.sh "$DIST" 26 | 27 | after_success: 28 | - wget -c https://github.com/probonopd/uploadtool/raw/master/upload.sh 29 | - if [ "$TRAVIS_BRANCH" != "master" ] && [ "$TRAVIS_TAG" == "" ]; then export TRAVIS_EVENT_TYPE=pull_request; fi 30 | - |2 31 | if [[ "$BUILD_LITE" != "" ]]; then 32 | bash upload.sh appimagelauncher-lite-*.AppImage* 33 | elif [[ "$DIST" == "xenial" ]]; then 34 | bash upload.sh appimagelauncher*.{deb,rpm}* appimagelauncher*.tar* 35 | else 36 | bash upload.sh appimagelauncher*.deb* 37 | fi 38 | 39 | branches: 40 | except: 41 | - # Do not build tags that we create when we upload to GitHub Releases 42 | - /^(?i:continuous)/ 43 | -------------------------------------------------------------------------------- /travis/Dockerfile.build-xenial-i386-cross: -------------------------------------------------------------------------------- 1 | # used to cache installed dependencies for bionic builds 2 | # this speeds up builds during development, as the dependencies are just installed _once_ 3 | 4 | FROM i386/ubuntu:xenial 5 | 6 | RUN dpkg --add-architecture i386 && \ 7 | apt-get update && \ 8 | apt-get -y --no-install-recommends install \ 9 | libfuse2:i386 \ 10 | qtbase5-dev:i386 \ 11 | qt5-qmake:i386 \ 12 | qtbase5-dev-tools \ 13 | libqt5core5a:i386 \ 14 | libqt5gui5:i386 \ 15 | libcurl4-openssl-dev:i386 \ 16 | libssl-dev:i386 \ 17 | libqt5widgets5:i386 \ 18 | librsvg2-bin:i386 \ 19 | libfuse-dev:i386 \ 20 | libcurl3:i386 \ 21 | libcurl3 \ 22 | libbsd-dev:i386 \ 23 | libglib2.0-dev:i386 \ 24 | liblzma-dev:i386 \ 25 | libgtest-dev \ 26 | libcairo-dev:i386 \ 27 | libgl1-mesa-dev:i386 \ 28 | libglu1-mesa-dev:i386 \ 29 | \ 30 | rpm \ 31 | gcc-multilib \ 32 | g++-multilib \ 33 | cmake \ 34 | make \ 35 | git \ 36 | ca-certificates \ 37 | automake \ 38 | autoconf \ 39 | libtool \ 40 | patch \ 41 | wget \ 42 | vim-common \ 43 | desktop-file-utils \ 44 | pkg-config \ 45 | qttools5-dev-tools:i386 \ 46 | libfontconfig1-dev:i386 \ 47 | libfreetype6-dev:i386 \ 48 | libarchive-dev:i386 \ 49 | libboost-filesystem-dev:i386 \ 50 | zlib1g:i386 51 | -------------------------------------------------------------------------------- /resources/AppImageLauncher.1.in: -------------------------------------------------------------------------------- 1 | .\" Manpage for AppImageLauncher. 2 | .\" Contact theassassin@assassinate-you.net to correct errors or typos. 3 | .TH man 1 "28 February 2019" "@APPIMAGELAUNCHER_VERSION@" "AppImageLauncher man page" 4 | .SH NAME 5 | AppImageLauncher \- Desktop integration helper for AppImages, for use by Linux distributions. 6 | .SH SYNOPSIS 7 | AppImageLauncher [PATH] 8 | .SH DESCRIPTION 9 | Helper application for Linux distributions serving as a 10 | kind of "entry point" for running and integrating AppImages. 11 | 12 | AppImage provides a way for upstream developers to provide 13 | “native” binaries for Linux users just the same way they could 14 | do for other operating systems. It allows for packaging applications 15 | for any common Linux based operating system, e.g., Debian. 16 | AppImages come with all dependencies that cannot be assumed 17 | to be part of each target system in a recent enough version and 18 | will run on most Linux distributions without further modifications. 19 | 20 | AppImageLauncher makes your Linux desktop AppImage ready™. 21 | You can integrate AppImages with a single mouse click, and manage 22 | them from your application launcher. Install AppImageLauncher today 23 | for your distribution and enjoy using AppImages as easy as never before! 24 | .SH OPTIONS 25 | .IP --appimagelauncher-help 26 | Display this help and exit 27 | .IP --appimagelauncher-version 28 | Display version and exit 29 | .SH ARGUMENTS 30 | Path to AppImage (mandatory) 31 | .SH BUGS 32 | No known bugs. 33 | .SH AUTHOR 34 | TheAssassin 35 | 36 | -------------------------------------------------------------------------------- /resources/mime/packages/appimage.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <_comment>AppImage application bundle (Type 1) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <_comment>AppImage application bundle (Type 2) 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/fusefs/error.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // system includes 4 | #include 5 | 6 | // base class 7 | class AppImageLauncherFSError : public std::runtime_error { 8 | public: 9 | explicit AppImageLauncherFSError(const std::string& msg = "") : runtime_error(msg) {} 10 | }; 11 | 12 | class AlreadyRunningError : public AppImageLauncherFSError { using AppImageLauncherFSError::AppImageLauncherFSError; }; 13 | class CouldNotOpenFileError : public AppImageLauncherFSError { using AppImageLauncherFSError::AppImageLauncherFSError; }; 14 | class FileNotFoundError : public AppImageLauncherFSError { using AppImageLauncherFSError::AppImageLauncherFSError; }; 15 | class InvalidPathError : public AppImageLauncherFSError { using AppImageLauncherFSError::AppImageLauncherFSError; }; 16 | class CouldNotFindRegisteredAppImageError : public AppImageLauncherFSError { using AppImageLauncherFSError::AppImageLauncherFSError; }; 17 | 18 | class AppImageAlreadyRegisteredError : public AppImageLauncherFSError { 19 | private: 20 | const int _id; 21 | 22 | public: 23 | explicit AppImageAlreadyRegisteredError(int id) : _id(id) {}; 24 | 25 | public: 26 | int id() { 27 | return _id; 28 | } 29 | }; 30 | 31 | class DuplicateRegisteredAppImageError : public AppImageLauncherFSError { 32 | private: 33 | const int _firstId; 34 | const int _secondId; 35 | 36 | public: 37 | DuplicateRegisteredAppImageError(int firstId, int secondId) : _firstId(firstId), _secondId(secondId) {}; 38 | 39 | public: 40 | int firstId() { 41 | return _firstId; 42 | } 43 | 44 | int secondId() { 45 | return _secondId; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # in version 3.6 IMPORTED_TARGET has been added to pkg_*_modules 2 | cmake_minimum_required(VERSION 3.6) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | # Find includes in corresponding build directories 8 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 9 | # Instruct CMake to run moc automatically when needed 10 | set(CMAKE_AUTOMOC ON) 11 | # Create code from a list of Qt designer ui files 12 | set(CMAKE_AUTOUIC ON) 13 | # Compile resource files 14 | set(CMAKE_AUTORCC ON) 15 | 16 | find_package(Qt5 REQUIRED COMPONENTS Core Widgets DBus) 17 | 18 | find_package(PkgConfig REQUIRED) 19 | pkg_check_modules(glib REQUIRED glib-2.0>=2.40 IMPORTED_TARGET) 20 | 21 | find_package(INotify REQUIRED) 22 | 23 | # disable Qt debug messages except for debug builds 24 | if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") 25 | message(STATUS "CMake build type is ${CMAKE_BUILD_TYPE}, hence disabling debug messages in AppImageLauncher") 26 | add_definitions(-DQT_NO_DEBUG_OUTPUT) 27 | add_definitions(-DQT_NO_INFO_OUTPUT) 28 | add_definitions(-DQT_NO_WARNING_OUTPUT) 29 | endif() 30 | 31 | # this shall propagate to all targets built in the subdirs 32 | if(ENABLE_UPDATE_HELPER) 33 | add_definitions(-DENABLE_UPDATE_HELPER) 34 | endif() 35 | 36 | add_compile_options(-Wall -Wpedantic) 37 | 38 | if(BUILD_LITE) 39 | add_definitions(-DBUILD_LITE) 40 | endif() 41 | 42 | # utility libraries 43 | add_subdirectory(fswatcher) 44 | add_subdirectory(i18n) 45 | add_subdirectory(trashbin) 46 | add_subdirectory(shared) 47 | 48 | # appimagelauncherd 49 | add_subdirectory(daemon) 50 | 51 | # main application and helper tools 52 | add_subdirectory(ui) 53 | 54 | # FUSE filesystem AppImageLauncherFS 55 | if(NOT BUILD_LITE) 56 | add_subdirectory(fusefs) 57 | endif() 58 | 59 | # CLI helper allowing other tools to utilize AppImageLauncher's code for e.g., integrating AppImages 60 | add_subdirectory(cli) 61 | -------------------------------------------------------------------------------- /resources/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # install configs for resource files 2 | 3 | install( 4 | DIRECTORY 5 | ${CMAKE_CURRENT_SOURCE_DIR}/icons 6 | ${CMAKE_CURRENT_SOURCE_DIR}/mime 7 | DESTINATION ${CMAKE_INSTALL_DATADIR} COMPONENT APPIMAGELAUNCHER 8 | ) 9 | 10 | install( 11 | FILES 12 | ${CMAKE_CURRENT_SOURCE_DIR}/appimagelauncher.desktop 13 | ${CMAKE_CURRENT_SOURCE_DIR}/appimagelaunchersettings.desktop 14 | DESTINATION ${CMAKE_INSTALL_DATADIR}/applications COMPONENT APPIMAGELAUNCHER 15 | ) 16 | 17 | set(INSTALL_MAINTAINER_SCRIPTS OFF CACHE BOOL "") 18 | 19 | # install maintainer scripts that distro packagers might find useful 20 | if (INSTALL_MAINTAINER_SCRIPTS) 21 | message(STATUS "Installing maintainer scripts") 22 | 23 | set(install_maintainer_scripts_dir ${PROJECT_BINARY_DIR}/cmake/install-maintainer-scripts/) 24 | 25 | configure_file( 26 | ${PROJECT_SOURCE_DIR}/resources/install-scripts/post-install.in 27 | ${install_maintainer_scripts_dir}/post-install 28 | @ONLY 29 | ) 30 | configure_file( 31 | ${PROJECT_SOURCE_DIR}/resources/install-scripts/post-uninstall.in 32 | ${install_maintainer_scripts_dir}/post-uninstall 33 | @ONLY 34 | ) 35 | 36 | install( 37 | FILES ${install_maintainer_scripts_dir}/post-install ${install_maintainer_scripts_dir}/post-uninstall 38 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/appimagelauncher/maintainer-scripts 39 | ) 40 | endif() 41 | 42 | 43 | # install man page 44 | set(INSTALL_MAN_PAGE ON CACHE BOOL "") 45 | 46 | if(INSTALL_MAN_PAGE) 47 | set(configured_man_page_path ${PROJECT_BINARY_DIR}/cmake/man/AppImageLauncher.1) 48 | 49 | configure_file( 50 | ${PROJECT_SOURCE_DIR}/resources/AppImageLauncher.1.in 51 | ${configured_man_page_path} 52 | ) 53 | 54 | install( 55 | FILES ${configured_man_page_path} 56 | DESTINATION share/man/man1 COMPONENT APPIMAGELAUNCHER 57 | ) 58 | endif() 59 | -------------------------------------------------------------------------------- /cmake/toolchains/i386-linux-gnu.cmake: -------------------------------------------------------------------------------- 1 | # this toolchain file works for cross compiling on Ubuntu when the following prerequisites are given: 2 | # - all dependencies that would be needed for a normal build must be installed as i386 versions 3 | # - building XZ/liblzma doesn't work yet, so one has to install liblzma-dev:i386 and set -DUSE_SYSTEM_XZ=ON 4 | # - building GTest doesn't work yet, so one has to install libgtest-dev:i386 and set -DUSE_SYSTEM_GTEST=ON 5 | # - building libarchive doesn't work yet, so one has to install liblzma-dev:i386 and set -DUSE_SYSTEM_LIBARCHIVE=ON (TODO: link system libarchive statically like liblzma) 6 | # some of the packets interfere with their x86_64 version (e.g., libfuse-dev:i386, libglib2-dev:i386), so building on a 7 | # normal system will most likely not work, but on systems like Travis it should work fine 8 | 9 | set(CMAKE_SYSTEM_NAME Linux CACHE STRING "" FORCE) 10 | set(CMAKE_SYSTEM_PROCESSOR i386 CACHE STRING "" FORCE) 11 | 12 | set(CMAKE_C_FLAGS "-m32" CACHE STRING "" FORCE) 13 | set(CMAKE_CXX_FLAGS "-m32" CACHE STRING "" FORCE) 14 | 15 | # CMAKE_SHARED_LINKER_FLAGS, CMAKE_STATIC_LINKER_FLAGS etc. must not be set, but CMAKE_EXE_LINKER_FLAGS is necessary 16 | set(CMAKE_EXE_LINKER_FLAGS "-m32" CACHE STRING "" FORCE) 17 | 18 | set(DEPENDENCIES_CFLAGS "-m32" CACHE STRING "" FORCE) 19 | set(DEPENDENCIES_CPPFLAGS "-m32" CACHE STRING "" FORCE) 20 | set(DEPENDENCIES_LDFLAGS "-m32" CACHE STRING "" FORCE) 21 | 22 | # host = target system 23 | # build = build system 24 | # both must be specified 25 | set(EXTRA_CONFIGURE_FLAGS "--host=i686-pc-linux-gnu" "--build=x86_64-pc-linux-gnu" CACHE STRING "" FORCE) 26 | 27 | # may help with some rare issues 28 | set(CMAKE_PREFIX_PATH /usr/lib/i386-linux-gnu CACHE STRING "" FORCE) 29 | 30 | # makes sure that at least on Ubuntu pkg-config will search for the :i386 packages 31 | set(ENV{PKG_CONFIG_PATH} /usr/lib/i386-linux-gnu/pkgconfig/ CACHE STRING "" FORCE) 32 | 33 | # make qtchooser find qt5 34 | set(ENV{QT_SELECT} qt5 CACHE STRING "" FORCE) 35 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | project(AppImageLauncher) 4 | 5 | # versioning 6 | include(cmake/versioning.cmake) 7 | 8 | set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/cmake/modules) 9 | 10 | include(cmake/scripts.cmake) 11 | 12 | include(cmake/reproducible_builds.cmake) 13 | 14 | # support for ccache 15 | # call CMake with -DUSE_CCACHE=ON to make use of it 16 | set(USE_CCACHE OFF CACHE BOOL "") 17 | if(USE_CCACHE) 18 | find_program(CCACHE ccache) 19 | if(CCACHE) 20 | message(STATUS "Using ccache") 21 | set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ${CCACHE}) 22 | set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ${CCACHE}) 23 | else() 24 | message(WARNING "USE_CCACHE set, but could not find ccache") 25 | endif() 26 | endif() 27 | 28 | # used by Debian packaging infrastructure 29 | include(GNUInstallDirs) 30 | 31 | # if there's a system libappimage package on the system, we can use that directly 32 | set(USE_SYSTEM_LIBAPPIMAGE OFF CACHE BOOL "") 33 | 34 | # if the system version should be used, import globally _before_ including third-party stuff and own executables/libs 35 | if(USE_SYSTEM_LIBAPPIMAGE) 36 | find_package(libappimage REQUIRED) 37 | endif() 38 | 39 | # optional; if AppImageUpdate dependency is not viable, the update helper can be disabled 40 | set(ENABLE_UPDATE_HELPER ON CACHE BOOL "") 41 | 42 | # install resources, bundle libraries privately, etc. 43 | # initializes important installation destination variables, therefore must be included before adding subdirectories 44 | include(cmake/install.cmake) 45 | 46 | add_subdirectory(lib) 47 | add_subdirectory(src) 48 | 49 | # contains install configs for resource files 50 | add_subdirectory(resources) 51 | 52 | # translation management 53 | add_subdirectory(i18n) 54 | 55 | # packaging 56 | include(cmake/cpack_general.cmake) 57 | include(cmake/cpack_source.cmake) 58 | include(cmake/cpack_deb.cmake) 59 | include(cmake/cpack_rpm.cmake) 60 | 61 | # must be the last instruction 62 | include(CPack) 63 | -------------------------------------------------------------------------------- /src/fusefs/main.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // library includes 16 | #include 17 | 18 | // local includes 19 | #include "error.h" 20 | #include "fs.h" 21 | 22 | 23 | int main(int argc, char *argv[]) { 24 | // sanity check 25 | // the FS must be run by regular users only 26 | // running as root is likely to create security holes 27 | if ((getuid() == 0) || (geteuid() == 0)) { 28 | fprintf(stderr, "Must not be run as root\n"); 29 | return 1; 30 | } 31 | 32 | // See which version of fuse we're running 33 | std::cerr << "FUSE version: " << FUSE_MAJOR_VERSION << "." << FUSE_MINOR_VERSION << std::endl; 34 | 35 | // Perform some sanity checking on the command line: make sure 36 | // there are enough arguments, and that neither of the last two 37 | // start with a hyphen (this will break if you actually have a 38 | // rootpoint or mountpoint whose name starts with a hyphen, but so 39 | // will a zillion other programs) 40 | if ((argc != 1)) { 41 | std::cerr << "Usage: " << argv[0] << std::endl; 42 | return 1; 43 | } 44 | 45 | std::shared_ptr fs; 46 | 47 | // create filesystem instance 48 | for (int i = 0; i < 2; i++) { 49 | try { 50 | fs = AppImageLauncherFS::getInstance(); 51 | } catch (const AlreadyRunningError&) { 52 | std::cerr << "Another instance is running already" << std::endl; 53 | return 1; 54 | } 55 | } 56 | 57 | if (fs == nullptr) { 58 | std::cerr << "Failed to create filesystem instance" << std::endl; 59 | return 1; 60 | } 61 | 62 | std::cerr << "mountpoint: " << fs->mountpoint() << std::endl; 63 | 64 | std::cerr << "Starting FUSE filesystem" << std::endl; 65 | int fuse_stat = fs->run(); 66 | std::cerr << "Shutting down FUSE filesystem" << std::endl; 67 | 68 | return fuse_stat; 69 | } 70 | -------------------------------------------------------------------------------- /src/cli/cli_main.cpp: -------------------------------------------------------------------------------- 1 | // system headers 2 | #include 3 | 4 | // library headers 5 | #include 6 | #include 7 | 8 | // local headers 9 | #include "CommandFactory.h" 10 | #include "exceptions.h" 11 | #include "logging.h" 12 | 13 | using namespace appimagelauncher::cli; 14 | using namespace appimagelauncher::cli::commands; 15 | 16 | int main(int argc, char** argv) { 17 | // we don't have support for UI (and don't want that), so let's tell shared to not display dialogs 18 | setenv("_FORCE_HEADLESS", "1", 1); 19 | 20 | QCoreApplication app(argc, argv); 21 | 22 | std::ostringstream version; 23 | version << "version " << APPIMAGELAUNCHER_VERSION << " " 24 | << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on " 25 | << APPIMAGELAUNCHER_BUILD_DATE; 26 | QCoreApplication::setApplicationVersion(QString::fromStdString(version.str())); 27 | 28 | QCommandLineParser parser; 29 | 30 | parser.addPositionalArgument("", "Command to run (see help for more information"); 31 | parser.addPositionalArgument("[...]", "command-specific additional arguments"); 32 | parser.addHelpOption(); 33 | parser.addVersionOption(); 34 | 35 | parser.process(app); 36 | 37 | auto posArgs = parser.positionalArguments(); 38 | 39 | if (posArgs.isEmpty()) { 40 | qerr() << parser.helpText().toStdString().c_str() << endl; 41 | 42 | qerr() << "Available commands:" << endl; 43 | qerr() << " integrate Integrate AppImages passed as commandline arguments" << endl; 44 | 45 | return 2; 46 | } 47 | 48 | auto commandName = posArgs.front(); 49 | posArgs.pop_front(); 50 | 51 | try { 52 | auto command = CommandFactory::getCommandByName(commandName); 53 | command->exec(posArgs); 54 | } catch (const CommandNotFoundError& e) { 55 | qerr() << e.what() << endl; 56 | return 1; 57 | } catch (const InvalidArgumentsError& e) { 58 | qerr() << "Invalid arguments: " << e.what() << endl; 59 | return 3; 60 | } catch (const UsageError& e) { 61 | qerr() << "Usage error: " << e.what() << endl; 62 | return 3; 63 | } 64 | 65 | return 0; 66 | } 67 | -------------------------------------------------------------------------------- /src/i18n/translationmanager.cpp: -------------------------------------------------------------------------------- 1 | // library headers 2 | #include 3 | #include 4 | #include 5 | 6 | // local headers 7 | #include "translationmanager.h" 8 | 9 | TranslationManager::TranslationManager(QCoreApplication& app) : app(app) { 10 | // set up translations 11 | auto qtTranslator = new QTranslator(); 12 | qtTranslator->load("qt_" + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); 13 | app.installTranslator(qtTranslator); 14 | 15 | const auto systemLocale = QLocale::system().name(); 16 | 17 | // we're using primarily short names for translations, so we should load these translations as well 18 | const auto shortSystemLocale = systemLocale.split('_')[0]; 19 | 20 | const auto translationDir = getTranslationDir(); 21 | 22 | auto myappTranslator = new QTranslator(); 23 | myappTranslator->load(translationDir + "/ui." + systemLocale + ".qm"); 24 | myappTranslator->load(translationDir + "/ui." + shortSystemLocale + ".qm"); 25 | app.installTranslator(myappTranslator); 26 | 27 | // store translators in list so they won't be deleted 28 | installedTranslators.push_back(qtTranslator); 29 | installedTranslators.push_back(myappTranslator); 30 | } 31 | 32 | TranslationManager::~TranslationManager() { 33 | for (auto& translator : installedTranslators) { 34 | delete translator; 35 | translator = nullptr; 36 | } 37 | } 38 | 39 | QString TranslationManager::getTranslationDir() { 40 | // first we need to find the translation directory 41 | // if this is run from the build tree, we try a path that can only work within the build directory 42 | // then, we try the expected install location relative to the main binary 43 | const auto binaryDirPath = QApplication::applicationDirPath(); 44 | 45 | // previously the path to the repo root dir was embedded to allow for finding the compiled translations 46 | // this lead to irreproducible builds 47 | // therefore the files are now generated within the build dir, and we guess the path based on the binary location 48 | auto translationDir = binaryDirPath + "/../../i18n/generated/l10n"; 49 | 50 | if (!QDir(translationDir).exists()) { 51 | translationDir = binaryDirPath + "/../share/appimagelauncher/l10n"; 52 | } 53 | 54 | return translationDir; 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(NOT BUILD_LITE) 2 | # main AppImageLauncher application 3 | add_executable(AppImageLauncher main.cpp resources.qrc first-run.cpp first-run.h first-run.ui) 4 | target_link_libraries(AppImageLauncher shared PkgConfig::glib libappimage shared) 5 | 6 | # set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used 7 | # by the installed binaries (be it the .deb, the AppImage, or whatever) 8 | # in order to make the whole install tree relocatable, a relative path is used 9 | set_target_properties(AppImageLauncher PROPERTIES INSTALL_RPATH ${_rpath}) 10 | 11 | install( 12 | TARGETS 13 | AppImageLauncher 14 | RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER 15 | LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER 16 | ) 17 | endif() 18 | 19 | # AppImageLauncherSettings application 20 | add_executable(AppImageLauncherSettings settings_main.cpp settings_dialog.ui settings_dialog.cpp) 21 | target_link_libraries(AppImageLauncherSettings shared) 22 | 23 | # set binary runtime rpath to make sure the libappimage.so built and installed by this project is going to be used 24 | # by the installed binaries (be it the .deb, the AppImage, or whatever) 25 | # in order to make the whole install tree relocatable, a relative path is used 26 | set_target_properties(AppImageLauncherSettings PROPERTIES INSTALL_RPATH ${_rpath}) 27 | 28 | install( 29 | TARGETS 30 | AppImageLauncherSettings 31 | RUNTIME DESTINATION ${_bindir} COMPONENT APPIMAGELAUNCHER 32 | LIBRARY DESTINATION ${_libdir} COMPONENT APPIMAGELAUNCHER 33 | ) 34 | 35 | # AppImage removal helper 36 | add_executable(remove remove_main.cpp remove.ui resources.qrc) 37 | target_link_libraries(remove shared translationmanager libappimage) 38 | # see AppImageLauncher for a description 39 | set_target_properties(remove PROPERTIES INSTALL_RPATH "\$ORIGIN") 40 | 41 | install( 42 | TARGETS remove 43 | RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER 44 | ) 45 | 46 | 47 | # AppImage update helper 48 | if(ENABLE_UPDATE_HELPER) 49 | add_executable(update update_main.cpp resources.qrc) 50 | target_link_libraries(update shared translationmanager libappimage libappimageupdate-qt) 51 | # see AppImageLauncher for a description 52 | set_target_properties(update PROPERTIES INSTALL_RPATH "\$ORIGIN") 53 | 54 | install( 55 | TARGETS update 56 | RUNTIME DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER 57 | ) 58 | endif() 59 | -------------------------------------------------------------------------------- /travis/travis-build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | # use RAM disk if possible 7 | if [ "$CI" == "" ] && [ -d /dev/shm ]; then 8 | TEMP_BASE=/dev/shm 9 | else 10 | TEMP_BASE=/tmp 11 | fi 12 | 13 | BUILD_DIR=$(mktemp -d -p "$TEMP_BASE" AppImageLauncher-build-XXXXXX) 14 | 15 | cleanup () { 16 | if [ -d "$BUILD_DIR" ]; then 17 | rm -rf "$BUILD_DIR" 18 | fi 19 | } 20 | 21 | trap cleanup EXIT 22 | 23 | # store repo root as variable 24 | REPO_ROOT=$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..) 25 | OLD_CWD=$(readlink -f .) 26 | 27 | pushd "$BUILD_DIR" 28 | 29 | # install more recent CMake version which fixes some linking issue in CMake < 3.10 30 | # Fixes https://github.com/TheAssassin/AppImageLauncher/issues/106 31 | # Upstream bug: https://gitlab.kitware.com/cmake/cmake/issues/17389 32 | wget https://cmake.org/files/v3.13/cmake-3.13.2-Linux-x86_64.tar.gz -qO- | tar xz --strip-components=1 33 | export PATH=$(readlink -f bin/):"$PATH" 34 | which cmake 35 | cmake --version 36 | 37 | if [ "$DEBIAN_DIST" != "" ]; then 38 | EXTRA_CMAKE_FLAGS=-DCPACK_DEBIAN_COMPATIBILITY_LEVEL="$DEBIAN_DIST" 39 | else 40 | echo "--- WARNING --- DEBIAN_DIST has not been set" 41 | fi 42 | 43 | if [ "$ARCH" == "i386" ]; then 44 | EXTRA_CMAKE_FLAGS="$EXTRA_CMAKE_FLAGS -DCMAKE_TOOLCHAIN_FILE=$REPO_ROOT/cmake/toolchains/i386-linux-gnu.cmake -DUSE_SYSTEM_XZ=ON -DUSE_SYSTEM_LIBARCHIVE=ON" 45 | if [ "$DEBIAN_DIST" != "bionic" ] && [ "$DEBIAN_DIST" != "cosmic" ]; then 46 | export QT_SELECT=qt5-i386-linux-gnu 47 | else 48 | export QT_SELECT=qt5 49 | fi 50 | fi 51 | 52 | cmake "$REPO_ROOT" -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo $EXTRA_CMAKE_FLAGS -DTRAVIS_BUILD=ON -DBUILD_TESTING=OFF 53 | 54 | # now, compile 55 | make -j $(nproc) 56 | 57 | # re-run cmake to find built shared objects with the globs, and update the CPack files 58 | cmake . 59 | 60 | # build Debian package 61 | cpack -V -G DEB 62 | 63 | # skip RPM and source tarball build on bionic 64 | if [ "$DEBIAN_DIST" == "xenial" ]; then 65 | # build RPM package 66 | cpack -V -G RPM 67 | 68 | # build source tarball 69 | # generates a lot of output, therefore not run in verbose mode 70 | cpack --config CPackSourceConfig.cmake 71 | 72 | # generate log for debugging 73 | # CPack is very verbose, therefore we generate a file and upload it 74 | cpack --config CPackSourceConfig.cmake -V 75 | fi 76 | 77 | # move AppImages to old cwd 78 | if [ "$DEBIAN_DIST" == "xenial" ]; then 79 | mv appimagelauncher*.{deb,rpm}* appimagelauncher*.tar* "$OLD_CWD"/ 80 | else 81 | mv appimagelauncher*.deb* "$OLD_CWD"/ 82 | fi 83 | 84 | popd 85 | -------------------------------------------------------------------------------- /i18n/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # lupdate must be installed and available 2 | check_program(NAME lupdate) 3 | check_program(NAME lrelease) 4 | 5 | # create target calling custom commands 6 | add_custom_target(i18n) 7 | 8 | # create/update AppImageLauncher strings 9 | function(ail_generate_translations) 10 | if(ARGC LESS 2) 11 | message(ERROR "ail_generate_translations called with wrong amount of arguments") 12 | endif() 13 | 14 | add_custom_command( 15 | TARGET i18n POST_BUILD 16 | COMMAND ${LUPDATE} "${ARGV0}" -ts "${ARGV1}" -locations relative 17 | MAIN_DEPENDENCY "${ARGV0}" 18 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 19 | 20 | ) 21 | endfunction() 22 | 23 | ail_generate_translations(../src/ ui.en.ts) 24 | 25 | file(GLOB TS_FILES *.ts) 26 | 27 | foreach(TS_FILE IN LISTS TS_FILES) 28 | get_filename_component(TS_FILENAME ${TS_FILE} NAME) 29 | ail_generate_translations(../src/ ${TS_FILENAME}) 30 | endforeach() 31 | 32 | add_custom_command( 33 | TARGET i18n POST_BUILD 34 | COMMAND ./auto-translate.py ui.en.ts 35 | MAIN_DEPENDENCY "${ARGV0}" 36 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 37 | ) 38 | 39 | 40 | add_custom_target(l10n ALL) 41 | 42 | # generate Qt qm files from translation source files 43 | function(ail_generate_qm) 44 | if(ARGC LESS 2) 45 | message(ERROR "ail_generate_translations called with wrong amount of arguments") 46 | endif() 47 | 48 | add_custom_command( 49 | TARGET l10n POST_BUILD 50 | COMMAND ${LRELEASE} "${ARGV0}" -qm "${ARGV1}" 51 | MAIN_DEPENDENCY "${ARGV0}" 52 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 53 | 54 | ) 55 | endfunction() 56 | 57 | file(GLOB TS_FILES *.ts) 58 | 59 | set(target_dir ${CMAKE_CURRENT_BINARY_DIR}/generated/l10n) 60 | 61 | foreach(TS_FILE IN LISTS TS_FILES) 62 | get_filename_component(TS_FILENAME ${TS_FILE} NAME) 63 | string(REPLACE .ts .qm QM_FILENAME ${TS_FILENAME}) 64 | ail_generate_qm(${TS_FILENAME} ${target_dir}/${QM_FILENAME}) 65 | endforeach() 66 | 67 | # deploy JSON files 68 | file(GLOB JSON_FILES *.json) 69 | 70 | foreach(JSON_FILE IN LISTS JSON_FILES) 71 | get_filename_component(JSON_FILENAME ${JSON_FILE} NAME) 72 | add_custom_command( 73 | TARGET l10n POST_BUILD 74 | COMMAND ${CMAKE_COMMAND} -E copy ${JSON_FILENAME} ${target_dir}/${JSON_FILENAME} 75 | WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} 76 | MAIN_DEPENDENCY ${JSON_FILE} 77 | ) 78 | endforeach() 79 | 80 | 81 | # empty directories aren't tracked by Git 82 | # therefore the directory needs to be created by CMake 83 | file(MAKE_DIRECTORY ${target_dir}) 84 | 85 | install( 86 | DIRECTORY ${target_dir} 87 | DESTINATION ${CMAKE_INSTALL_DATADIR}/appimagelauncher COMPONENT APPIMAGELAUNCHER 88 | ) 89 | -------------------------------------------------------------------------------- /cmake/cpack_rpm.cmake: -------------------------------------------------------------------------------- 1 | # required for RPM-DEFAULT to work as intended 2 | cmake_minimum_required(VERSION 3.6) 3 | 4 | # allow building RPM packages on non-RPM systems 5 | if(DEFINED ENV{ARCH}) 6 | set(CPACK_RPM_PACKAGE_ARCHITECTURE $ENV{ARCH}) 7 | if(CPACK_RPM_PACKAGE_ARCHITECTURE MATCHES "i686") 8 | set(CPACK_RPM_PACKAGE_ARCHITECTURE "i386") 9 | elseif(CPACK_RPM_PACKAGE_ARCHITECTURE MATCHES "amd64") 10 | set(CPACK_RPM_PACKAGE_ARCHITECTURE "x86_64") 11 | endif() 12 | endif() 13 | 14 | # override default package naming 15 | set(CPACK_RPM_FILE_NAME RPM-DEFAULT) 16 | 17 | # make sure to package components separately 18 | #set(CPACK_DEB_PACKAGE_COMPONENT ON) 19 | set(CPACK_RPM_COMPONENT_INSTALL ON) 20 | 21 | # RPM packaging global options 22 | set(CPACK_RPM_COMPRESSION_TYPE xz) 23 | 24 | # use git hash as package release 25 | set(CPACK_RPM_PACKAGE_RELEASE "git${APPIMAGELAUNCHER_GIT_COMMIT_DATE_SHORT}.${APPIMAGELAUNCHER_GIT_COMMIT}") 26 | 27 | # append build ID, similar to AppImage naming 28 | if(DEFINED ENV{TRAVIS_BUILD_NUMBER}) 29 | set(CPACK_RPM_PACKAGE_RELEASE "travis$ENV{TRAVIS_BUILD_NUMBER}~${CPACK_RPM_PACKAGE_RELEASE}") 30 | else() 31 | set(CPACK_RPM_PACKAGE_RELEASE "local~${CPACK_RPM_PACKAGE_RELEASE}") 32 | endif() 33 | 34 | # bash is required to run install hooks 35 | set(CPACK_RPM_APPIMAGELAUNCHER_REQUIRES_PRE "bash") 36 | 37 | # package name 38 | set(CPACK_RPM_APPIMAGELAUNCHER_PACKAGE_NAME "appimagelauncher") 39 | 40 | # TODO: packagers watch out: you should set this to depend on a libappimage package, and avoid installing the library 41 | # to a custom location in install.cmake 42 | set(CPACK_RPM_APPIMAGELAUNCHER_PACKAGE_DREQUIRES "pkgconfig(Qt5Widgets) (>= 5.2.1), pkgconfig(Qt5Gui) (>= 5.2.1), pkgconfig(Qt5Core) (>= 5.2.1), libcurl4") 43 | # deactivate automatic generation of dependency list 44 | # it wants to add libappimage.so to the list of dependencies, which is actually shipped by the package 45 | set(CPACK_RPM_PACKAGE_AUTOREQ OFF) 46 | 47 | # avoid conflicts with the filesystem package 48 | set(CPACK_RPM_EXCLUDE_FROM_AUTO_FILELIST_ADDITION 49 | /usr/share/applications 50 | /usr/lib/binfmt.d 51 | /usr/lib/systemd 52 | /usr/lib/systemd/user 53 | /usr/share/man 54 | /usr/share/man/man1 55 | ) 56 | 57 | # add postinst and prerm hooks to RPM package 58 | configure_file( 59 | ${PROJECT_SOURCE_DIR}/resources/install-scripts/post-install.in 60 | ${PROJECT_BINARY_DIR}/cmake/rpm/post-install 61 | @ONLY 62 | ) 63 | configure_file( 64 | ${PROJECT_SOURCE_DIR}/resources/install-scripts/post-uninstall.in 65 | ${PROJECT_BINARY_DIR}/cmake/rpm/post-uninstall 66 | @ONLY 67 | ) 68 | 69 | set(CPACK_RPM_POST_INSTALL_SCRIPT_FILE "${PROJECT_BINARY_DIR}/cmake/rpm/post-install") 70 | set(CPACK_RPM_PRE_UNINSTALL_SCRIPT_FILE "${PROJECT_BINARY_DIR}/cmake/rpm/post-uninstall") 71 | -------------------------------------------------------------------------------- /travis/build-lite.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if [ "$ARCH" == "" ]; then 4 | echo "Error: you must set \$ARCH" 5 | exit 2 6 | fi 7 | 8 | set -x 9 | set -e 10 | 11 | # use RAM disk if possible 12 | if [ "$CI" == "" ] && [ -d /dev/shm ]; then 13 | TEMP_BASE=/dev/shm 14 | else 15 | TEMP_BASE=/tmp 16 | fi 17 | 18 | BUILD_DIR=$(mktemp -d -p "$TEMP_BASE" AppImageLauncher-build-XXXXXX) 19 | 20 | cleanup () { 21 | if [ -d "$BUILD_DIR" ]; then 22 | rm -rf "$BUILD_DIR" 23 | fi 24 | } 25 | 26 | trap cleanup EXIT 27 | 28 | # store repo root as variable 29 | REPO_ROOT=$(readlink -f $(dirname "${BASH_SOURCE[0]}")/..) 30 | OLD_CWD=$(readlink -f .) 31 | 32 | pushd "$BUILD_DIR" 33 | 34 | # install more recent CMake version which fixes some linking issue in CMake < 3.10 35 | # Fixes https://github.com/TheAssassin/AppImageLauncher/issues/106 36 | # Upstream bug: https://gitlab.kitware.com/cmake/cmake/issues/17389 37 | wget https://cmake.org/files/v3.13/cmake-3.13.2-Linux-x86_64.tar.gz -qO- | tar xz --strip-components=1 38 | export PATH=$(readlink -f bin/):"$PATH" 39 | which cmake 40 | cmake --version 41 | 42 | EXTRA_CMAKE_FLAGS= 43 | 44 | if [ "$ARCH" == "i386" ]; then 45 | EXTRA_CMAKE_FLAGS="$EXTRA_CMAKE_FLAGS -DCMAKE_TOOLCHAIN_FILE=$REPO_ROOT/cmake/toolchains/i386-linux-gnu.cmake -DUSE_SYSTEM_XZ=ON -DUSE_SYSTEM_LIBARCHIVE=ON" 46 | # TODO check if this can be removed 47 | if [ "$DEBIAN_DIST" != "bionic" ] && [ "$DEBIAN_DIST" != "cosmic" ]; then 48 | export QT_SELECT=qt5-i386-linux-gnu 49 | else 50 | export QT_SELECT=qt5 51 | fi 52 | fi 53 | 54 | cmake "$REPO_ROOT" -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo $EXTRA_CMAKE_FLAGS -DTRAVIS_BUILD=ON -DBUILD_TESTING=OFF -DBUILD_LITE=ON 55 | 56 | # compile dependencies 57 | make -j $(nproc) libappimage libappimageupdate libappimageupdate-qt 58 | 59 | # re-run cmake to update paths to dependencies 60 | cmake . 61 | 62 | # build rest 63 | make -j $(nproc) 64 | 65 | # prepare AppDir 66 | make install DESTDIR=AppDir 67 | 68 | # build AppImage 69 | wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-"$ARCH".AppImage 70 | wget https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-"$ARCH".AppImage 71 | wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-"$ARCH".AppImage 72 | chmod -v +x linuxdeploy*-"$ARCH".AppImage 73 | 74 | export VERSION=$(src/cli/ail-cli --version | awk '{print $3}') 75 | export OUTPUT=appimagelauncher-lite-"$VERSION"-"$ARCH".AppImage 76 | export APPIMAGE_EXTRACT_AND_RUN=1 77 | 78 | ./linuxdeploy-"$ARCH".AppImage --plugin qt --appdir $(readlink -f AppDir) --custom-apprun "$REPO_ROOT"/resources/appimagelauncher-lite-AppRun.sh --output appimage \ 79 | -d "$REPO_ROOT"/resources/appimagelauncher-lite.desktop \ 80 | -e $(find AppDir/usr/lib/{,*/}appimagelauncher/remove | head -n1) \ 81 | -e $(find AppDir/usr/lib/{,*/}appimagelauncher/update | head -n1) 82 | 83 | mv "$OUTPUT" "$OLD_CWD" 84 | -------------------------------------------------------------------------------- /src/trashbin/trashbin.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | 5 | // library includes 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // local includes 12 | #include "trashbin.h" 13 | #include "shared.h" 14 | 15 | class TrashBin::PrivateData { 16 | public: 17 | const QDir dir; 18 | 19 | public: 20 | PrivateData() : dir(integratedAppImagesDestination().path() + "/.trash") { 21 | // make sure trash directory exists 22 | QDir(integratedAppImagesDestination().path()).mkdir(".trash"); 23 | } 24 | 25 | bool canBeCleanedUp(const QString& path) { 26 | return true; 27 | } 28 | }; 29 | 30 | TrashBin::TrashBin() { 31 | d = new PrivateData(); 32 | } 33 | 34 | QString TrashBin::path() { 35 | return d->dir.path(); 36 | } 37 | 38 | bool TrashBin::disposeAppImage(const QString& pathToAppImage) { 39 | if (!QFile(pathToAppImage).exists()) { 40 | std::cerr << "No such file or directory: " << pathToAppImage.toStdString() << std::endl; 41 | return false; 42 | } 43 | 44 | // moving AppImages into the trash bin might fail if there's a file with the same filename 45 | // removing multiple files with the same filenames is a valid use case, though 46 | // therefore, a timestamp shall be prepended to the filename 47 | // it is very unlike that some user will remove more than a AppImage per second, but if that should be the case, 48 | // we could eventually increase the precision of the timestamp 49 | // for now, this is not necessary 50 | auto timestamp = QDateTime::currentDateTime().toString(Qt::ISODate); 51 | auto newPath = d->dir.path() + QString("/") + timestamp + "_" + QFileInfo(pathToAppImage).fileName(); 52 | 53 | if (!QFile(pathToAppImage).rename(newPath)) 54 | return false; 55 | 56 | if (!makeNonExecutable(newPath)) 57 | return false; 58 | 59 | return true; 60 | } 61 | 62 | bool TrashBin::cleanUp() { 63 | for (QDirIterator iterator(d->dir, QDirIterator::FollowSymlinks); iterator.hasNext();) { 64 | auto currentPath = iterator.next(); 65 | 66 | if (!QFileInfo(currentPath).isFile()) 67 | continue; 68 | 69 | if (appimage_get_type(currentPath.toStdString().c_str(), false) <= 0) 70 | continue; 71 | 72 | if (!d->canBeCleanedUp(currentPath)) { 73 | std::cerr << "Cannot clean up AppImage yet: " << currentPath.toStdString() << std::endl; 74 | continue; 75 | } 76 | 77 | std::cerr << "Removing AppImage: " << currentPath.toStdString() << std::endl; 78 | 79 | // silently ignore if files can not be removed 80 | // they shall be removed on subsequent runs 81 | // if this won't happen and the trash directory will only get bigger at some point, we might need to 82 | // reconsider this decision 83 | if (!QFile(currentPath).remove()) 84 | continue; 85 | } 86 | 87 | return true; 88 | } 89 | -------------------------------------------------------------------------------- /cmake/versioning.cmake: -------------------------------------------------------------------------------- 1 | set(V_MAJOR 1) 2 | set(V_MINOR 3) 3 | set(V_PATCH 1) 4 | set(V_SUFFIX "") 5 | 6 | set(APPIMAGELAUNCHER_VERSION ${V_MAJOR}.${V_MINOR}.${V_PATCH}${V_SUFFIX}) 7 | 8 | # check whether git is available 9 | find_program(GIT git) 10 | set(GIT_COMMIT_CACHE_FILE "${PROJECT_SOURCE_DIR}/cmake/GIT_COMMIT") 11 | 12 | if(NOT GIT STREQUAL "GIT-NOTFOUND") 13 | # read Git revision ID 14 | # WARNING: this value will be stored in the CMake cache 15 | # to update it, you will have to reset the CMake cache 16 | # (doesn't matter for CI builds like Travis for instance, where there's no permanent CMake cache) 17 | execute_process( 18 | COMMAND git rev-parse --short HEAD 19 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 20 | OUTPUT_VARIABLE APPIMAGELAUNCHER_GIT_COMMIT 21 | OUTPUT_STRIP_TRAILING_WHITESPACE 22 | ERROR_QUIET 23 | RESULT_VARIABLE GIT_RESULT 24 | ) 25 | 26 | if(GIT_RESULT EQUAL 0) 27 | message(STATUS "Storing git commit ID in cache file") 28 | file(WRITE "${GIT_COMMIT_CACHE_FILE}" "${APPIMAGELAUNCHER_GIT_COMMIT}") 29 | endif() 30 | endif() 31 | 32 | if(NOT GIT_RESULT EQUAL 0) 33 | # git call failed or git hasn't been found, might happen when calling CMake in an extracted source tarball 34 | # therefore we try to find the git commit cache file 35 | # if it doesn't exist, refuse to configure 36 | 37 | message(WARNING "Failed to gather commit ID via git command, trying to read cache file") 38 | if(EXISTS "${GIT_COMMIT_CACHE_FILE}") 39 | file(READ "${GIT_COMMIT_CACHE_FILE}" APPIMAGELAUNCHER_CACHED_GIT_COMMIT) 40 | mark_as_advanced(FORCE APPIMAGELAUNCHER_CACHED_GIT_COMMIT) 41 | string(REPLACE "\n" "" APPIMAGELAUNCHER_GIT_COMMIT "${APPIMAGELAUNCHER_CACHED_GIT_COMMIT}") 42 | else() 43 | message(FATAL_ERROR "Could not find git commit cache file, git commit ID not available for versioning") 44 | endif() 45 | endif() 46 | 47 | if("${APPIMAGELAUNCHER_GIT_COMMIT}" STREQUAL "") 48 | message(FATAL_ERROR "Invalid git commit ID: ${APPIMAGELAUNCHER_GIT_COMMIT}") 49 | endif() 50 | 51 | message(STATUS "Git commit: ${APPIMAGELAUNCHER_GIT_COMMIT}") 52 | mark_as_advanced(FORCE APPIMAGELAUNCHER_GIT_COMMIT) 53 | 54 | # add build number based on Travis build number if possible 55 | if("$ENV{TRAVIS_BUILD_NUMBER}" STREQUAL "") 56 | set(APPIMAGELAUNCHER_BUILD_NUMBER "") 57 | else() 58 | set(APPIMAGELAUNCHER_BUILD_NUMBER "$ENV{TRAVIS_BUILD_NUMBER}") 59 | endif() 60 | 61 | # get current date 62 | execute_process( 63 | COMMAND env LC_ALL=C date -u "+%Y-%m-%d %H:%M:%S %Z" 64 | OUTPUT_VARIABLE APPIMAGELAUNCHER_BUILD_DATE 65 | OUTPUT_STRIP_TRAILING_WHITESPACE 66 | ) 67 | 68 | # get current date, short form 69 | execute_process( 70 | COMMAND env LC_ALL=C git show -s --format=%ci ${APPIMAGELAUNCHER_GIT_COMMIT} 71 | OUTPUT_VARIABLE APPIMAGELAUNCHER_GIT_COMMIT_DATE 72 | OUTPUT_STRIP_TRAILING_WHITESPACE 73 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 74 | ) 75 | execute_process( 76 | COMMAND env LC_ALL=C date -u "+%Y%m%d" -d ${APPIMAGELAUNCHER_GIT_COMMIT_DATE} 77 | OUTPUT_VARIABLE APPIMAGELAUNCHER_GIT_COMMIT_DATE_SHORT 78 | OUTPUT_STRIP_TRAILING_WHITESPACE 79 | WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} 80 | ) 81 | 82 | add_definitions(-DAPPIMAGELAUNCHER_VERSION="${APPIMAGELAUNCHER_VERSION}") 83 | add_definitions(-DAPPIMAGELAUNCHER_GIT_COMMIT="${APPIMAGELAUNCHER_GIT_COMMIT}") 84 | add_definitions(-DAPPIMAGELAUNCHER_BUILD_NUMBER="${APPIMAGELAUNCHER_BUILD_NUMBER}") 85 | add_definitions(-DAPPIMAGELAUNCHER_BUILD_DATE="${APPIMAGELAUNCHER_BUILD_DATE}") 86 | -------------------------------------------------------------------------------- /src/ui/settings_dialog.cpp: -------------------------------------------------------------------------------- 1 | // libraries 2 | #include 3 | 4 | // local 5 | #include "settings_dialog.h" 6 | #include "ui_settings_dialog.h" 7 | #include "shared.h" 8 | 9 | SettingsDialog::SettingsDialog(QWidget* parent) : 10 | QDialog(parent), 11 | ui(new Ui::SettingsDialog) { 12 | ui->setupUi(this); 13 | 14 | loadSettings(); 15 | 16 | // cosmetic changes in lite mode 17 | #ifndef BUILD_LITE 18 | ui->checkBoxEnableDaemon->setChecked(true); 19 | ui->checkBoxEnableDaemon->setEnabled(false); 20 | 21 | ui->checkBoxAskMove->setChecked(false); 22 | ui->checkBoxAskMove->setEnabled(false); 23 | #endif 24 | 25 | connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::onDialogAccepted); 26 | connect(ui->toolButtonChooseAppsDir, &QToolButton::released, this, &SettingsDialog::onChooseAppsDirClicked); 27 | 28 | QStringList availableFeatures; 29 | 30 | #ifdef ENABLE_UPDATE_HELPER 31 | availableFeatures << " " + tr("updater available for AppImages supporting AppImageUpdate"); 32 | #else 33 | availableFeatures << "🞬 " + tr("updater unavailable"); 34 | #endif 35 | 36 | #ifndef BUILD_LITE 37 | availableFeatures << "

" 38 | << tr("Note: this is an AppImageLauncher Lite build, only supports a limited set of features
" 39 | "Please install the full version via the provided native packages to enjoy the full AppImageLauncher experience"); 40 | #endif 41 | 42 | ui->featuresLabel->setText(availableFeatures.join('\n')); 43 | } 44 | 45 | SettingsDialog::~SettingsDialog() { 46 | delete ui; 47 | } 48 | 49 | void SettingsDialog::loadSettings() { 50 | settingsFile = getConfig(); 51 | 52 | if (settingsFile) { 53 | ui->checkBoxEnableDaemon->setChecked(settingsFile->value("AppImageLauncher/enable_daemon", false).toBool()); 54 | ui->checkBoxAskMove->setChecked(settingsFile->value("AppImageLauncher/ask_to_move", false).toBool()); 55 | ui->lineEditApplicationsDir->setText(settingsFile->value("AppImageLauncher/destination").toString()); 56 | } 57 | } 58 | 59 | void SettingsDialog::onDialogAccepted() { 60 | saveSettings(); 61 | toggleDaemon(); 62 | } 63 | 64 | void SettingsDialog::saveSettings() { 65 | createConfigFile(ui->checkBoxAskMove->isChecked(), 66 | ui->lineEditApplicationsDir->text(), 67 | ui->checkBoxEnableDaemon->isChecked()); 68 | 69 | settingsFile = getConfig(); 70 | } 71 | 72 | void SettingsDialog::toggleDaemon() { 73 | // assumes defaults if config doesn't exist or lacks the related key(s) 74 | if (settingsFile) { 75 | if (settingsFile->value("AppImageLauncher/enable_daemon", false).toBool()) { 76 | system("systemctl --user enable appimagelauncherd.service"); 77 | system("systemctl --user start appimagelauncherd.service"); 78 | } else { 79 | system("systemctl --user disable appimagelauncherd.service"); 80 | system("systemctl --user stop appimagelauncherd.service"); 81 | } 82 | } 83 | } 84 | 85 | void SettingsDialog::onChooseAppsDirClicked() { 86 | QFileDialog fileDialog(this); 87 | fileDialog.setFileMode(QFileDialog::DirectoryOnly); 88 | fileDialog.setWindowTitle(tr("Select Applications directory")); 89 | fileDialog.setDirectory(QDir::home()); 90 | if (fileDialog.exec()) { 91 | QString dirPath = fileDialog.selectedFiles().first(); 92 | ui->lineEditApplicationsDir->setText(dirPath); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/ui/remove.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | RemoveDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 431 10 | 151 11 | 12 | 13 | 14 | Please confirm 15 | 16 | 17 | 18 | 19 | 20 20 | 20 21 | 391 22 | 111 23 | 24 | 25 | 26 | 27 | QLayout::SetMinimumSize 28 | 29 | 30 | 31 | 32 | <html><head/><body><p>Are you sure you want to remove this AppImage?</body></html> 33 | 34 | 35 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 0 44 | 0 45 | 46 | 47 | 48 | 49 | 389 50 | 0 51 | 52 | 53 | 54 | %1 55 | 56 | 57 | 58 | 59 | 60 | 61 | <html><head/><body><p>Uncheck to only remove the desktop integration, but leave the file on the system.</p></body></html> 62 | 63 | 64 | Remove AppImage file from system 65 | 66 | 67 | true 68 | 69 | 70 | 71 | 72 | 73 | 74 | Qt::Horizontal 75 | 76 | 77 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | buttonBox 88 | accepted() 89 | RemoveDialog 90 | accept() 91 | 92 | 93 | 248 94 | 254 95 | 96 | 97 | 157 98 | 274 99 | 100 | 101 | 102 | 103 | buttonBox 104 | rejected() 105 | RemoveDialog 106 | reject() 107 | 108 | 109 | 316 110 | 260 111 | 112 | 113 | 286 114 | 274 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /cmake/cpack_deb.cmake: -------------------------------------------------------------------------------- 1 | # required for DEB-DEFAULT to work as intended 2 | cmake_minimum_required(VERSION 3.6) 3 | 4 | # Fix for https://github.com/TheAssassin/AppImageLauncher/issues/28 5 | execute_process( 6 | COMMAND lsb_release -c 7 | OUTPUT_VARIABLE _lsb_release_output 8 | ) 9 | 10 | if(_lsb_release_output MATCHES bionic) 11 | message(STATUS "platform is bionic, enabling compatibility mode for CPack Debian packaging") 12 | set(_compatibility_level bionic) 13 | elseif(_lsb_release_output MATCHES cosmic) 14 | message(STATUS "platform is cosmic, enabling compatibility mode for CPack Debian packaging") 15 | set(_compatibility_level cosmic) 16 | else() 17 | set(_compatibility_level "") 18 | endif() 19 | 20 | set(CPACK_DEBIAN_COMPATIBILITY_LEVEL ${_compatibility_level} CACHE STRING "Available values: bionic (Ubuntu 18.04 LTS), cosmic (Ubuntu 18.10)") 21 | 22 | unset(_lsb_release_output) 23 | unset(_compatibility_level) 24 | 25 | # allow building Debian packages on non-Debian systems 26 | if(DEFINED ENV{ARCH}) 27 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE $ENV{ARCH}) 28 | if(CPACK_DEBIAN_PACKAGE_ARCHITECTURE MATCHES "i686") 29 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "i386") 30 | elseif(CPACK_DEBIAN_PACKAGE_ARCHITECTURE MATCHES "x86_64") 31 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "amd64") 32 | endif() 33 | endif() 34 | 35 | # make sure to package components separately 36 | #set(CPACK_DEB_PACKAGE_COMPONENT ON) 37 | set(CPACK_DEB_COMPONENT_INSTALL ON) 38 | 39 | # override default package naming 40 | set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) 41 | 42 | # Debian packaging global options 43 | set(CPACK_DEBIAN_COMPRESSION_TYPE xz) 44 | 45 | # use git hash as package release 46 | set(CPACK_DEBIAN_PACKAGE_RELEASE "git${APPIMAGELAUNCHER_GIT_COMMIT_DATE_SHORT}.${APPIMAGELAUNCHER_GIT_COMMIT}") 47 | 48 | # append build ID, similar to AppImage naming 49 | if(DEFINED ENV{TRAVIS_BUILD_NUMBER}) 50 | set(CPACK_DEBIAN_PACKAGE_RELEASE "travis$ENV{TRAVIS_BUILD_NUMBER}~${CPACK_DEBIAN_PACKAGE_RELEASE}") 51 | else() 52 | set(CPACK_DEBIAN_PACKAGE_RELEASE "local~${CPACK_DEBIAN_PACKAGE_RELEASE}") 53 | endif() 54 | 55 | if(CPACK_DEBIAN_COMPATIBILITY_LEVEL) 56 | set(CPACK_DEBIAN_PACKAGE_RELEASE "${CPACK_DEBIAN_PACKAGE_RELEASE}+${CPACK_DEBIAN_COMPATIBILITY_LEVEL}") 57 | endif() 58 | 59 | # bash is required to run install hooks 60 | set(CPACK_DEBIAN_APPIMAGELAUNCHER_PACKAGE_PREDEPENDS bash) 61 | 62 | # package name 63 | set(CPACK_DEBIAN_APPIMAGELAUNCHER_PACKAGE_NAME "appimagelauncher") 64 | 65 | # TODO: packagers watch out: you should set this to depend on a libappimage package, and avoid installing the library 66 | # to a custom location in install.cmake 67 | 68 | if(CPACK_DEBIAN_COMPATIBILITY_LEVEL STREQUAL "bionic" OR CPACK_DEBIAN_COMPATIBILITY_LEVEL STREQUAL "cosmic") 69 | set(CPACK_DEBIAN_APPIMAGELAUNCHER_PACKAGE_DEPENDS "libqt5widgets5 (>= 5.2.1), libqt5gui5 (>= 5.2.1), libqt5core5a (>= 5.2.1), binfmt-support (>= 2.0), libcurl4") 70 | else() 71 | set(CPACK_DEBIAN_APPIMAGELAUNCHER_PACKAGE_DEPENDS "libqt5widgets5 (>= 5.2.1), libqt5gui5 (>= 5.2.1), libqt5core5a (>= 5.2.1), binfmt-support (>= 2.0), libcurl3") 72 | endif() 73 | 74 | # improve dependency list 75 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 76 | 77 | # add postinst and prerm hooks to Debian package 78 | configure_file( 79 | ${PROJECT_SOURCE_DIR}/resources/install-scripts/post-install.in 80 | ${PROJECT_BINARY_DIR}/cmake/debian/postinst 81 | @ONLY 82 | ) 83 | configure_file( 84 | ${PROJECT_SOURCE_DIR}//resources/install-scripts/post-uninstall.in 85 | ${PROJECT_BINARY_DIR}/cmake/debian/postrm 86 | @ONLY 87 | ) 88 | 89 | set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA 90 | "${PROJECT_BINARY_DIR}/cmake/debian/postinst" 91 | "${PROJECT_BINARY_DIR}/cmake/debian/postrm" 92 | ) 93 | -------------------------------------------------------------------------------- /src/cli/commands/IntegrateCommand.cpp: -------------------------------------------------------------------------------- 1 | // library headers 2 | #include 3 | 4 | // local headers 5 | #include "IntegrateCommand.h" 6 | #include "exceptions.h" 7 | #include "shared.h" 8 | #include "logging.h" 9 | 10 | namespace appimagelauncher { 11 | namespace cli { 12 | namespace commands { 13 | void IntegrateCommand::exec(QList arguments) { 14 | if (arguments.empty()) { 15 | throw InvalidArgumentsError("No AppImages passed on commandline"); 16 | } 17 | 18 | // make sure all AppImages exist on disk before further processing 19 | for (auto& path : arguments) { 20 | if (!QFileInfo(path).exists()) { 21 | throw UsageError("could not find file " + path); 22 | } 23 | 24 | // make path absolute 25 | // that will just prevent mistakes in libappimage and shared etc. 26 | // (stuff like TryExec keys etc. being set to paths relative to CWD when running the command , ...) 27 | path = QFileInfo(path).absoluteFilePath(); 28 | } 29 | 30 | for (const auto& pathToAppImage : arguments) { 31 | qout() << "Processing " << pathToAppImage << endl; 32 | 33 | if (!QFileInfo(pathToAppImage).isFile()) { 34 | qerr() << "Warning: Not a file, skipping: " << pathToAppImage << endl; 35 | continue; 36 | } 37 | 38 | if (!isAppImage(pathToAppImage)) { 39 | qerr() << "Warning: Not an AppImage, skipping: " << pathToAppImage << endl; 40 | continue; 41 | } 42 | 43 | if (hasAlreadyBeenIntegrated(pathToAppImage)) { 44 | if (desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) { 45 | qout() << "AppImage has been integrated already and doesn't need to be re-integrated, skipping" << endl; 46 | continue; 47 | } 48 | 49 | qout() << "AppImage has already been integrated, but needs to be reintegrated" << endl; 50 | } 51 | 52 | auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage); 53 | 54 | // check if it's already in the right place 55 | if (QFileInfo(pathToAppImage).absoluteFilePath() != QFileInfo(pathToIntegratedAppImage).absoluteFilePath()) { 56 | qout() << "Moving AppImage to integration directory" << endl; 57 | 58 | if (QFile::exists(pathToIntegratedAppImage) && !QFile(pathToIntegratedAppImage).remove()) { 59 | qerr() << "Could not move AppImage into integration directory (error: failed to overwrite existing file)" << endl; 60 | continue; 61 | } 62 | 63 | if (!QFile(pathToAppImage).rename(pathToIntegratedAppImage)) { 64 | qerr() << "Cannot move AppImage to integration directory (permission problem?), attempting to copy instead" << endl; 65 | 66 | if (!QFile(pathToAppImage).copy(pathToIntegratedAppImage)) { 67 | throw CliError("Failed to copy AppImage, giving up"); 68 | } 69 | } 70 | } else { 71 | qout() << "AppImage already in integration directory" << endl; 72 | } 73 | 74 | installDesktopFileAndIcons(pathToAppImage); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/daemon/main.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | #include 5 | 6 | // library includes 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // local includes 14 | #include "shared.h" 15 | #include "filesystemwatcher.h" 16 | #include "worker.h" 17 | 18 | int main(int argc, char* argv[]) { 19 | // make sure shared won't try to use the UI 20 | setenv("_FORCE_HEADLESS", "1", 1); 21 | 22 | QCoreApplication app(argc, argv); 23 | 24 | // by default, watch configured/default destination directory only 25 | const auto defaultDestination = integratedAppImagesDestination(); 26 | FileSystemWatcher watcher(defaultDestination.absolutePath()); 27 | 28 | // create a daemon worker instance 29 | // it is used to integrate all AppImages initially, and to integrate files found via inotify 30 | Worker worker; 31 | 32 | // initial search for AppImages; if AppImages are found, they will be integrated, unless they already are 33 | std::cout << "Searching for existing AppImages" << std::endl; 34 | for (const auto& dir : watcher.directories()) { 35 | for (QDirIterator it(dir); it.hasNext();) { 36 | const auto& path = it.next(); 37 | if (QFileInfo(path).isFile()) { 38 | const auto appImageType = appimage_get_type(path.toStdString().c_str(), false); 39 | const auto isAppImage = 0 < appImageType && appImageType <= 2; 40 | if (isAppImage) { 41 | // at application startup, we don't want to integrate AppImages that have been integrated already, 42 | // as that it slows down very much 43 | // the integration will be updated as soon as any of these AppImages is run with AppImageLauncher 44 | std::cout << "Found AppImage: " << path.toStdString() << std::endl; 45 | if (!appimage_is_registered_in_system(path.toStdString().c_str())) { 46 | std::cout << "AppImage is not integrated yet, integrating" << std::endl; 47 | worker.scheduleForIntegration(path); 48 | } else if (!desktopFileHasBeenUpdatedSinceLastUpdate(path)) { 49 | std::cout << "AppImage has been integrated already but needs to be reintegrated" << std::endl; 50 | worker.scheduleForIntegration(path); 51 | } else { 52 | std::cout << "AppImage integrated already, skipping" << std::endl; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | // (re-)integrate all AppImages at once 59 | worker.executeDeferredOperations(); 60 | 61 | // after (re-)integrating all AppImages, clean up old desktop integration resources before start 62 | if (!cleanUpOldDesktopIntegrationResources()) { 63 | std::cout << "Failed to clean up old desktop integration resources" << std::endl; 64 | } 65 | 66 | std::cout << "Watching directories: "; 67 | for (const auto& dir : watcher.directories()) { 68 | std::cout << dir.toStdString().c_str(); 69 | } 70 | std::cout << std::endl; 71 | 72 | FileSystemWatcher::connect(&watcher, &FileSystemWatcher::fileChanged, &worker, &Worker::scheduleForIntegration, Qt::QueuedConnection); 73 | FileSystemWatcher::connect(&watcher, &FileSystemWatcher::fileRemoved, &worker, &Worker::scheduleForUnintegration, Qt::QueuedConnection); 74 | 75 | if (!watcher.startWatching()) { 76 | std::cerr << "Could not start watching directories" << std::endl; 77 | return 1; 78 | } 79 | 80 | watcher.startWatching(); 81 | QObject::connect(&app, &QCoreApplication::aboutToQuit, &watcher, &FileSystemWatcher::stopWatching); 82 | 83 | return QCoreApplication::exec(); 84 | } 85 | -------------------------------------------------------------------------------- /resources/icons/hicolor/scalable/apps/AppImageLauncher.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 35 | 37 | 46 | 51 | 55 | 62 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/shared/shared.h: -------------------------------------------------------------------------------- 1 | /* central file for utility functions */ 2 | 3 | #pragma once 4 | 5 | // system headers 6 | #include 7 | #include 8 | 9 | // library headers 10 | #include 11 | #include 12 | #include 13 | 14 | enum IntegrationState { 15 | INTEGRATION_FAILED = 0, 16 | INTEGRATION_SUCCESSFUL, 17 | INTEGRATION_ABORTED 18 | }; 19 | 20 | // standard location for integrated AppImages 21 | // currently hardcoded, can not be changed by users 22 | static const auto DEFAULT_INTEGRATION_DESTINATION = QString(getenv("HOME")) + "/Applications/"; 23 | 24 | // little convenience method to display warnings 25 | void displayWarning(const QString& message); 26 | 27 | // little convenience method to display errors 28 | void displayError(const QString& message); 29 | 30 | // reliable way to check if the current session is graphical or not 31 | bool isHeadless(); 32 | 33 | // makes an existing file executable 34 | bool makeExecutable(const QString& path); 35 | 36 | // removes executable bits from file's permissions 37 | bool makeNonExecutable(const QString& path); 38 | 39 | // installs desktop file for given AppImage, including AppImageLauncher specific modifications 40 | // set resolveCollisions to false in order to leave the Name entries as-is 41 | bool installDesktopFileAndIcons(const QString& pathToAppImage, bool resolveCollisions = true); 42 | 43 | // update AppImage's existing desktop file with AppImageLauncher specific entries 44 | // this alias for installDesktopFileAndIcons does not perform any collision detection and resolving 45 | bool updateDesktopFileAndIcons(const QString& pathToAppImage); 46 | 47 | // update desktop database and icon caches of desktop environments 48 | // this makes sure that: 49 | // - outdated entries are removed from the launcher 50 | // - icons of freshly integrated AppImages are displayed in the launcher 51 | bool updateDesktopDatabaseAndIconCaches(); 52 | 53 | // integrates an AppImage using a standard workflow used across all AppImageLauncher applications 54 | IntegrationState integrateAppImage(const QString& pathToAppImage, const QString& pathToIntegratedAppImage); 55 | 56 | // write config file to standard location with given configuration values 57 | // askToMove and enableDaemon both are bools but represented as int to add some sort of "unset" state 58 | // < 0: unset; 0 = false; > 0 = true 59 | // destination is a string that, when empty, will be interpreted as "use default" 60 | void createConfigFile(int askToMove, QString destination, int enableDaemon); 61 | 62 | // load config file and return it 63 | std::shared_ptr getConfig(); 64 | 65 | // return directory into which the integrated AppImages will be moved 66 | QDir integratedAppImagesDestination(); 67 | 68 | // build path to standard location for integrated AppImages 69 | QString buildPathToIntegratedAppImage(const QString& pathToAppImage); 70 | 71 | // get AppImage MD5 digest 72 | // extracts the digest embedded in the file 73 | // if no such digest has been embedded, it calculates it using libappimage 74 | QString getAppImageDigestMd5(const QString& path); 75 | 76 | // checks whether AppImage has been integrated already 77 | bool hasAlreadyBeenIntegrated(const QString& pathToAppImage); 78 | 79 | // checks whether file is in a given directory 80 | bool isInDirectory(const QString& pathToAppImage, const QDir& directory); 81 | 82 | // clean up old desktop files (and related resources, such as icons) 83 | bool cleanUpOldDesktopIntegrationResources(bool verbose = false); 84 | 85 | // returns absolute path to currently running binary 86 | std::shared_ptr getOwnBinaryPath(); 87 | 88 | // returns true if AppImageLauncher was updated since the desktop file for a given AppImage has been updated last 89 | bool desktopFileHasBeenUpdatedSinceLastUpdate(const QString& pathToAppImage); 90 | 91 | // returns true if the AppImageLauncherFS service was restarted since the last AppImageLauncher update 92 | bool fsDaemonHasBeenRestartedSinceLastUpdate(); 93 | 94 | // checks whether a file is an AppImage 95 | bool isAppImage(const QString& path); 96 | -------------------------------------------------------------------------------- /cmake/install.cmake: -------------------------------------------------------------------------------- 1 | # define private libraries install destionation 2 | if(NOT IS_ABSOLUTE ${CMAKE_INSTALL_LIBDIR}) 3 | set(_libdir ${CMAKE_INSTALL_LIBDIR}) 4 | else() 5 | file(RELATIVE_PATH _libdir ${CMAKE_INSTALL_LIBDIR} ${CMAKE_INSTALL_PREFIX}) 6 | endif() 7 | 8 | set(_private_libdir ${_libdir}/appimagelauncher) 9 | 10 | # calculate relative path from binary install destination to private library install dir 11 | if(NOT IS_ABSOLUTE ${CMAKE_INSTALL_BINDIR}) 12 | set(_bindir ${CMAKE_INSTALL_BINDIR}) 13 | else() 14 | file(RELATIVE_PATH _bindir ${CMAKE_INSTALL_BINDIR} ${CMAKE_INSTALL_PREFIX}) 15 | #set(_bindir ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}) 16 | endif() 17 | 18 | set(_abs_bindir ${CMAKE_INSTALL_PREFIX}/${_bindir}) 19 | set(_abs_private_libdir ${CMAKE_INSTALL_PREFIX}/${_private_libdir}) 20 | 21 | file(RELATIVE_PATH _rpath ${_abs_bindir} ${_abs_private_libdir}) 22 | set(_rpath "\$ORIGIN/${_rpath}") 23 | 24 | 25 | # install libappimage.so into lib/appimagekit to avoid overwriting a libappimage potentially installed into /usr/lib 26 | # or /usr/lib/x86_64-... or wherever the OS puts its libraries 27 | # for some reason, using TARGETS ... doesn't work here, therefore using the absolute file path 28 | file(GLOB libappimage_files ${PROJECT_BINARY_DIR}/lib/libappimage/src/libappimage/libappimage.so*) 29 | file(GLOB libappimageupdate_files ${PROJECT_BINARY_DIR}/lib/AppImageUpdate/src/libappimageupdate.so*) 30 | file(GLOB libappimageupdate-qt_files ${PROJECT_BINARY_DIR}/lib/AppImageUpdate/src/qt-ui/libappimageupdate-qt.so*) 31 | message(STATUS ${PROJECT_BINARY_DIR}/lib/libappimage/src/libappimage/libappimage.so*) 32 | foreach(i libappimage libappimageupdate libappimageupdate-qt) 33 | # prevent unnecessary messages 34 | if(NOT i STREQUAL libappimage OR NOT USE_SYSTEM_LIBAPPIMAGE) 35 | if(NOT ${i}_files) 36 | message(WARNING "Could not find ${i} library files, cannot bundle; if you want to bundle the files, please re-run cmake before calling make install") 37 | else() 38 | install( 39 | FILES 40 | ${${i}_files} 41 | DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER 42 | ) 43 | endif() 44 | endif() 45 | endforeach() 46 | 47 | if(NOT BUILD_LITE) 48 | # TODO: find alternative to the following "workaround" (a pretty dirty hack, actually...) 49 | # bundle update-binfmts as a fallback for distros which don't have it installed 50 | find_program(UPDATE_BINFMTS 51 | NAMES update-binfmts 52 | PATHS /usr/sbin 53 | ) 54 | 55 | if(NOT UPDATE_BINFMTS STREQUAL UPDATE_BINFMTS-NOTFOUND AND EXISTS ${UPDATE_BINFMTS}) 56 | message(STATUS "Found update-binfmts, bundling: ${UPDATE_BINFMTS}") 57 | install( 58 | FILES /usr/sbin/update-binfmts 59 | PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE 60 | DESTINATION ${_private_libdir} COMPONENT APPIMAGELAUNCHER 61 | ) 62 | else() 63 | message(WARNING "update-binfmts could not be found. Please install the binfmt-support package if you intend to build RPM packages.") 64 | endif() 65 | 66 | # binfmt.d config file -- used as a fallback, if update-binfmts is not available 67 | configure_file( 68 | ${PROJECT_SOURCE_DIR}/resources/binfmt.d/appimage.conf.in 69 | ${PROJECT_BINARY_DIR}/resources/binfmt.d/appimage.conf 70 | @ONLY 71 | ) 72 | # caution: don't use ${CMAKE_INSTALL_LIBDIR} here, it's really just lib/binfmt.d 73 | install( 74 | FILES ${PROJECT_BINARY_DIR}/resources/binfmt.d/appimage.conf 75 | DESTINATION lib/binfmt.d COMPONENT APPIMAGELAUNCHER 76 | ) 77 | 78 | # install systemd service configuration for appimagelauncherfs 79 | configure_file( 80 | ${PROJECT_SOURCE_DIR}/resources/appimagelauncherfs.service.in 81 | ${PROJECT_BINARY_DIR}/resources/appimagelauncherfs.service 82 | @ONLY 83 | ) 84 | # caution: don't use ${CMAKE_INSTALL_LIBDIR} here, it's really just lib/systemd/user 85 | install( 86 | FILES ${PROJECT_BINARY_DIR}/resources/appimagelauncherfs.service 87 | DESTINATION lib/systemd/user/ COMPONENT APPIMAGELAUNCHERFS 88 | ) 89 | endif() 90 | 91 | # install systemd service configuration for appimagelauncherd 92 | configure_file( 93 | ${PROJECT_SOURCE_DIR}/resources/appimagelauncherd.service.in 94 | ${PROJECT_BINARY_DIR}/resources/appimagelauncherd.service 95 | @ONLY 96 | ) 97 | # caution: don't use ${CMAKE_INSTALL_LIBDIR} here, it's really just lib/systemd/user 98 | install( 99 | FILES ${PROJECT_BINARY_DIR}/resources/appimagelauncherd.service 100 | DESTINATION lib/systemd/user/ COMPONENT APPIMAGELAUNCHER 101 | ) 102 | -------------------------------------------------------------------------------- /src/ui/remove_main.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | 5 | // library includes 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | extern "C" { 18 | #include 19 | } 20 | 21 | // local includes 22 | #include "shared.h" 23 | #include "translationmanager.h" 24 | #include "trashbin.h" 25 | #include "ui_remove.h" 26 | 27 | bool unregisterAppImage(const QString& pathToAppImage) { 28 | auto rv = appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false); 29 | 30 | if (rv != 0) 31 | return false; 32 | 33 | return true; 34 | } 35 | 36 | int main(int argc, char** argv) { 37 | QCommandLineParser parser; 38 | parser.setApplicationDescription(QObject::tr("Removes AppImages after desktop integration, for use by Linux distributions")); 39 | QApplication app(argc, argv); 40 | QApplication::setApplicationDisplayName(QObject::tr("AppImageLauncher remove", "remove helper app name")); 41 | QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg")); 42 | 43 | std::ostringstream version; 44 | version << "version " << APPIMAGELAUNCHER_VERSION << " " 45 | << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on " 46 | << APPIMAGELAUNCHER_BUILD_DATE; 47 | QApplication::setApplicationVersion(QString::fromStdString(version.str())); 48 | 49 | // install translations 50 | TranslationManager translationManager(app); 51 | 52 | parser.addHelpOption(); 53 | parser.addVersionOption(); 54 | 55 | parser.process(app); 56 | 57 | parser.addPositionalArgument("path", QObject::tr("Path to AppImage"), QObject::tr("")); 58 | 59 | if (parser.positionalArguments().empty()) { 60 | parser.showHelp(1); 61 | } 62 | 63 | const auto pathToAppImage = parser.positionalArguments().first(); 64 | 65 | if (!QFile(pathToAppImage).exists()) { 66 | QMessageBox::critical(nullptr, "Error", QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage)); 67 | return 1; 68 | } 69 | 70 | const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false); 71 | 72 | if (type <= 0 || type > 2) { 73 | QMessageBox::critical( 74 | nullptr, 75 | QObject::tr("AppImage remove helper error"), 76 | QObject::tr("Not an AppImage:\n\n%1").arg(pathToAppImage) 77 | ); 78 | return 1; 79 | } 80 | 81 | // this tool should not do anything if the file isn't integrated 82 | // the file is only supposed to work on integrated AppImages and _nothing else_ 83 | if (!hasAlreadyBeenIntegrated(pathToAppImage)) { 84 | QMessageBox::critical( 85 | nullptr, 86 | QObject::tr("AppImage remove helper error"), 87 | QObject::tr("Refusing to work on non-integrated AppImage:\n\n%1").arg(pathToAppImage) 88 | ); 89 | return 1; 90 | } 91 | 92 | QDialog dialog; 93 | Ui::RemoveDialog ui; 94 | ui.setupUi(&dialog); 95 | ui.pathLabel->setText(pathToAppImage); 96 | 97 | auto rv = dialog.exec(); 98 | 99 | switch (rv) { 100 | case 1: { 101 | // first, unregister AppImage 102 | if (!unregisterAppImage(pathToAppImage)) { 103 | QMessageBox::critical( 104 | nullptr, 105 | QObject::tr("Error"), 106 | QObject::tr("Failed to unregister AppImage: %1").arg(pathToAppImage) 107 | ); 108 | return 1; 109 | } 110 | 111 | if (ui.checkBox->checkState() == Qt::Checked) { 112 | TrashBin bin; 113 | 114 | // now, move AppImage into trash bin 115 | if (!bin.disposeAppImage(pathToAppImage)) { 116 | QMessageBox::critical( 117 | nullptr, 118 | QObject::tr("Error"), 119 | QObject::tr("Failed to move AppImage into trash bin directory") 120 | ); 121 | return 1; 122 | } 123 | 124 | // run clean up cycle for trash bin 125 | // if the current AppImage is ready to be deleted, this call will immediately remove it from the system 126 | // otherwise, it'll be cleaned up at some subsequent run of AppImageLauncher or the removal tool 127 | if (!bin.cleanUp()) { 128 | QMessageBox::critical( 129 | nullptr, 130 | QObject::tr("Error"), 131 | QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path()) 132 | ); 133 | return 1; 134 | } 135 | 136 | // update desktop database and icon caches 137 | if (!updateDesktopDatabaseAndIconCaches()) 138 | return 1; 139 | } 140 | 141 | return 0; 142 | } 143 | default: { 144 | // exit without any actions 145 | return 0; 146 | } 147 | } 148 | 149 | // sanity check: this line _should_ be unreachable 150 | throw std::runtime_error("This line shouldn't have been reachable..."); 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/ui/settings_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SettingsDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 472 10 | 376 11 | 12 | 13 | 14 | AppImageLauncher Settings 15 | 16 | 17 | 18 | 19 | 20 | Launcher Dialog 21 | 22 | 23 | 24 | 25 | 26 | Ask whether to move AppImage files into the applications directory 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Applications directory 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | .. 47 | 48 | 49 | 50 | 51 | 52 | 53 | Location where to store your AppImage files to ease their management 54 | 55 | 56 | true 57 | 58 | 59 | 60 | 61 | 62 | 63 | /path 64 | 65 | 66 | 67 | 68 | 69 | 70 | Enable auto-integration daemon 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 0 82 | 0 83 | 84 | 85 | 86 | 87 | 88 | 89 | Available Features 90 | 91 | 92 | 93 | 94 | 10 95 | 30 96 | 431 97 | 430 98 | 99 | 100 | 101 | 102 | 0 103 | 0 104 | 105 | 106 | 107 | 108 | 0 109 | 430 110 | 111 | 112 | 113 | Qt::LeftToRight 114 | 115 | 116 | TextLabel 117 | 118 | 119 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 120 | 121 | 122 | true 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | Qt::Horizontal 131 | 132 | 133 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | buttonBox 143 | accepted() 144 | SettingsDialog 145 | accept() 146 | 147 | 148 | 248 149 | 254 150 | 151 | 152 | 157 153 | 274 154 | 155 | 156 | 157 | 158 | buttonBox 159 | rejected() 160 | SettingsDialog 161 | reject() 162 | 163 | 164 | 316 165 | 260 166 | 167 | 168 | 286 169 | 274 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/fswatcher/filesystemwatcher.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | #include 5 | 6 | // library includes 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // local includes 14 | #include "filesystemwatcher.h" 15 | 16 | class INotifyEvent { 17 | public: 18 | uint32_t mask; 19 | QString path; 20 | 21 | public: 22 | INotifyEvent(uint32_t mask, QString path) : mask(mask), path(std::move(path)) {} 23 | }; 24 | 25 | class FileSystemWatcher::PrivateData { 26 | public: 27 | enum EVENT_TYPES { 28 | // events that indicate file creations, modifications etc. 29 | fileChangeEvents = IN_CLOSE_WRITE | IN_MOVE, 30 | // events that indicate a file removal from a directory, e.g., deletion or moving to another location 31 | fileRemovalEvents = IN_DELETE | IN_MOVED_FROM, 32 | }; 33 | 34 | public: 35 | QStringList watchedDirectories; 36 | QTimer eventsLoopTimer; 37 | 38 | private: 39 | int fd = -1; 40 | std::map watchFdMap; 41 | 42 | public: 43 | // reads events from the inotify fd and emits the correct signals 44 | std::vector readEventsFromFd() { 45 | // read raw bytes into buffer 46 | // this is necessary, as the inotify_events have dynamic sizes 47 | static const auto bufSize = 4096; 48 | char buffer[bufSize] __attribute__ ((aligned(8))); 49 | 50 | const auto rv = read(fd, buffer, bufSize); 51 | const auto error = errno; 52 | 53 | if (rv == 0) { 54 | throw FileSystemWatcherError("read() on inotify FD must never return 0"); 55 | } 56 | 57 | if (rv == -1) { 58 | // we're using a non-blocking inotify fd, therefore, if errno is set to EAGAIN, we just didn't find any 59 | // new events 60 | // this is not an error case 61 | if (error == EAGAIN) 62 | return {}; 63 | 64 | throw FileSystemWatcherError(QString("Failed to read from inotify fd: ") + strerror(error)); 65 | } 66 | 67 | // read events into vector 68 | std::vector events; 69 | 70 | for (char* p = buffer; p < buffer + rv;) { 71 | // create inotify_event from current position in buffer 72 | auto* currentEvent = (struct inotify_event*) p; 73 | 74 | // initialize new INotifyEvent with the data from the currentEvent 75 | QString relativePath(currentEvent->name); 76 | auto directoryPath = watchFdMap[currentEvent->wd]; 77 | events.emplace_back(currentEvent->mask, directoryPath + "/" + relativePath); 78 | 79 | // update current position in buffer 80 | p += sizeof(struct inotify_event) + currentEvent->len; 81 | } 82 | 83 | return events; 84 | } 85 | 86 | PrivateData() : watchedDirectories() { 87 | fd = inotify_init1(IN_NONBLOCK); 88 | if (fd < 0) { 89 | auto error = errno; 90 | throw FileSystemWatcherError(strerror(error)); 91 | } 92 | }; 93 | 94 | bool startWatching() { 95 | static const auto mask = fileChangeEvents | fileRemovalEvents; 96 | 97 | for (const auto& directory : watchedDirectories) { 98 | const int watchFd = inotify_add_watch(fd, directory.toStdString().c_str(), mask); 99 | 100 | if (watchFd == -1) { 101 | const auto error = errno; 102 | std::cerr << "Failed to start watching: " << strerror(error) << std::endl; 103 | return false; 104 | } 105 | 106 | watchFdMap[watchFd] = directory; 107 | eventsLoopTimer.start(); 108 | } 109 | 110 | return true; 111 | } 112 | 113 | bool stopWatching() { 114 | while (!watchFdMap.empty()) { 115 | const auto pair = *(watchFdMap.begin()); 116 | const auto watchFd = pair.first; 117 | 118 | if (inotify_rm_watch(fd, watchFd) == -1) { 119 | const auto error = errno; 120 | std::cerr << "Failed to stop watching: " << strerror(error) << std::endl; 121 | return false; 122 | } 123 | 124 | watchFdMap.erase(watchFd); 125 | eventsLoopTimer.stop(); 126 | } 127 | 128 | return true; 129 | } 130 | }; 131 | 132 | FileSystemWatcher::FileSystemWatcher() { 133 | d = std::make_shared(); 134 | 135 | d->eventsLoopTimer.setInterval(100); 136 | connect(&d->eventsLoopTimer, &QTimer::timeout, this, &FileSystemWatcher::readEvents); 137 | } 138 | 139 | FileSystemWatcher::FileSystemWatcher(const QString& path) : FileSystemWatcher() { 140 | if (!QDir(path).exists()) 141 | QDir().mkdir(path); 142 | d->watchedDirectories.append(path); 143 | } 144 | 145 | FileSystemWatcher::FileSystemWatcher(const QStringList& paths) : FileSystemWatcher() { 146 | d->watchedDirectories.append(paths); 147 | } 148 | 149 | QStringList FileSystemWatcher::directories() { 150 | return d->watchedDirectories; 151 | } 152 | 153 | bool FileSystemWatcher::startWatching() { 154 | return d->startWatching(); 155 | } 156 | 157 | bool FileSystemWatcher::stopWatching() { 158 | return d->stopWatching(); 159 | } 160 | 161 | void FileSystemWatcher::readEvents() { 162 | auto events = d->readEventsFromFd(); 163 | 164 | for (const auto& event : events) { 165 | const auto mask = event.mask; 166 | 167 | if (mask & d->fileChangeEvents) { 168 | emit fileChanged(event.path); 169 | } else if (mask & d->fileRemovalEvents) { 170 | emit fileRemoved(event.path); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/ui/first-run.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | 4 | // library includes 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // local includes 20 | #include "ui_first-run.h" 21 | #include "shared.h" 22 | 23 | 24 | class FirstRunDialog : public QDialog { 25 | private: 26 | Ui::FirstRunDialog* firstRunDialog{}; 27 | 28 | // stores custom destination dir for AppImages 29 | // if this is empty, the default value (defined in-code, might change over time) will be chosen 30 | // the default will not be written to the config file, hence we need a way to detect that state, and it's assumed 31 | // that when this string is empty, that's the case 32 | // we could also just compare this directory with the default value just before saving, but IMO it's more obvious 33 | // to the user that the "default" state is lost after having saved something with the "choose" button in a file 34 | // dialog 35 | QString destinationDir; 36 | 37 | private Q_SLOTS: 38 | void resetDefaults() { 39 | firstRunDialog->askMoveCheckBox->setChecked(true); 40 | 41 | destinationDir = ""; 42 | updateDestinationDirLabel(); 43 | } 44 | 45 | void handleButtonClicked(QAbstractButton* button) { 46 | if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::RestoreDefaults)) { 47 | qDebug() << "restore defaults"; 48 | resetDefaults(); 49 | } else if (button == firstRunDialog->buttonBox->button(QDialogButtonBox::Help)) { 50 | qDebug() << "help"; 51 | QDesktopServices::openUrl(QUrl("https://github.com/TheAssassin/AppImageLauncher/wiki/First-run")); 52 | 53 | } else { 54 | qDebug() << "unknown button clicked" << button; 55 | } 56 | } 57 | 58 | void handleAskMoveCheckBoxStateChange(int state) { 59 | qDebug() << "new ask move check box state" << state; 60 | 61 | // this alone unfortunately doesn't do the trick... 62 | for (auto* layout : { 63 | static_cast(firstRunDialog->destDirVertLayout), 64 | static_cast(firstRunDialog->destDirHorLayout), 65 | }) { 66 | layout->setEnabled(state > 0); 67 | } 68 | 69 | // have to also manually enable/disable all the 70 | for (auto* label : { 71 | static_cast(firstRunDialog->destinationDirDescLabel), 72 | static_cast(firstRunDialog->destinationDirLabel), 73 | static_cast(firstRunDialog->customizeIntegrationDirButton), 74 | }) { 75 | label->setEnabled(state > 0); 76 | } 77 | } 78 | 79 | void handleCustomizeIntegrationDirButtonClicked(bool checked = false) { 80 | (void) checked; 81 | 82 | auto oldDir = destinationDir; 83 | if (oldDir.isEmpty()) 84 | oldDir = integratedAppImagesDestination().absolutePath(); 85 | 86 | auto newDir = QFileDialog::getExistingDirectory(this, tr("Choose integration destination dir"), oldDir); 87 | 88 | // the call above returns an empty string if the user aborts the dialog 89 | if (!newDir.isEmpty()) { 90 | destinationDir = newDir; 91 | } 92 | 93 | // updating never is a bad idea 94 | updateDestinationDirLabel(); 95 | } 96 | 97 | private: 98 | void updateDestinationDirLabel() { 99 | QString text = destinationDir; 100 | 101 | // fallback to default 102 | if (text.isEmpty()) 103 | text = integratedAppImagesDestination().absolutePath() + " " + tr("(default)"); 104 | 105 | firstRunDialog->destinationDirLabel->setText(text); 106 | } 107 | 108 | void initUi() { 109 | firstRunDialog = new Ui::FirstRunDialog; 110 | // setupUi will modify this dialog so that it looks just like what we designed in Qt Designer 111 | firstRunDialog->setupUi(this); 112 | 113 | // set up logo in a QLabel 114 | firstRunDialog->logoLabel->setText(""); 115 | auto pixmap = QPixmap::fromImage(QImage(":/AppImageLauncher.svg")).scaled(QSize(128,128), 116 | Qt::KeepAspectRatio, Qt::SmoothTransformation 117 | ); 118 | firstRunDialog->logoLabel->setPixmap(pixmap); 119 | 120 | // setting icon in Qt Designer doesn't seem to work 121 | firstRunDialog->customizeIntegrationDirButton->setIcon(this->style()->standardIcon(QStyle::SP_DirIcon)); 122 | 123 | // reset defaults 124 | resetDefaults(); 125 | 126 | // set up all connections 127 | connect(firstRunDialog->buttonBox, &QDialogButtonBox::clicked, this, &FirstRunDialog::handleButtonClicked); 128 | connect(firstRunDialog->askMoveCheckBox, &QCheckBox::stateChanged, this, &FirstRunDialog::handleAskMoveCheckBoxStateChange); 129 | 130 | connect(firstRunDialog->customizeIntegrationDirButton, &QPushButton::clicked, this, &FirstRunDialog::handleCustomizeIntegrationDirButtonClicked); 131 | } 132 | 133 | public: 134 | FirstRunDialog() { 135 | initUi(); 136 | } 137 | 138 | void writeConfigFile() { 139 | bool askToMove = firstRunDialog->askMoveCheckBox->checkState() == Qt::Checked; 140 | createConfigFile(askToMove ? 1 : 0, destinationDir, -1); 141 | } 142 | }; 143 | 144 | 145 | void showFirstRunDialog() { 146 | auto dialog = new FirstRunDialog; 147 | 148 | auto rv = dialog->exec(); 149 | 150 | if (rv <= 0) { 151 | QApplication::exit(3); 152 | exit(3); 153 | } 154 | 155 | dialog->writeConfigFile(); 156 | } 157 | -------------------------------------------------------------------------------- /src/daemon/worker.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | #include 5 | 6 | // library includes 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // local includes 16 | #include "worker.h" 17 | #include "shared.h" 18 | 19 | enum OP_TYPE { 20 | INTEGRATE = 0, 21 | UNINTEGRATE = 1, 22 | }; 23 | 24 | typedef std::pair Operation; 25 | 26 | class Worker::PrivateData { 27 | public: 28 | QTimer deferredOperationsTimer; 29 | 30 | static constexpr int TIMEOUT = 15 * 1000; 31 | 32 | // std::set is unordered, therefore using std::deque to keep the order of the operations 33 | std::deque deferredOperations; 34 | 35 | class OperationTask : public QRunnable { 36 | private: 37 | Operation operation; 38 | std::shared_ptr mutex; 39 | 40 | public: 41 | OperationTask(const Operation& operation, std::shared_ptr mutex) : operation(operation), 42 | mutex(std::move(mutex)) {} 43 | 44 | void run() override { 45 | const auto& path = operation.first; 46 | const auto& type = operation.second; 47 | 48 | const auto exists = QFile::exists(path); 49 | const auto appImageType = appimage_get_type(path.toStdString().c_str(), false); 50 | const auto isAppImage = 0 < appImageType && appImageType <= 2; 51 | 52 | if (type == INTEGRATE) { 53 | { // Scope for Output Mutex Locker 54 | QMutexLocker mutexLocker(mutex.get()); 55 | std::cout << "Integrating: " << path.toStdString() << std::endl; 56 | 57 | if (!exists) { 58 | std::cout << "ERROR: file does not exist, cannot integrate" << std::endl; 59 | return; 60 | } 61 | 62 | if (!isAppImage) { 63 | std::cout << "ERROR: not an AppImage, skipping" << std::endl; 64 | return; 65 | } 66 | } 67 | 68 | // check for X-AppImage-Integrate=false 69 | if (appimage_shall_not_be_integrated(path.toStdString().c_str())) { 70 | QMutexLocker mutexLocker(mutex.get()); 71 | std::cout << "WARNING: AppImage shall not be integrated, skipping" << std::endl; 72 | return; 73 | } 74 | 75 | if (!installDesktopFileAndIcons(path)) { 76 | QMutexLocker mutexLocker(mutex.get()); 77 | std::cout << "ERROR: Failed to register AppImage in system" << std::endl; 78 | return; 79 | } 80 | } else if (type == UNINTEGRATE) { 81 | // nothing to do 82 | } 83 | } 84 | }; 85 | 86 | public: 87 | PrivateData() { 88 | deferredOperationsTimer.setSingleShot(true); 89 | deferredOperationsTimer.setInterval(TIMEOUT); 90 | } 91 | 92 | public: 93 | // in addition to a simple duplicate check, this function is context sensitive 94 | // it starts with the last element, and checks for duplicates until an opposite action is found 95 | // for instance, when the element shall integrated, it will check for duplicates until an unintegration operation 96 | // is found 97 | bool isDuplicate(Operation operation) { 98 | for (auto it = deferredOperations.rbegin(); it != deferredOperations.rend(); ++it) { 99 | if ((*it).first == operation.first) { 100 | // if operation type is different, then the operation is new, and should be added to the list 101 | // if it is equal, it's a duplicate 102 | // in either case, the loop can be aborted here 103 | return (*it).second == operation.second; 104 | } 105 | } 106 | 107 | return false; 108 | } 109 | }; 110 | 111 | Worker::Worker() { 112 | d = std::make_shared(); 113 | 114 | connect(this, &Worker::startTimer, this, &Worker::startTimerIfNecessary, Qt::QueuedConnection); 115 | connect(&d->deferredOperationsTimer, &QTimer::timeout, this, &Worker::executeDeferredOperations); 116 | } 117 | 118 | void Worker::executeDeferredOperations() { 119 | std::cout << "Executing deferred operations" << std::endl; 120 | 121 | auto outputMutex = std::make_shared(); 122 | 123 | while (!d->deferredOperations.empty()) { 124 | auto operation = d->deferredOperations.front(); 125 | d->deferredOperations.pop_front(); 126 | QThreadPool::globalInstance()->start(new PrivateData::OperationTask(operation, outputMutex)); 127 | } 128 | 129 | // wait until all AppImages have been integrated 130 | QThreadPool::globalInstance()->waitForDone(); 131 | 132 | std::cout << "Cleaning up old desktop integration files" << std::endl; 133 | if (!cleanUpOldDesktopIntegrationResources(true)) { 134 | std::cout << "Failed to clean up old desktop integration files" << std::endl; 135 | } 136 | 137 | // make sure the icons in the launcher are refreshed 138 | std::cout << "Updating desktop database and icon caches" << std::endl; 139 | if (!updateDesktopDatabaseAndIconCaches()) 140 | std::cout << "Failed to update desktop database and icon caches" << std::endl; 141 | 142 | std::cout << "Done" << std::endl; 143 | } 144 | 145 | void Worker::scheduleForIntegration(const QString& path) { 146 | auto operation = std::make_pair(path, INTEGRATE); 147 | if (!d->isDuplicate(operation)) { 148 | std::cout << "Scheduling for (re-)integration: " << path.toStdString() << std::endl; 149 | d->deferredOperations.push_back(operation); 150 | emit startTimer(); 151 | } 152 | 153 | } 154 | 155 | void Worker::scheduleForUnintegration(const QString& path) { 156 | auto operation = std::make_pair(path, UNINTEGRATE); 157 | if (!d->isDuplicate(operation)) { 158 | std::cout << "Scheduling for unintegration: " << path.toStdString() << std::endl; 159 | d->deferredOperations.push_back(operation); 160 | emit startTimer(); 161 | } 162 | } 163 | 164 | void Worker::startTimerIfNecessary() { 165 | if (!d->deferredOperationsTimer.isActive()) 166 | QMetaObject::invokeMethod(&d->deferredOperationsTimer, "start"); 167 | } 168 | -------------------------------------------------------------------------------- /resources/appimagelauncher-lite-AppRun.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | if [[ "$VERBOSE" != "" ]]; then 6 | set -x 7 | fi 8 | 9 | # shift does not work if no arguments have been passed, therefore we just handle that situation right now 10 | if [[ "$1" == "" ]]; then 11 | echo "Error: no option passed, use --help for more information" 12 | exit 2 13 | fi 14 | 15 | firstarg="$1" 16 | shift 17 | 18 | 19 | prefix="appimagelauncher-lite" 20 | install_dir=~/.local/lib/appimagelauncher-lite 21 | installed_appimage_path="$install_dir"/appimagelauncher-lite.AppImage 22 | settings_desktop_file_path=~/.local/share/applications/"$prefix"-AppImageLauncherSettings.desktop 23 | systemd_user_units_dir=~/.config/systemd/user/ 24 | appimagelauncherd_systemd_service_name=appimagelauncherd.service 25 | integrated_icon_path=~/.local/share/icons/hicolor/scalable/apps/AppImageLauncher-Lite.svg 26 | no_desktop_integration_marker_path=~/.local/share/appimagekit/no_desktopintegration 27 | 28 | test_globally_installed() { 29 | which AppImageLauncher &>/dev/null && return 0 30 | type AppImageLauncher &>/dev/null && return 0 31 | [[ -d /usr/lib/*/appimagelauncher ]] && return 0 32 | 33 | return 1 34 | } 35 | 36 | test_installed_already() { 37 | [[ -d "$install_dir" ]] && return 0 38 | 39 | return 1 40 | } 41 | 42 | ail_lite_notify_desktop_integration() { 43 | update-desktop-database ~/.local/share/applications 44 | gtk-update-icon-cache 45 | } 46 | 47 | ail_lite_install() { 48 | if [[ "$APPIMAGE" == "" ]]; then 49 | echo "Error: not running from AppImage, aborting" 50 | return 2 51 | fi 52 | 53 | # create default Applications directory 54 | mkdir -p ~/Applications 55 | 56 | # prepare install dir 57 | mkdir -p "$install_dir" 58 | mkdir -p "$install_dir"/systemd 59 | 60 | # copy ourselves to install dir 61 | cp "$APPIMAGE" "$installed_appimage_path" 62 | 63 | # set up appimagelauncherd 64 | cat > "$install_dir"/systemd/"$appimagelauncherd_systemd_service_name" < ~/.local/share/applications/appimagelauncher-lite-AppImageLauncherSettings.desktop < ..." 132 | echo 133 | echo "Main options:" 134 | echo " install Install AppImageLauncher into your user account" 135 | echo " uninstall Uninstall AppImageLauncher from your user account" 136 | echo " help|--help Display this help" 137 | echo 138 | echo "Other options (mainly for use by AppImageLauncher Lite internally):" 139 | echo " appimagelauncherd Run appimagelauncherd" 140 | echo " AppImageLauncherSettings Display AppImageLauncher Lite configuration utility" 141 | echo " cli [or ali-cli] Run AppImageLauncher cli (use \"cli --help\" for more information)" 142 | echo " remove Run removal helper to remove AppImage " 143 | echo " update Run update helper to update AppImage " 144 | } 145 | 146 | # ensure this important variable is available, as most operations (except for install) can be done without being run 147 | # from via AppImage runtime (e.g., while the AppImage is extracted, which is great for testing) 148 | export APPDIR=${APPDIR:-$(readlink -f $(dirname "$0"))} 149 | 150 | case "$firstarg" in 151 | help|--help) 152 | print_help 153 | exit 0 154 | ;; 155 | appimagelauncherd|AppImageLauncherSettings) 156 | exec "$APPDIR"/usr/bin/"$firstarg" "$@" 157 | ;; 158 | cli|ail-cli) 159 | exec "$APPDIR"/usr/bin/ail-cli "$@" 160 | ;; 161 | remove|update) 162 | #exec "$APPDIR"/usr/lib/**/appimagelauncher/"$firstarg" "$@" 163 | exec "$APPDIR"/usr/bin/"$firstarg" "$@" 164 | ;; 165 | install) 166 | if test_globally_installed; then 167 | echo "Error: AppImageLauncher is installed system-wide already, not installing on top" 1>&2 168 | exit 2 169 | fi 170 | 171 | if test_installed_already; then 172 | echo "Error: AppImageLauncher Lite is installed already, please uninstall before trying to reinstall" 1>&2 173 | exit 2 174 | fi 175 | 176 | echo "Installing AppImageLauncher Lite" 177 | ail_lite_install 178 | ;; 179 | uninstall) 180 | if ! test_installed_already; then 181 | echo "Error: AppImageLauncher Lite does not seem to be installed" 1>&2 182 | exit 2 183 | fi 184 | 185 | echo "Uninstalling AppImageLauncher Lite" 186 | ail_lite_uninstall 187 | ;; 188 | *) 189 | echo "Unknown operation: $firstarg"; 190 | exit 2 191 | ;; 192 | esac 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/ui/first-run.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | FirstRunDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 532 10 | 300 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | First run 21 | 22 | 23 | true 24 | 25 | 26 | 27 | QLayout::SetDefaultConstraint 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 0 35 | 36 | 37 | 38 | 39 | 130 40 | 130 41 | 42 | 43 | 44 | 45 | 46 | 47 | :/AppImageLauncher.svg 48 | 49 | 50 | false 51 | 52 | 53 | Qt::AlignHCenter|Qt::AlignTop 54 | 55 | 56 | 8 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 0 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 73 | 74 | 75 | <html><head/><body><p><span style=" font-size:11pt; font-weight:600;">Welcome to AppImageLauncher!</span></p><p>This little helper is designed to improve your AppImage experience on your computer.</p><p>It appears you have never run AppImageLauncher before. Please take a minute and configure your preferences. You can always change these later on, using the control panel.</p></body></html> 76 | 77 | 78 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 79 | 80 | 81 | true 82 | 83 | 84 | 2 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 0 93 | 0 94 | 95 | 96 | 97 | Ask me whether to move new AppImages into a central location 98 | 99 | 100 | true 101 | 102 | 103 | 104 | 105 | 106 | 107 | 0 108 | 109 | 110 | 111 | 112 | 113 | 0 114 | 0 115 | 116 | 117 | 118 | Integration target destination directory: 119 | 120 | 121 | 122 | 123 | 124 | 125 | QLayout::SetDefaultConstraint 126 | 127 | 128 | 129 | 130 | 131 | DejaVu Sans Mono 132 | 133 | 134 | 135 | path placeholder 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 15 144 | 0 145 | 146 | 147 | 148 | 149 | 100 150 | 0 151 | 152 | 153 | 154 | Customize 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | Qt::Horizontal 166 | 167 | 168 | QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Save 169 | 170 | 171 | false 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | buttonBox 181 | accepted() 182 | FirstRunDialog 183 | accept() 184 | 185 | 186 | 248 187 | 254 188 | 189 | 190 | 157 191 | 274 192 | 193 | 194 | 195 | 196 | buttonBox 197 | rejected() 198 | FirstRunDialog 199 | reject() 200 | 201 | 202 | 316 203 | 260 204 | 205 | 206 | 286 207 | 274 208 | 209 | 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /resources/icons/AppImageLauncher_inkscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 23 | 36 | 49 | 62 | 75 | 76 | 102 | 104 | 105 | 107 | image/svg+xml 108 | 110 | 111 | 112 | 113 | 114 | 119 | 124 | 126 | 135 | 144 | 148 | 155 | 162 | 163 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/ui/update_main.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | 5 | // library includes 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | extern "C" { 18 | #include 19 | } 20 | #include 21 | 22 | // local includes 23 | #include "shared.h" 24 | #include "translationmanager.h" 25 | 26 | 27 | int main(int argc, char** argv) { 28 | QCommandLineParser parser; 29 | parser.setApplicationDescription(QObject::tr("Updates AppImages after desktop integration, for use by Linux distributions")); 30 | 31 | QApplication app(argc, argv); 32 | QApplication::setApplicationDisplayName(QObject::tr("AppImageLauncher update", "update helper app name")); 33 | QApplication::setWindowIcon(QIcon(":/AppImageLauncher.svg")); 34 | 35 | std::ostringstream version; 36 | version << "version " << APPIMAGELAUNCHER_VERSION << " " 37 | << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on " 38 | << APPIMAGELAUNCHER_BUILD_DATE; 39 | QApplication::setApplicationVersion(QString::fromStdString(version.str())); 40 | 41 | // install translations 42 | TranslationManager translationManager(app); 43 | 44 | parser.addHelpOption(); 45 | parser.addVersionOption(); 46 | 47 | parser.process(app); 48 | 49 | parser.addPositionalArgument("path", "Path to AppImage", ""); 50 | 51 | if (parser.positionalArguments().empty()) { 52 | parser.showHelp(1); 53 | } 54 | 55 | const auto pathToAppImage = parser.positionalArguments().first(); 56 | 57 | auto criticalUpdaterError = [](const QString& message) { 58 | QMessageBox::critical(nullptr, "Error", message); 59 | }; 60 | 61 | if (!QFile(pathToAppImage).exists()) { 62 | criticalUpdaterError(QString::fromStdString(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage).toStdString())); 63 | return 1; 64 | } 65 | 66 | const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false); 67 | 68 | if (type <= 0 || type > 2) { 69 | criticalUpdaterError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage)); 70 | return 1; 71 | } 72 | 73 | bool hasBeenRegisteredBefore; 74 | 75 | if (!(hasBeenRegisteredBefore = hasAlreadyBeenIntegrated(pathToAppImage))) { 76 | QString message = QObject::tr("The AppImage hasn't been integrated before. This tool will, however, integrate the " 77 | "updated AppImage.") + 78 | "\n\n" + 79 | QObject::tr("Do you wish to continue?"); 80 | 81 | switch (QMessageBox::warning(nullptr, QObject::tr("Warning"), message, QMessageBox::Ok | QMessageBox::Cancel)) { 82 | case (QMessageBox::Ok): 83 | break; 84 | default: 85 | return 0; 86 | } 87 | } 88 | 89 | appimage::update::qt::QtUpdater updater(pathToAppImage); 90 | updater.enableRunUpdatedAppImageButton(false); 91 | 92 | std::ostringstream updaterStatusMessages; 93 | 94 | updater.connect( 95 | &updater, 96 | &appimage::update::qt::QtUpdater::newStatusMessage, 97 | &updater, 98 | [&updater, &updaterStatusMessages](const std::string& newMessage) { 99 | if (!(updaterStatusMessages.tellp() <= 0)) 100 | updaterStatusMessages << std::endl; 101 | 102 | updaterStatusMessages << newMessage; 103 | } 104 | ); 105 | 106 | auto updateCheckResult = updater.checkForUpdates(); 107 | 108 | switch (updateCheckResult) { 109 | case 1: 110 | // update available, continue after switch block 111 | break; 112 | case 0: { 113 | QMessageBox::information( 114 | nullptr, 115 | QObject::tr("No updates found"), 116 | QObject::tr("Could not find updates for AppImage %1").arg(pathToAppImage) 117 | ); 118 | return 0; 119 | } 120 | case -1: { 121 | QMessageBox::information( 122 | nullptr, 123 | QObject::tr("No update information found"), 124 | QObject::tr("Could not find update information in AppImage:\n%1" 125 | "\n" 126 | "\n" 127 | "The AppImage doesn't support updating. Please ask the authors to set up" 128 | "update information to allow for easy updating.").arg(pathToAppImage) 129 | ); 130 | return 0; 131 | } 132 | default: { 133 | QMessageBox::information( 134 | nullptr, 135 | QObject::tr("Error"), 136 | QObject::tr("Failed to check for updates:\n\n%1").arg(updaterStatusMessages.str().c_str()) 137 | ); 138 | return 1; 139 | } 140 | } 141 | 142 | // clear existing status messages before performing the actual update 143 | updaterStatusMessages.clear(); 144 | 145 | bool removeAfterUpdate = false; 146 | 147 | { 148 | const auto message = QObject::tr("An update has been found for the AppImage %1").arg(pathToAppImage) + 149 | "\n\n" + 150 | QObject::tr("Do you want to perform the update?") + "\n"; 151 | 152 | QMessageBox messageBox(QMessageBox::Icon::Question, "Update found", message, QMessageBox::Ok | QMessageBox::Cancel); 153 | 154 | QCheckBox removeCheckBox(QObject::tr("Remove old AppImage after successful update")); 155 | removeCheckBox.setChecked(false); 156 | 157 | messageBox.setCheckBox(&removeCheckBox); 158 | 159 | switch (messageBox.exec()) { 160 | case QMessageBox::Ok: 161 | break; 162 | default: 163 | return 0; 164 | } 165 | 166 | removeAfterUpdate = removeCheckBox.isChecked(); 167 | } 168 | 169 | auto rv = updater.exec(); 170 | 171 | // if the update has failed, return immediately 172 | if (rv != 0) { 173 | QMessageBox::information( 174 | nullptr, 175 | QObject::tr("Error"), 176 | QObject::tr("Failed to update AppImage:\n\n%1").arg(updaterStatusMessages.str().c_str()) 177 | ); 178 | 179 | return rv; 180 | } 181 | 182 | // get path to new file, un-integrate old file, remove it, and register updated AppImage 183 | QString pathToUpdatedAppImage; 184 | 185 | if (!updater.pathToNewFile(pathToUpdatedAppImage)) 186 | return 1; 187 | 188 | // sanity check 189 | if (!QFile::exists(pathToUpdatedAppImage)) { 190 | criticalUpdaterError(QObject::tr("File reported as updated does not exist: %1").arg(pathToUpdatedAppImage)); 191 | return 1; 192 | } 193 | 194 | const auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage); 195 | 196 | if (!appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str())) { 197 | if (!integrateAppImage(pathToUpdatedAppImage, pathToIntegratedAppImage)) { 198 | criticalUpdaterError(QObject::tr("Failed to register updated AppImage in system")); 199 | return 1; 200 | } 201 | } 202 | 203 | // a crappy attempt to prevent deletion of the updated AppImage in a rare case (see below) 204 | const auto pathToIntegratedUpdatedAppImage = buildPathToIntegratedAppImage(pathToUpdatedAppImage); 205 | 206 | if (removeAfterUpdate) { 207 | // make sure not to delete the updated(!) AppImage if the filenames of the new and old file are equal 208 | // in this case, a warning is shown, asking the user whether to overwrite the old file, and in that case we 209 | // don't need to unregister nor delete the file 210 | if (pathToIntegratedAppImage != pathToIntegratedUpdatedAppImage) { 211 | if (hasBeenRegisteredBefore && appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false) != 0) { 212 | criticalUpdaterError(QObject::tr("Failed to unregister old AppImage in system")); 213 | return 1; 214 | } 215 | 216 | if (!QFile::remove(pathToAppImage)) { 217 | criticalUpdaterError(QObject::tr("Failed to remove old AppImage")); 218 | return 1; 219 | } 220 | } 221 | } 222 | 223 | // we're done! 224 | return 0; 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # AppImageLauncher 6 | 7 |

8 | 9 |

10 | 11 | AppImageLauncher makes your Linux desktop AppImage ready™. By installing it, you won't ever have to worry about AppImages again. You can always double click them without making them executable first, just like you should be able to do nowadays. You can integrate AppImages with a single mouse click, and manage them from your application launcher. Updating and removing AppImages becomes as easy as never before. 12 | 13 | Due to its simple but efficient way to integrate into your system, it plays well with other applications that can be used to manage AppImages, for example app stores. However, it doesn't depend on any of those, and can run completely standalone. 14 | 15 | Install AppImageLauncher today for your distribution and enjoy using AppImages as easy as never before! 16 | 17 | 18 | ## Features 19 | 20 | ### AppImage desktop integration 21 | 22 | The core feature of AppImageLauncher is the so-called desktop integration. AppImageLauncher allows you to integrate AppImages you download into your application menu or launcher to make it easier for you to launch them. It also takes care of moving them into a central location, where you can find them later if you need access to them again. Furthermore, it sets up the update and removal entries in the launcher for you. 23 | 24 | ### Update management 25 | 26 | AppImageLauncher provides a simple to use update mechanism. After desktop integration, the context menu of the AppImage's entry in the application launcher will have an entry for updating that launches a little helper tool that uses AppImageUpdate internally. Just click the entry and have the tool search and apply updates. 27 | 28 | ### Remove AppImages from system 29 | 30 | Removing integrated AppImages is pretty simple, too. Similar to updating AppImages, you will find an entry in the context menu in the application launcher that triggers a removal tool. You will be asked to confirm the removal. If you choose to do so, the desktop integration is undone, and the file is removed from your system. 31 | 32 | 33 | ## About the project 34 | 35 | AppImages and Linux desktops, that's two things which don't work together very well currently. Since AppImages are normal executables, it'd suffice if desktop environments like KDE, GNOME, Xfce, ... would assist users in making those files executable, but as we learned recently, some desktop environments consider this a security risk, and want to force users to use app stores. 36 | 37 | Being executable isn't really all that is needed to provide a good desktop experience. AppImages should be accessible from the application menus and launchers. This so-called "desktop integration" can't be provided by the AppImages themselves even though some AppImages ship with a "desktop integration script" prompting the user to integrate the AppImages, as there's too many impliciations that require an external software, especially regarding the cleanup and removal of AppImages. Also, if applications are simply made executable, they're still spread all over the users' personal files and folders. The average user doesn't necessarily like a Downloads directory that is full of AppImages with cryptic filenames. 38 | 39 | Therefore, new, system-side solutions have been developed to perform the desktop integration. The oldest available solution is [appimaged](https://github.com/AppImage/AppImageKit), a daemon users can install that performs everything in the background, automagically, without notifying the user in any way. It scans a predefined set of directories including `~/Downloads` and `~/.bin`, makes AppImages which are found executable and performs the desktop integration. This is rather inefficient, as appimaged's operations and monitoring produce a lot of file I/O. Also, many users don't like the lack of control. 40 | 41 | A new solution for native AppImage support has been developed: AppImageLauncher. AppImageLauncher integrates deeply in the system and intercepts all attempts to open an AppImage, becoming the first instance to handle all AppImage invocations. 42 | 43 | Being the launcher for AppImages, AppImageLauncher can control how the system treats AppImages. It can perform the desktop integration, AppImage removal (also called "uninstallation" sometimes, but as AppImages are not really installed, this term doesn't fit very well), and could be used for many other tasks in the future, like update checks and alike. 44 | 45 | 46 | ## Articles about AppImageLauncher 47 | 48 | A few articles have been written about AppImageLauncher already: 49 | 50 | - https://www.linuxuprising.com/2018/04/easily-run-and-integrate-appimage-files.html (English) 51 | - https://www.freeyourdesktop.com/2018/07/install-manage-appimages-with-appimagelauncher/ (English) 52 | - same article also available here: https://medium.com/@freeyourdesktopblog/install-manage-appimages-with-appimagelauncher-2a2078c55f37 53 | - http://linux-os.net/appimagelauncher-ejecuta-e-integra-facilmente-aplicaciones-en-appimage/ (Spanish) 54 | - same article also available here: https://blog.desdelinux.net/appimagelauncher-ejecuta-e-integra-facilmente-aplicaciones-en-appimage/ 55 | - http://www.edivaldobrito.com.br/integrador-appimagelauncher-no-linux/ (Portuguese) 56 | - https://404.g-net.pl/2018/08/appimagelauncher/ (Polish) 57 | - https://linuxmint.hu/blog/2018/12/appimage (Hungarian) 58 | - https://www.freeyourdesktop.com/2018/07/install-manage-appimages-with-appimagelauncher/ (English) 59 | - please note that AppImageLauncher could *not* be "installed" via AppImage at that time, only recently we added a Lite version that now can be installed from an AppImage (more information will follow soon!) 60 | 61 | 62 | ## Installation 63 | 64 | ### System wide Installation 65 | 66 | AppImageLauncher is supposed to integrate deeply in the systems. Therefore, an installation via native system packages is the preferred way to install AppImageLauncher. This way, AppImageLauncher's package can perform the necessary steps to have your system use it for all AppImage invocations. 67 | 68 | Compatibility table (likely incomplete, please feel free to send PRs to add distributions) 69 | 70 | | Release filename | Build system | Compatible distributions (incomplete) | 71 | | ---------------- | ------------ | ------------------------------------- | 72 | | `appimagelauncher-.xenial_(amd64,i386).deb` | Ubuntu xenial | Ubuntu xenial (16.04), Debian jessie (8, oldstable), Netrunner 17.01 | 73 | | `appimagelauncher-.bionic_(amd64,i386).deb` | Ubuntu bionic | Ubuntu bionic (18.04) and newer, Debian stretch (9, stable) and newer, Netrunner 18.03 and newer | 74 | | `appimagelauncher-.(i386,x86_64).rpm` | Ubuntu xenial | openSUSE Leap 42 and newer, possibly openSUSE Tumbleweed, SUSE Enterprise Linux, RHEL 7, CentOS 7 | 75 | 76 | - Ubuntu trusty (14.04) and newer 77 | - **Important:** Ubuntu bionic (and newer) broke with the backwards compatibility of its `libcurl` packages, therefore users of these systems need to install the special `bionic` package 78 | - Debian stable (jessie, 8) and newer 79 | - Netrunner 17 and newer 80 | - openSUSE Leap 42 and newer 81 | - openSUSE Tumbleweed 82 | 83 | The installation of packages on systems with a set of packages similar to one of the listed ones (e.g., Linux Mint, Fedora, etc.) should work as well. 84 | 85 | Manjaro and Netrunner Rolling users can install AppImageLauncher with a distribution-provided package called `appimagelauncher`. 86 | 87 | Arch Linux, Manjaro, Antergos and Netrunner Rolling users can use AUR to install AppImageLauncher by installing [appimagelauncher-git](https://aur.archlinux.org/packages/appimagelauncher-git) (thanks @NuLogicSystems for setting up the build). 88 | 89 | Other systems derived from the listed ones, such as for instance Linux Mint (Ubuntu), should support AppImageLauncher as well. If they don't, please don't hesitate to create an issue on GitHub. 90 | 91 | **Note:** If your system is not listed above as supported, please feel free to request support in an issue on GitHub. We can then discuss adding support. 92 | 93 | 94 | ## How it works 95 | 96 | AppImageLauncher is responsible for the desktop integration. When the user launches an AppImage, the software checks whether the AppImage has been integrated already. If not, it displays a dialog prompting the user whether to run the AppImage once, or move it to a predefined location and adding it to the application menus, launchers, etc. 97 | 98 | 99 | ## Technical background information 100 | 101 | Details about how AppImageLauncher registers itself in the system can be found on [this Wiki page](https://github.com/TheAssassin/AppImageLauncher/wiki/Idea). 102 | -------------------------------------------------------------------------------- /src/ui/main.cpp: -------------------------------------------------------------------------------- 1 | // system includes 2 | #include 3 | #include 4 | #include 5 | extern "C" { 6 | #include 7 | #include 8 | #include 9 | #include 10 | } 11 | 12 | // library includes 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | extern "C" { 28 | #include 29 | } 30 | 31 | // local headers 32 | #include "shared.h" 33 | #include "trashbin.h" 34 | #include "translationmanager.h" 35 | #include "first-run.h" 36 | 37 | // Runs an AppImage. Returns suitable exit code for main application. 38 | int runAppImage(const QString& pathToAppImage, unsigned long argc, char** argv) { 39 | // needs to be converted to std::string to be able to use c_str() 40 | // when using QString and then .toStdString().c_str(), the std::string instance will be an rvalue, and the 41 | // pointer returned by c_str() will be invalid 42 | auto x = pathToAppImage.toStdString(); 43 | auto fullPathToAppImage = QFileInfo(pathToAppImage).absoluteFilePath(); 44 | 45 | auto type = appimage_get_type(fullPathToAppImage.toStdString().c_str(), false); 46 | if (type < 1 || type > 3) { 47 | displayError(QObject::tr("AppImageLauncher does not support type %1 AppImages at the moment.").arg(type)); 48 | return 1; 49 | } 50 | 51 | // first of all, chmod +x the AppImage registerFile 52 | // not strictly necessary for AppImageLauncherFS, but if AppImageLauncher is going to be removed, the user will 53 | // be happy the registerFile is executable already 54 | if (!makeExecutable(fullPathToAppImage)) { 55 | displayError(QObject::tr("Could not make AppImage executable: %1").arg(fullPathToAppImage)); 56 | return 1; 57 | } 58 | 59 | // make sure to restart service in case AppImageLauncher has been updated 60 | // if (!fsDaemonHasBeenRestartedSinceLastUpdate()) { 61 | // auto rv = system("systemctl --user restart appimagelauncherfs 2>&1 1>/dev/null"); 62 | // if (rv != 0) { 63 | // displayError(QObject::tr("Failed to register AppImage in AppImageLauncherFS: error while trying to restart appimagelauncherfs.service after update")); 64 | // return 1; 65 | // } 66 | // } 67 | 68 | // make sure appimagelauncherfs service is running 69 | auto rv = system("systemctl --user enable appimagelauncherfs 2>&1 1>/dev/null"); 70 | rv += system("systemctl --user start appimagelauncherfs 2>&1 1>/dev/null"); 71 | 72 | if (rv != 0) { 73 | displayError(QObject::tr("Failed to register AppImage in AppImageLauncherFS: error while trying to start appimagelauncherfs.service")); 74 | return 1; 75 | } 76 | 77 | // suppress desktop integration script etc. 78 | setenv("DESKTOPINTEGRATION", "AppImageLauncher", true); 79 | 80 | // build path to AppImageLauncherFS endpoint 81 | QString pathToFSEndpoint = "/run/user/" + QString::number(getuid()) + "/appimagelauncherfs/"; 82 | 83 | // resolve path to virtual file in map 84 | QString pathToVirtualAppImage; 85 | 86 | bool registrationWorked = false, pathResolvingWorked = false, openingMapFileWorked = false; 87 | 88 | // try to fetch ID of registered AppImage up to 10 times, with increasing timeouts 89 | for (useconds_t i = 1; i < 10; i++) { 90 | usleep(i * 100000); 91 | 92 | // register current AppImage 93 | if (!registrationWorked){ 94 | QFile registerFile(pathToFSEndpoint + "/register"); 95 | 96 | if (registerFile.open(QIODevice::Append)) { 97 | QTextStream stream(®isterFile); 98 | stream << pathToAppImage << endl; 99 | } 100 | 101 | registrationWorked = true; 102 | 103 | registerFile.close(); 104 | } 105 | 106 | // reading line wise properly is [hard, impossible[ in Qt 107 | // using good ol' C++ 108 | std::string mapFilePath = (pathToFSEndpoint + "/map").toStdString(); 109 | 110 | std::ifstream mapFile(mapFilePath); 111 | 112 | if (!mapFile) { 113 | openingMapFileWorked = false; 114 | continue; 115 | } 116 | 117 | openingMapFileWorked = true; 118 | 119 | std::string currentLine; 120 | while (std::getline(mapFile, currentLine)) { 121 | if (currentLine.find(pathToAppImage.toStdString()) != std::string::npos) { 122 | auto virtualFileName = currentLine.substr(0, currentLine.find(" -> ")); 123 | pathToVirtualAppImage = pathToFSEndpoint + "/" + QString::fromStdString(virtualFileName); 124 | break; 125 | } 126 | } 127 | 128 | mapFile.close(); 129 | 130 | if (!pathToVirtualAppImage.isEmpty()) { 131 | pathResolvingWorked = true; 132 | break; 133 | } 134 | } 135 | 136 | // error message handling 137 | if (pathToVirtualAppImage.isEmpty()) { 138 | QString errorMessage; 139 | 140 | if (!registrationWorked) { 141 | errorMessage = QObject::tr("Failed to register AppImage in AppImageLauncherFS: failed to register AppImage path %1").arg(pathToAppImage); 142 | } else if (!openingMapFileWorked) { 143 | errorMessage = QObject::tr("Failed to register AppImage in AppImageLauncherFS: could not open map file"); 144 | } else if (!pathResolvingWorked) { 145 | errorMessage = QObject::tr("Failed to register AppImage in AppImageLauncherFS: could not find virtual file for AppImage"); 146 | } else { 147 | // this should _never_ be reachable 148 | errorMessage = QObject::tr("Failed to register AppImage in AppImageLauncherFS: unknown failure"); 149 | } 150 | 151 | displayError(errorMessage); 152 | 153 | return 1; 154 | } 155 | 156 | // need a char pointer instead of a const one, therefore can't use .c_str() 157 | std::vector argv0Buffer(pathToAppImage.toStdString().size() + 1, '\0'); 158 | strcpy(argv0Buffer.data(), pathToAppImage.toStdString().c_str()); 159 | 160 | std::vector args; 161 | 162 | args.push_back(argv0Buffer.data()); 163 | 164 | // copy arguments 165 | for (unsigned long i = 1; i < argc; i++) { 166 | args.push_back(argv[i]); 167 | } 168 | 169 | // args need to be null terminated 170 | args.push_back(nullptr); 171 | 172 | execv(pathToVirtualAppImage.toStdString().c_str(), args.data()); 173 | 174 | const auto& error = errno; 175 | std::cerr << QObject::tr("execv() failed: %1").arg(strerror(error)).toStdString() << std::endl; 176 | return 1; 177 | } 178 | 179 | // factory method to build and return a suitable Qt application instance 180 | // it remembers a previously created instance, and will return it if available 181 | // otherwise a new one is created and configure 182 | // caution: cannot use .exec() any more, instead call .show() and use QApplication::exec() 183 | QCoreApplication* getApp(char** argv) { 184 | if (QCoreApplication::instance() != nullptr) 185 | return QCoreApplication::instance(); 186 | 187 | // build application version string 188 | std::string version; 189 | { 190 | std::ostringstream oss; 191 | oss << "version " << APPIMAGELAUNCHER_VERSION << " " 192 | << "(git commit " << APPIMAGELAUNCHER_GIT_COMMIT << "), built on " 193 | << APPIMAGELAUNCHER_BUILD_DATE; 194 | version = oss.str(); 195 | } 196 | 197 | QCoreApplication* app; 198 | 199 | // need to pass rvalue, hence defining a variable 200 | int* fakeArgc = new int{1}; 201 | 202 | static char** fakeArgv = new char*{strdup(argv[0])}; 203 | 204 | if (isHeadless()) { 205 | app = new QCoreApplication(*fakeArgc, fakeArgv); 206 | } else { 207 | auto uiApp = new QApplication(*fakeArgc, fakeArgv); 208 | QApplication::setApplicationDisplayName("AppImageLauncher"); 209 | 210 | // this doesn't seem to have any effect... but it doesn't hurt either 211 | uiApp->setWindowIcon(QIcon(":/AppImageLauncher.svg")); 212 | 213 | app = uiApp; 214 | } 215 | 216 | QCoreApplication::setApplicationName("AppImageLauncher"); 217 | QCoreApplication::setApplicationVersion(QString::fromStdString(version)); 218 | 219 | return app; 220 | } 221 | 222 | int main(int argc, char** argv) { 223 | // create a suitable application object (either graphical (QApplication) or headless (QCoreApplication)) 224 | // Use a fake argc value to avoid QApplication from modifying the arguments 225 | QCoreApplication* app = getApp(argv); 226 | 227 | // install translations 228 | TranslationManager translationManager(*app); 229 | 230 | // clean up old desktop files 231 | if (!cleanUpOldDesktopIntegrationResources()) { 232 | displayError(QObject::tr("Failed to clean up old desktop files")); 233 | return 1; 234 | } 235 | 236 | // clean up trash directory 237 | { 238 | TrashBin bin; 239 | if (!bin.cleanUp()) { 240 | displayError(QObject::tr("Failed to clean up AppImage trash bin: %1").arg(bin.path())); 241 | } 242 | } 243 | 244 | std::ostringstream usage; 245 | usage << QObject::tr("Usage: %1 [options] ").arg(argv[0]).toStdString() << std::endl 246 | << QObject::tr("Desktop integration helper for AppImages, for use by Linux distributions.").toStdString() << std::endl 247 | << std::endl 248 | << QObject::tr("Options:").toStdString() << std::endl 249 | << " --appimagelauncher-help " << QObject::tr("Display this help and exit").toStdString() << std::endl 250 | << " --appimagelauncher-version " << QObject::tr("Display version and exit").toStdString() << std::endl 251 | << std::endl 252 | << QObject::tr("Arguments:").toStdString() << std::endl 253 | << " path " << QObject::tr("Path to AppImage (mandatory)").toStdString() << std::endl; 254 | 255 | auto displayVersion = [&app]() { 256 | std::cerr << "AppImageLauncher " << app->applicationVersion().toStdString() << std::endl; 257 | }; 258 | 259 | // display usage and exit if path to AppImage is missing 260 | if (argc <= 1) { 261 | displayVersion(); 262 | std::cerr << std::endl; 263 | std::cerr << usage.str(); 264 | return 1; 265 | } 266 | 267 | std::vector appImageArgv; 268 | // search for --appimagelauncher-* arguments in args list 269 | for (int i = 1; i < argc; i++) { 270 | QString arg = argv[i]; 271 | 272 | // reserved argument space 273 | const QString prefix = "--appimagelauncher-"; 274 | 275 | if (arg.startsWith(prefix)) { 276 | if (arg == prefix + "help") { 277 | displayVersion(); 278 | std::cerr << std::endl; 279 | std::cerr << usage.str(); 280 | return 0; 281 | } else if (arg == prefix + "version") { 282 | displayVersion(); 283 | return 0; 284 | } else if (arg == prefix + "cleanup") { 285 | // exit immediately after cleanup 286 | return 0; 287 | } else { 288 | std::cerr << QObject::tr("Unknown AppImageLauncher option: %1").arg(arg).toStdString() << std::endl; 289 | return 1; 290 | } 291 | } else { 292 | appImageArgv.emplace_back(argv[i]); 293 | } 294 | } 295 | 296 | // sanitize path 297 | auto pathToAppImage = QDir(QString(argv[1])).absolutePath(); 298 | 299 | if (!QFile(pathToAppImage).exists()) { 300 | displayError(QObject::tr("Error: no such file or directory: %1").arg(pathToAppImage)); 301 | return 1; 302 | } 303 | 304 | // if the users wishes to disable AppImageLauncher, we just run the AppImage as-ish 305 | if (getenv("APPIMAGELAUNCHER_DISABLE") != nullptr) { 306 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 307 | } 308 | 309 | const auto type = appimage_get_type(pathToAppImage.toStdString().c_str(), false); 310 | 311 | if (type <= 0 || type > 2) { 312 | displayError(QObject::tr("Not an AppImage: %1").arg(pathToAppImage)); 313 | return 1; 314 | } 315 | 316 | // type 2 specific checks 317 | if (type == 2) { 318 | // check parameters 319 | { 320 | for (int i = 0; i < argc; i++) { 321 | QString arg = argv[i]; 322 | 323 | // reserved argument space 324 | const QString prefix = "--appimage-"; 325 | 326 | if (arg.startsWith(prefix)) { 327 | // don't annoy users who try to mount or extract AppImages 328 | if (arg == prefix + "mount" || arg == prefix + "extract" || arg == prefix + "updateinformation") { 329 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 330 | } 331 | } 332 | } 333 | } 334 | } 335 | 336 | // enable and start/disable and stop appimagelauncherd service 337 | auto config = getConfig(); 338 | 339 | // assumes defaults if config doesn't exist or lacks the related key(s) 340 | if (config == nullptr || !config->contains("AppImageLauncher/enable_daemon") || config->value("AppImageLauncher/enable_daemon").toBool()) { 341 | system("systemctl --user enable appimagelauncherd.service"); 342 | system("systemctl --user start appimagelauncherd.service"); 343 | } else { 344 | system("systemctl --user disable appimagelauncherd.service"); 345 | system("systemctl --user stop appimagelauncherd.service"); 346 | } 347 | 348 | // beyond the next block, the code requires a UI 349 | // as we don't want to offer integration over a headless connection, we just run the AppImage 350 | if (isHeadless()) { 351 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 352 | } 353 | 354 | // if config doesn't exist, create a default one 355 | if (config == nullptr) { 356 | showFirstRunDialog(); 357 | config = getConfig(); 358 | } 359 | 360 | if (config == nullptr) { 361 | displayError("Could not read config file"); 362 | } 363 | 364 | // if the user opted out of the "ask move" thing, we can just run the AppImage 365 | if (config->contains("AppImageLauncher/ask_to_move") && !config->value("AppImageLauncher/ask_to_move").toBool()) { 366 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 367 | } 368 | 369 | // check for X-AppImage-Integrate=false 370 | auto shallNotBeIntegrated = appimage_shall_not_be_integrated(pathToAppImage.toStdString().c_str()); 371 | if (shallNotBeIntegrated < 0) 372 | std::cerr << "AppImageLauncher error: appimage_shall_not_be_integrated() failed (returned " << shallNotBeIntegrated << ")" << std::endl; 373 | else if (shallNotBeIntegrated > 0) 374 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 375 | 376 | // AppImages in AppImages are not supposed to be integrated 377 | if (pathToAppImage.startsWith("/tmp/.mount_")) 378 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 379 | 380 | // ignore terminal apps (fixes #2) 381 | auto isTerminalApp = appimage_is_terminal_app(pathToAppImage.toStdString().c_str()); 382 | if (isTerminalApp < 0) 383 | std::cerr << "AppImageLauncher error: appimage_is_terminal_app() failed (returned " << isTerminalApp << ")" << std::endl; 384 | else if (isTerminalApp > 0) 385 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 386 | 387 | // AppImages in AppImages are not supposed to be integrated 388 | if (pathToAppImage.startsWith("/tmp/.mount_")) 389 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 390 | 391 | const auto pathToIntegratedAppImage = buildPathToIntegratedAppImage(pathToAppImage); 392 | 393 | auto integrateAndRunAppImage = [&pathToAppImage, &pathToIntegratedAppImage, &appImageArgv]() { 394 | // check whether integration was successful 395 | auto rv = integrateAppImage(pathToAppImage, pathToIntegratedAppImage); 396 | 397 | // make sure the icons in the launcher are refreshed 398 | if (!updateDesktopDatabaseAndIconCaches()) 399 | return 1; 400 | 401 | if (rv == INTEGRATION_FAILED) { 402 | return 1; 403 | } else if (rv == INTEGRATION_ABORTED) { 404 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 405 | } else { 406 | return runAppImage(pathToIntegratedAppImage, appImageArgv.size(), appImageArgv.data()); 407 | } 408 | }; 409 | 410 | // after checking whether the AppImage can/must be run without integrating it, we now check whether it actually 411 | // has been integrated already 412 | if (hasAlreadyBeenIntegrated(pathToAppImage)) { 413 | auto updateAndRunAppImage = [&pathToAppImage, &appImageArgv]() { 414 | // in case there was an update of AppImageLauncher, we should should also update the desktop database 415 | // and icon caches 416 | if (!desktopFileHasBeenUpdatedSinceLastUpdate(pathToAppImage)) { 417 | if (!updateDesktopFileAndIcons(pathToAppImage)) 418 | return 1; 419 | 420 | // make sure the icons in the launcher are refreshed after updating the desktop file 421 | if (!updateDesktopDatabaseAndIconCaches()) 422 | return 1; 423 | } 424 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 425 | }; 426 | 427 | if (!isInDirectory(pathToAppImage, integratedAppImagesDestination().path())) { 428 | auto* messageBox = new QMessageBox( 429 | QMessageBox::Warning, 430 | QMessageBox::tr("Warning"), 431 | QMessageBox::tr("AppImage %1 has already been integrated, but it is not in the current integration " 432 | "destination directory." 433 | "\n\n" 434 | "Do you want to move it into the new destination?" 435 | "\n\n" 436 | "Choosing No will run the AppImage once, and leave the AppImage in its current " 437 | "directory." 438 | "\n\n").arg(pathToAppImage) + 439 | // translate separately to share string with the other dialog 440 | QObject::tr("The directory the integrated AppImages are stored in is currently set to:\n" 441 | "%1").arg(integratedAppImagesDestination().path()) + "\n", 442 | QMessageBox::Yes | QMessageBox::No 443 | ); 444 | 445 | messageBox->setDefaultButton(QMessageBox::Yes); 446 | messageBox->show(); 447 | 448 | QApplication::exec(); 449 | 450 | // if the user selects No, then continue as if the AppImage would not be in this directory 451 | if (messageBox->clickedButton() == messageBox->button(QMessageBox::Yes)) { 452 | // unregister AppImage, move, and re-integrate 453 | if (appimage_unregister_in_system(pathToAppImage.toStdString().c_str(), false) != 0) { 454 | displayError(QMessageBox::tr("Failed to unregister AppImage before re-integrating it")); 455 | return 1; 456 | } 457 | 458 | return integrateAndRunAppImage(); 459 | } else { 460 | return updateAndRunAppImage(); 461 | } 462 | } else { 463 | return updateAndRunAppImage(); 464 | } 465 | } 466 | 467 | std::ostringstream explanationStrm; 468 | explanationStrm << QObject::tr("Integrating it will move the AppImage into a predefined location, " 469 | "and include it in your application launcher.").toStdString() << std::endl 470 | << std::endl 471 | << QObject::tr("To remove or update the AppImage, please use the context menu of the " 472 | "application icon in your task bar or launcher.").toStdString() << std::endl 473 | << std::endl 474 | << QObject::tr("The directory the integrated AppImages are stored in is currently " 475 | "set to:").toStdString() << std::endl 476 | << integratedAppImagesDestination().path().toStdString() << std::endl; 477 | 478 | auto explanation = explanationStrm.str(); 479 | 480 | std::ostringstream messageStrm; 481 | messageStrm << QObject::tr("%1 has not been integrated into your system.").arg(pathToAppImage).toStdString() << "\n\n" 482 | << QObject::tr(explanation.c_str()).toStdString(); 483 | 484 | auto* messageBox = new QMessageBox( 485 | QMessageBox::Question, 486 | QObject::tr("Desktop Integration"), 487 | QString::fromStdString(messageStrm.str()) 488 | ); 489 | 490 | auto* okButton = messageBox->addButton(QObject::tr("Integrate and run"), QMessageBox::AcceptRole); 491 | auto* runOnceButton = messageBox->addButton(QObject::tr("Run once"), QMessageBox::ApplyRole); 492 | 493 | // *whyever* Qt somehow needs a button with "RejectRole" or the X button won't close the current window... 494 | auto* cancelButton = messageBox->addButton(QObject::tr("Cancel"), QMessageBox::RejectRole); 495 | // ... but it is fine to hide that button after creating it, so it's not displayed 496 | cancelButton->hide(); 497 | 498 | messageBox->setDefaultButton(QMessageBox::Ok); 499 | 500 | // cannot use messageBox.exec(), will produce SEGFAULTS as QCoreApplications can't show message boxes 501 | messageBox->show(); 502 | 503 | // don't need to cast around, exec() is a static method anyway, and QApplication is a singleton 504 | QApplication::exec(); 505 | 506 | const auto* clickedButton = messageBox->clickedButton(); 507 | 508 | if (clickedButton == okButton) { 509 | return integrateAndRunAppImage(); 510 | } else if (clickedButton == runOnceButton) { 511 | return runAppImage(pathToAppImage, appImageArgv.size(), appImageArgv.data()); 512 | } else if (clickedButton == cancelButton) { 513 | return 0; 514 | } 515 | 516 | // _should_ be unreachable 517 | return 1; 518 | } 519 | 520 | --------------------------------------------------------------------------------