diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc0eb95fd6d..8be8fbbdc8a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,11 +20,6 @@ jobs: steps: - name: Configure git run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - name: Update cmake - run: | - apt install snap - snap install cmake --classic - alias cmake='snap run cmake' - name: Check out uses: actions/checkout@v3 with: diff --git a/cmake/linux/CMakeLists.txt b/cmake/linux/CMakeLists.txt index c7b35fae85d..e64b4eb67cc 100644 --- a/cmake/linux/CMakeLists.txt +++ b/cmake/linux/CMakeLists.txt @@ -38,13 +38,33 @@ set(CPACK_CMAKE_COMMAND "${CMAKE_COMMAND}" PARENT_SCOPE) # TODO: Canidate for DetectMachine.cmake if(IS_X86_64) set(CPACK_TARGET_ARCH x86_64 PARENT_SCOPE) +elseif(IS_X86) + set(CPACK_TARGET_ARCH i386 PARENT_SCOPE) elseif(IS_ARM64) - set(CPACK_TARGET_ARCH arm64 PARENT_SCOPE) + set(CPACK_TARGET_ARCH aarch64 PARENT_SCOPE) +elseif(IS_ARM32) + set(CPACK_TARGET_ARCH armhf PARENT_SCOPE) else() set(CPACK_TARGET_ARCH unknown PARENT_SCOPE) endif() -set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/LinuxDeployQt.cmake" PARENT_SCOPE) +# Build tool "linuxdeployqt" is VERY difficult to work with due to Ubuntu 20.04 requirement +# - See myriad of complaints and issues here: https://github.com/probonopd/linuxdeployqt/issues/340 +# - Build tool "linuxdeploy" (without the "qt") does NOT have these limitations +# +# If viable: +# - The below "WANT_LINUXDEPLOY_QT" can be removed, we move away from "linuxdeployqt" forever +# +# If not viable: +# - We can keep them both (yuck) and document the toggle/flag in root /CMakeLists.txt +# +# TODO: Remove this check; Default to LinuxDeploy; Remove LinuxDeployQt.cmake +# +if(WANT_LINUXDEPLOY_QT) + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/LinuxDeployQt.cmake" PARENT_SCOPE) +else() + set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/LinuxDeploy.cmake" PARENT_SCOPE) +endif() if(CMAKE_VERSION VERSION_LESS "3.19") message(WARNING "DMG creation requires at least CMake 3.19") diff --git a/cmake/linux/LinuxDeploy.cmake b/cmake/linux/LinuxDeploy.cmake new file mode 100644 index 00000000000..1d8a4db9b33 --- /dev/null +++ b/cmake/linux/LinuxDeploy.cmake @@ -0,0 +1,204 @@ +# Variables must be prefixed with "CPACK_" to be visible here +set(lmms "${CPACK_PROJECT_NAME}") +set(LMMS "${CPACK_PROJECT_NAME_UCASE}") +set(ARCH "${CPACK_TARGET_ARCH}") +set(APP "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/${LMMS}.AppDir") + +# Target AppImage file +set(APPIMAGE_FILE "${CPACK_BINARY_DIR}/${CPACK_PACKAGE_FILE_NAME}.AppImage") +set(APPIMAGE_BEFORE_RENAME "${CPACK_BINARY_DIR}/${LMMS}-${ARCH}.AppImage") + +set(DESKTOP_FILE "${APP}/usr/share/applications/${lmms}.desktop") + +# 0 = no output, 1 = error/warning, 2 = normal, 3 = debug +set(VERBOSITY 1) +# Set to "STDOUT" to show all verbose commands +set(COMMAND_ECHO NONE) + +if(CPACK_DEBUG) + set(VERBOSITY 2) + set(COMMAND_ECHO STDOUT) +endif() + +include("${CPACK_SOURCE_DIR}/cmake/modules/DownloadBinary.cmake") +include("${CPACK_SOURCE_DIR}/cmake/modules/CreateSymlink.cmake") + +# Cleanup CPack "External" json files, old AppImage files +file(GLOB cleanup "${CPACK_BINARY_DIR}/lmms-*.json" + "${CPACK_BINARY_DIR}/${LMMS}-*.AppImage" + "${CPACK_BINARY_DIR}/${CPACK_PROJECT_NAME_UCASE}-*.AppImage" + "${CPACK_BINARY_DIR}/install_manifest.txt") +file(REMOVE ${cleanup}) + +# Download linuxdeploy +download_binary(LINUXDEPLOY_BIN + "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage" + linuxdeploy-${ARCH}.AppImage + false) + +# Download linuxdeploy-plugin-qt +download_binary(LINUXDEPLOY_PLUGIN_BIN + "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-${ARCH}.AppImage" + linuxdeploy-plugin-qt-${ARCH}.AppImage + false) + +message(STATUS "Creating AppDir ${APP}...") + +file(REMOVE_RECURSE "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/include") +file(MAKE_DIRECTORY "${APP}/usr") + +# Setup AppDir structure (/usr/bin, /usr/lib, /usr/share... etc) +file(GLOB files "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/*") +foreach(_file ${files}) + get_filename_component(_filename "${_file}" NAME) + if(NOT _filename MATCHES ".AppDir") + file(RENAME "${_file}" "${APP}/usr/${_filename}") + endif() +endforeach() + +# Ensure project's "qmake" executable is first on the PATH +get_filename_component(QTBIN "${CPACK_QMAKE_EXECUTABLE}" DIRECTORY) +set(ENV{PATH} "${QTBIN}:$ENV{PATH}") + +# Ensure "linuxdeploy.AppImage" binary is first on the PATH +set(ENV{PATH} "${CPACK_CURRENT_BINARY_DIR}:$ENV{PATH}") + +# Symlink executables so linuxdeployqt can find them +set(BIN_ZYN "${APP}/usr/bin/RemoteZynAddSubFx") +set(BIN_VST32 "${APP}/usr/bin/RemoteVstPlugin32.exe.so") +set(BIN_VST64 "${APP}/usr/bin/RemoteVstPlugin64.exe.so") + +create_symlink("${APP}/usr/lib/${lmms}/RemoteZynAddSubFx" "${BIN_ZYN}") +create_symlink("${APP}/usr/lib/${lmms}/32/RemoteVstPlugin32.exe.so" "${BIN_VST32}") +create_symlink("${APP}/usr/lib/${lmms}/RemoteVstPlugin64.exe.so" "${BIN_VST64}") + +# Deliberatly clobber LD_LIBRARY_PATH per https://github.com/probonopd/linuxdeployqt/issues/129 +set(ENV{LD_LIBRARY_PATH} "${APP}/usr/lib/${lmms}/:${APP}/usr/lib/${lmms}/optional") + +# Handle wine linking +if(IS_DIRECTORY "${CPACK_WINE_32_LIBRARY_DIR}") + execute_process(COMMAND ldd "${BIN_VST32}" + OUTPUT_VARIABLE LDD_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ECHO ${COMMAND_ECHO} + COMMAND_ERROR_IS_FATAL ANY) + string(replace "\n" ";" LDD_LIST "${LDD_OUTPUT}") + foreach(line ${LDD_LIST}) + if(line MATCHES "libwine.so" AND line MATCHES "not found") + set(ENV{LD_LIBRARY_PATH} "$ENV{LD_LIBRARY_PATH}:${CPACK_WINE_32_LIBRARY_DIR}") + continue() + endif() + endforeach() +endif() +if(IS_DIRECTORY "${CPACK_WINE_64_LIBRARY_DIR}") + execute_process(COMMAND ldd "${BIN_VST64}" + OUTPUT_VARIABLE LDD_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ECHO ${COMMAND_ECHO} + COMMAND_ERROR_IS_FATAL ANY) + string(replace "\n" ";" LDD_LIST "${LDD_OUTPUT}") + foreach(line ${LDD_LIST}) + if(line MATCHES "libwine.so" AND line MATCHES "not found") + set(ENV{LD_LIBRARY_PATH} "$ENV{LD_LIBRARY_PATH}:${CPACK_WINE_64_LIBRARY_DIR}") + continue() + endif() + endforeach() +endif() + +# Patch desktop file +file(READ "${DESKTOP_FILE}" DESKTOP_FILE_CONTENTS) +#string(REPLACE "Exec=${lmms}" "Exec=${lmms}.real" DESKTOP_FILE_CONTENTS "${DESKTOP_FILE_CONTENTS}") +file(WRITE "${DESKTOP_FILE}" "${DESKTOP_FILE_CONTENTS}") +file(APPEND "${DESKTOP_FILE}" "X-AppImage-Version=${CPACK_PROJECT_VERSION}\n") + +# TODO: Fix launch_lmms.sh to point directly to /usr/bin/lmms +# linuxdeploy supports wrappers natively; linuxdeployqt didn't; keep until linuxdeployqt is removed +create_symlink("${APP}/usr/bin/${lmms}" "${APP}/usr/bin/${lmms}.real") + +# Build list of executables to inform linuxdeploy about +# e.g. -executable=foo.dylib -executable=bar.dylib +file(GLOB LIBS "${APP}/usr/lib/${lmms}/*.so") +#file(GLOB LADSPA "${APP}/usr/lib/${lmms}/ladspa/*.so") +# TODO: Both Linux and Mac have LADPSA plugins in this listing, but why? +list(APPEND LIBS "${APP}/usr/lib/lmms/ladspa/imp_1199.so") +list(APPEND LIBS "${APP}/usr/lib/lmms/ladspa/imbeq_1197.so") +list(APPEND LIBS "${APP}/usr/lib/lmms/ladspa/pitch_scale_1193.so") +list(APPEND LIBS "${APP}/usr/lib/lmms/ladspa/pitch_scale_1194.so") +list(APPEND LIBS ${LADSPA}) +list(APPEND LIBS "${BIN_ZYN}") +list(APPEND LIBS "${BIN_VST32}") +list(APPEND LIBS "${BIN_VST64}") +list(SORT LIBS) + +# Construct linuxdeploy parameters +foreach(_LIB IN LISTS LIBS) + list(APPEND EXECUTABLES "-executable=${_LIB}") +endforeach() + +# Call linuxdeploy +message(STATUS "Calling ${LINUXDEPLOY_BIN} ${DESKTOP_FILE} [... executables]") +execute_process(COMMAND "${LINUXDEPLOY_BIN}" + --appdir "${APP}" + --icon-file "${CPACK_SOURCE_DIR}/cmake/linux/icons/scalable/apps/${lmms}.svg" + --desktop-file "${APP}/usr/share/applications/${lmms}.desktop" + --custom-apprun "${CPACK_SOURCE_DIR}/cmake/linux/launch_lmms.sh" + --plugin qt + --verbosity ${VERBOSITY} + COMMAND_ECHO ${COMMAND_ECHO} + COMMAND_ERROR_IS_FATAL ANY) + +# Remove libraries that are normally sytem-provided +file(GLOB UNWANTED_LIBS + "${APP}/usr/lib/libwine*" + "${APP}/usr/lib/libcarla*" + "${APP}/usr/lib/optional/libcarla*" + "${APP}/usr/lib/libjack*") + +foreach(_LIB UNWANTED_LIBS) + file(REMOVE "${_LIB}") +endforeach() + +# Bundle jack to avoid crash for systems without it +# See https://github.com/LMMS/lmms/pull/4186 +execute_process(COMMAND ldd "${APP}/usr/bin/${lmms}" + OUTPUT_VARIABLE LDD_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ECHO ${COMMAND_ECHO} + COMMAND_ERROR_IS_FATAL ANY) +string(REPLACE "\n" ";" LDD_LIST "${LDD_OUTPUT}") +foreach(line ${LDD_LIST}) + if(line MATCHES "libjack\\.so") + # Assume format "libjack.so.0 => /lib/x86_64-linux-gnu/libjack.so.0 (0x00007f48d0b0e000)" + string(REPLACE " " ";" parts "${line}") + list(LENGTH parts len) + math(EXPR index "${len}-2") + list(GET parts ${index} lib) + # Get symlink target + file(REAL_PATH "${lib}" libreal) + get_filename_component(symname "${lib}" NAME) + get_filename_component(realname "${libreal}" NAME) + file(MAKE_DIRECTORY "${APP}/usr/lib/lmms/optional/") + # Copy, but with original symlink name + file(COPY "${libreal}" DESTINATION "${APP}/usr/lib/lmms/optional/") + file(RENAME "${APP}/usr/lib/lmms/optional/${realname}" "${APP}/usr/lib/lmms/optional/${symname}") + continue() + endif() +endforeach() + +# Create AppImage +message(STATUS "Finishing the AppImage...") +execute_process(COMMAND "${LINUXDEPLOY_BIN}" + --appdir "${APP}" + --output appimage + --verbosity ${VERBOSITY} + COMMAND_ECHO ${COMMAND_ECHO} + COMMAND_ERROR_IS_FATAL ANY) + +message(STATUS "AppImage created successfully... renaming...") + +if(EXISTS "${APPIMAGE_BEFORE_RENAME}") + file(RENAME "${APPIMAGE_BEFORE_RENAME}" "${APPIMAGE_FILE}") + message(STATUS "AppImage create: ${APPIMAGE_FILE}") +else() + message(FATAL_ERROR "An error occured generating the AppImage") +endif() \ No newline at end of file diff --git a/cmake/linux/LinuxDeployQt.cmake b/cmake/linux/LinuxDeployQt.cmake index 9bec650c022..2e50e9945bb 100644 --- a/cmake/linux/LinuxDeployQt.cmake +++ b/cmake/linux/LinuxDeployQt.cmake @@ -19,6 +19,8 @@ if(CPACK_DEBUG) set(COMMAND_ECHO STDOUT) endif() +include("${CPACK_SOURCE_DIR}/cmake/modules/CreateSymlink.cmake") + set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/") string(APPEND LINUXDEPLOYQT_URL "linuxdeployqt-continuous-${CPACK_TARGET_ARCH}.AppImage") set(DESKTOP_FILE "${APP}/usr/share/applications/${CPACK_PROJECT_NAME}.desktop") @@ -26,11 +28,6 @@ set(LMMS "${CPACK_PROJECT_NAME}") message(STATUS "Creating AppDir ${APP}...") -# Offer symlink support via "cmake -E create_symlink" -macro(install_symlink filepath sympath) - execute_process(COMMAND ${CPACK_CMAKE_COMMAND} -E create_symlink "${filepath}" "${sympath}" COMMAND_ECHO ${COMMAND_ECHO}) -endmacro() - file(REMOVE_RECURSE "${CPACK_TEMPORARY_INSTALL_DIRECTORY}/include") file(MAKE_DIRECTORY "${APP}/usr") @@ -100,9 +97,9 @@ set(BIN_ZYN "${APP}/usr/bin/RemoteZynAddSubFx") set(BIN_VST32 "${APP}/usr/bin/RemoteVstPlugin32.exe.so") set(BIN_VST64 "${APP}/usr/bin/RemoteVstPlugin64.exe.so") -install_symlink("${APP}/usr/lib/${LMMS}/RemoteZynAddSubFx" "${BIN_ZYN}") -install_symlink("${APP}/usr/lib/${LMMS}/32/RemoteVstPlugin32.exe.so" "${BIN_VST32}") -install_symlink("${APP}/usr/lib/${LMMS}/RemoteVstPlugin64.exe.so" "${BIN_VST64}") +create_symlink("${APP}/usr/lib/${LMMS}/RemoteZynAddSubFx" "${BIN_ZYN}") +create_symlink("${APP}/usr/lib/${LMMS}/32/RemoteVstPlugin32.exe.so" "${BIN_VST32}") +create_symlink("${APP}/usr/lib/${LMMS}/RemoteVstPlugin64.exe.so" "${BIN_VST64}") # Deliberatly clobber LD_LIBRARY_PATH per https://github.com/probonopd/linuxdeployqt/issues/129 set(ENV{LD_LIBRARY_PATH} "${APP}/usr/lib/${LMMS}/:${APP}/usr/lib/${LMMS}/optional") @@ -212,10 +209,10 @@ endforeach() # Point the AppRun to the wrapper script file(REMOVE "${APP}/AppRun") -install_symlink("${APP}/usr/bin/${LMMS}" "${APP}/AppRun") +create_symlink("${APP}/usr/bin/${LMMS}" "${APP}/AppRun") # Add AppDir icon -install_symlink("${APP}/${LMMS}.png" "${APP}/.DirIcon") +create_symlink("${APP}/${LMMS}.png" "${APP}/.DirIcon") # Create AppImage message(STATUS "Finishing the AppImage...") diff --git a/cmake/modules/CreateSymlink.cmake b/cmake/modules/CreateSymlink.cmake new file mode 100644 index 00000000000..c9d6e818db0 --- /dev/null +++ b/cmake/modules/CreateSymlink.cmake @@ -0,0 +1,10 @@ +# Offer symlink support via "cmake -E create_symlink" +# For verbose, set ${COMMAND_ECHO} to STDOUT in calling script +macro(create_symlink filepath sympath) + if(NOT DEFINED COMMAND_ECHO) + set(_COMMAND_ECHO STDOUT) + else() + set(_COMMAND_ECHO "${COMMAND_ECHO}") + endif() + execute_process(COMMAND ${CPACK_CMAKE_COMMAND} -E create_symlink "${filepath}" "${sympath}" COMMAND_ECHO ${_COMMAND_ECHO}) +endmacro() \ No newline at end of file diff --git a/cmake/modules/DownloadBinary.cmake b/cmake/modules/DownloadBinary.cmake new file mode 100644 index 00000000000..3a6ff2282c8 --- /dev/null +++ b/cmake/modules/DownloadBinary.cmake @@ -0,0 +1,109 @@ +# Downloads an executable from the provided URL for use in a build system +# and adds it to the PATH +# +# Assumes: +# - ${CMAKE_CURRENT_BINARY_DIR}/[${_name}] +# - ${CPACK_CURRENT_BINARY_DIR}/[${_name}] +# - Fallback to $ENV{_tmpdir}/[RANDOM]/[${_name}] +# - For verbose, set ${COMMAND_ECHO} to STDOUT in calling script +# +# Use with caution, this will impact the PATH +macro(download_binary RESULT_VARIABLE _url _name _append_to_path) + if(NOT COMMAND_ECHO) + set(_command_echo STDOUT) + set(_output_quiet OUTPUT_QUIET) + else() + set(_command_echo "${COMMAND_ECHO}") + set(_output_quiet "") + set(_error_quiet "ERROR_QUIET") + endif() + + # Determine a suitable working directory + if(CMAKE_CURRENT_BINARY_DIR) + # Assume we're called from configure step + set(_working_dir "${CMAKE_CURRENT_BINARY_DIR}") + elseif(CPACK_CURRENT_BINARY_DIR) + # Assume cpack (non-standard variable name, but used throughout) + set(_working_dir "${CPACK_CURRENT_BINARY_DIR}") + else() + # Fallback to somewhere temporary, writable + if($ENV{_tmpdir}) + # POSIX + set(_tmpdir "$ENV{_tmpdir}") + elseif($ENV{TEMP}) + # Windows + set(_tmpdir "$ENV{TEMP}") + else() + # Linux, shame on you! + find_program(MKTEMP mktemp) + if(MKTEMP) + execute_process(COMMAND mktemp + OUTPUT_VARIABLE _working_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + ${_output_quiet} + COMMAND_ECHO ${_command_echo}) + # mktemp is already formatted how we want it + else() + # Ummm... Linux you can do better! + set(_tmpdir "/tmp") + endif() + endif() + if(NOT DEFINED _working_dir) + string(RANDOM subdir) + set(_working_dir "${_tmpdir}/tmp.${subdir}") + endif() + if(NOT EXISTS "${_working_dir}") + file(MAKE_DIRECTORY "${_working_dir}") + endif() + endif() + + if(_append_to_path) + # Ensure the PATH is configured + string(FIND "$ENV{PATH}" "${_working_dir}" _pathloc) + if(NOT $_pathloc EQUAL 0) + set(ENV{PATH} "${_working_dir}:$ENV{PATH}") + endif() + endif() + + # First ensure the binary doesn't already exist + find_program(_${RESULT_VARIABLE} "${_name}" HINTS "${_working_dir}") + + set(_binary_path "${_working_dir}/${_name}") + if(NOT _${RESULT_VARIABLE}) + message(STATUS "Downloading ${_name}...") + file(DOWNLOAD + "${_url}" + "${_binary_path}" + STATUS DOWNLOAD_STATUS) + # Check if download was successful. + list(GET DOWNLOAD_STATUS 0 STATUS_CODE) + list(GET DOWNLOAD_STATUS 1 ERROR_MESSAGE) + if(NOT ${STATUS_CODE} EQUAL 0) + file(REMOVE "${_binary_path}") + message(FATAL_ERROR "Error downloading ${_url} ${ERROR_MESSAGE}") + endif() + + # Ensure the file is executable + file(CHMOD "${_binary_path}" PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ + GROUP_EXECUTE GROUP_WRITE GROUP_READ) + + # Ensure it's found + find_program(_${RESULT_VARIABLE} "${_name}" HINTS "${_working_dir}" REQUIRED) + endif() + + # TODO: + # - Handle bad binaries that set "$?" to an error code for no good reason + # - Handle binaries on Windows expecting "/?" instead of "--help" + message(STATUS "Testing that ${_name} works on this system...") + set(_test_param "--help") + + execute_process(COMMAND "${_${RESULT_VARIABLE}}" ${_test_param} + COMMAND_ECHO ${_command_echo} + ${_output_quiet} + ${_error_quiet} + COMMAND_ERROR_IS_FATAL ANY) + + message(STATUS "The binary \"${_${RESULT_VARIABLE}}\" is now available") + set(${RESULT_VARIABLE} "${_${RESULT_VARIABLE}}") +endmacro() \ No newline at end of file