diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index 3cc49fa02..e87d79665 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -58,9 +58,7 @@ runs: GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - - cp ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.{metainfo,appdata}.xml + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }} if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then echo "$GPG_PRIVATE_KEY" > privkey.asc @@ -70,11 +68,23 @@ runs: echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY fi - appimagetool -s deploy "$INSTALL_APPIMAGE_DIR"/usr/share/applications/*.desktop - cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/usr/bin/ - # FIXME(@getchoo): Validate AppStream information when https://github.com/probonopd/go-appimage/pull/379 is merged + sharun lib4bin \ + --hard-links \ + --with-hooks \ + --dst-dir "$INSTALL_APPIMAGE_DIR" \ + "$INSTALL_APPIMAGE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so + + cp ~/bin/AppImageUpdate.AppImage "$INSTALL_APPIMAGE_DIR"/bin/ + # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! + find "$INSTALL_APPIMAGE_DIR" -name '*gamemode*' -exec rm {} + + + ln -s org.prismlauncher.PrismLauncher.metainfo.xml "$INSTALL_APPIMAGE_DIR"/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml + ln -s share/applications/org.prismlauncher.PrismLauncher.desktop "$INSTALL_APPIMAGE_DIR" + ln -s share/icons/hicolor/256x256/apps/org.prismlauncher.PrismLauncher.png "$INSTALL_APPIMAGE_DIR" + mv "$INSTALL_APPIMAGE_DIR"/{sharun,AppRun} + ls -la "$INSTALL_APPIMAGE_DIR" + mkappimage \ - --no-appstream \ --updateinformation "gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" \ "$INSTALL_APPIMAGE_DIR" \ "PrismLauncher-Linux-$VERSION-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" @@ -89,9 +99,14 @@ runs: cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - #the linked cmark .so is of the version that ubuntu uses, so without this it breaks on most updated distros - mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/$APPIMAGE_ARCH-linux-gnu/libcmark.so.0.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + sharun lib4bin \ + --with-hooks \ + --hard-links \ + --dst-dir "$INSTALL_PORTABLE_DIR" \ + "$INSTALL_PORTABLE_DIR"/bin/* "$QT_PLUGIN_PATH"/*/*.so + + # FIXME(@getchoo): gamemode doesn't seem to be very portable with DBus. Find a way to make it work! + find "$INSTALL_PORTABLE_DIR" -name '*gamemode*' -exec rm {} + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt cd ${{ env.INSTALL_PORTABLE_DIR }} diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml index f49f6b9c9..bd402328c 100644 --- a/.github/actions/package/windows/action.yml +++ b/.github/actions/package/windows/action.yml @@ -54,13 +54,13 @@ runs: Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - name: Emit warning for unsigned builds - if: ${{ github.ref_name != 'develop' || inputs.azure-client-id == '' }} + if: ${{ env.CI_HAS_ACCESS_TO_AZURE == '' || inputs.azure-client-id == '' }} shell: pwsh run: | ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - name: Login to Azure - if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }} + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/login@v2 with: client-id: ${{ inputs.azure-client-id }} @@ -68,7 +68,7 @@ runs: subscription-id: ${{ inputs.azure-subscription-id }} - name: Sign executables - if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }} + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/trusted-signing-action@v0 with: endpoint: https://eus.codesigning.azure.net/ @@ -140,7 +140,7 @@ runs: makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - name: Sign installer - if: ${{ github.ref_name == 'develop' && inputs.azure-client-id != '' }} + if: ${{ env.CI_HAS_ACCESS_TO_AZURE != '' && inputs.azure-client-id != '' }} uses: azure/trusted-signing-action@v0 with: endpoint: https://eus.codesigning.azure.net/ diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 4c19474b7..a8ecba583 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -21,7 +21,6 @@ inputs: qt-version: description: Version of Qt to use required: true - default: 6.9.3 outputs: build-type: @@ -78,6 +77,5 @@ runs: with: aqtversion: "==3.1.*" version: ${{ inputs.qt-version }} - arch: ${{ inputs.qt-architecture }} modules: qtimageformats qtnetworkauth cache: ${{ inputs.build-type == 'Debug' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 67a1a9826..753ea19fe 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -12,29 +12,7 @@ runs: dpkg-dev \ ninja-build extra-cmake-modules pkg-config scdoc \ cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \ - libxcb-cursor-dev - - # TODO(@getchoo): Install with the above when all targets use Ubuntu 24.04 - - name: Install tomlplusplus - if: ${{ runner.arch == 'ARM64' }} - shell: bash - run: | - sudo apt-get -y install libtomlplusplus-dev - - # FIXME(@getchoo): THIS IS HORRIBLE TO DO! - # Install tomlplusplus from Ubuntu 24.04, since it never got backported to 22.04 - # I've done too much to continue keeping this as a submodule.... - - name: Install tomlplusplus from 24.04 - if: ${{ runner.arch != 'ARM64' }} - shell: bash - run: | - deb_arch="$(dpkg-architecture -q DEB_HOST_ARCH)" - curl -Lo libtomlplusplus-dev.deb http://mirrors.kernel.org/ubuntu/pool/universe/t/tomlplusplus/libtomlplusplus-dev_3.4.0+ds-0.2build1_"$deb_arch".deb - curl -Lo libtomlplusplus3t64.deb http://mirrors.kernel.org/ubuntu/pool/universe/t/tomlplusplus/libtomlplusplus3t64_3.4.0+ds-0.2build1_"$deb_arch".deb - sudo dpkg -i libtomlplusplus3t64.deb - sudo dpkg -i libtomlplusplus-dev.deb - rm *.deb - sudo apt-get install -f + libxcb-cursor-dev libtomlplusplus-dev - name: Setup AppImage tooling shell: bash @@ -56,19 +34,20 @@ runs: ;; esac + gh release download \ + --repo VHSgunzo/sharun \ + --pattern "sharun-$APPIMAGE_ARCH-aio" \ + --output ~/bin/sharun + + # FIXME!: revert this to probonopd/go-appimage once https://github.com/probonopd/go-appimage/pull/377 is merged! gh release download continuous \ - --repo probonopd/go-appimage \ - --pattern "appimagetool-*-$APPIMAGE_ARCH.AppImage" \ - --output ~/bin/appimagetool - gh release download continuous \ - --repo probonopd/go-appimage \ + --repo DioEgizio/go-appimage \ --pattern "mkappimage-*-$APPIMAGE_ARCH.AppImage" \ --output ~/bin/mkappimage - chmod +x ~/bin/appimagetool ~/bin/mkappimage - echo "$HOME/bin" >> "$GITHUB_PATH" gh release download \ --repo AppImageCommunity/AppImageUpdate \ --pattern "AppImageUpdate-$APPIMAGE_ARCH.AppImage" \ --output ~/bin/AppImageUpdate.AppImage - chmod +x ~/bin/AppImageUpdate.AppImage + chmod +x ~/bin/* + echo "$HOME/bin" >> "$GITHUB_PATH" diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml index 0e6d48621..7de08e261 100644 --- a/.github/actions/setup-dependencies/macos/action.yml +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -14,7 +14,7 @@ runs: shell: bash run: | brew update - brew install ninja extra-cmake-modules temurin@17 + brew install ninja extra-cmake-modules temurin@17 mono - name: Set JAVA_HOME shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5eef33805..d83963b13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,9 @@ on: description: Type of build (Debug or Release) type: string default: Debug + environment: + description: Deployment environment to run under + type: string workflow_dispatch: inputs: build-type: @@ -73,6 +76,8 @@ jobs: build: name: Build (${{ matrix.artifact-name }}) + environment: ${{ inputs.environment || '' }} + permissions: # Required for Azure Trusted Signing id-token: write @@ -83,17 +88,15 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 + - os: ubuntu-24.04 artifact-name: Linux cmake-preset: linux + qt-version: 6.10.1 - # NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!! - # - # It's not really documented anywhere AFAICT, but upstream Qt binaries - # *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64 - os: ubuntu-24.04-arm artifact-name: Linux-aarch64 cmake-preset: linux + qt-version: 6.10.1 - os: windows-2022 artifact-name: Windows-MinGW-w64 @@ -112,16 +115,19 @@ jobs: cmake-preset: windows_msvc # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! vcvars-arch: amd64 + qt-version: 6.10.1 - os: windows-11-arm artifact-name: Windows-MSVC-arm64 cmake-preset: windows_msvc vcvars-arch: arm64 + qt-version: 6.10.1 - - os: macos-14 + - os: macos-26 artifact-name: macOS cmake-preset: macos_universal macosx-deployment-target: 12.0 + qt-version: 6.9.3 runs-on: ${{ matrix.os }} @@ -155,7 +161,7 @@ jobs: artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} vcvars-arch: ${{ matrix.vcvars-arch }} - qt-architecture: ${{ matrix.qt-architecture }} + qt-version: ${{ matrix.qt-version }} ## # BUILD @@ -214,6 +220,8 @@ jobs: - name: Package (Windows) if: ${{ runner.os == 'Windows' }} uses: ./.github/actions/package/windows + env: + CI_HAS_ACCESS_TO_AZURE: ${{ vars.CI_HAS_ACCESS_TO_AZURE || '' }} with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 17b722118..8a5fa26fb 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -79,6 +79,7 @@ jobs: uses: ./.github/actions/setup-dependencies with: build-type: Debug + qt-version: 6.10.1 - name: Configure and Build run: | diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index e457538ff..e3fbb2c32 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -77,9 +77,6 @@ jobs: - os: ubuntu-22.04 arch: x86_64 - - os: ubuntu-22.04-arm - arch: aarch64 - runs-on: ${{ matrix.os }} container: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index dbf8e8f52..2035668f4 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -86,9 +86,6 @@ jobs: - os: ubuntu-22.04-arm system: aarch64-linux - - os: macos-15-intel - system: x86_64-darwin - - os: macos-14 system: aarch64-darwin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e5445c66..bd22fe05c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,7 @@ jobs: uses: ./.github/workflows/build.yml with: build-type: Release + environment: Release secrets: inherit create_release: @@ -34,6 +35,7 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz + mv PrismLauncher-Linux-aarch64-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-*.AppImage/PrismLauncher-*-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-*.AppImage/PrismLauncher-*-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage @@ -88,6 +90,7 @@ jobs: name: Prism Launcher ${{ env.VERSION }} draft: true prerelease: false + fail_on_unmatched_files: true files: | PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync diff --git a/CMakeLists.txt b/CMakeLists.txt index 197839893..8816be82e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.22) # minimum version required by Qt -project(Launcher) +project(Launcher LANGUAGES C CXX) +if(APPLE) + enable_language(OBJC OBJCXX) +endif() string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) if(IS_IN_SOURCE_BUILD) @@ -79,19 +82,6 @@ else() if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") - # Emit PDBs for WinDbg, etc. - if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") - set(CMAKE_EXE_LINKER_FLAGS "-Wl,--pdb= ${CMAKE_EXE_LINKER_FLAGS}") - - foreach(lang C CXX) - set("CMAKE_${lang}_FLAGS" "-gcodeview ${CMAKE_${lang}_FLAGS}") - - # Force-enabling this to use generator expressions like TARGET_PDB_FILE - # (and because we can actually emit PDBs) - set("CMAKE_${lang}_LINKER_SUPPORTS_PDB" ON) - endforeach() - endif() - # -ffunction-sections and -fdata-sections help reduce binary size # -mguard=cf enables Control Flow Guard # TODO: Look into -gc-sections to further reduce binary size @@ -116,30 +106,24 @@ endif() option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser -if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND DEBUG_ADDRESS_SANITIZER) +if (DEBUG_ADDRESS_SANITIZER) message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") - if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") - if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") - # using clang with clang-cl front end - message(STATUS "Address Sanitizer available on Clang MSVC frontend") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") - else() - # AppleClang and Clang - message(STATUS "Address Sanitizer available on Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover=null") - endif() - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # GCC - message(STATUS "Address Sanitizer available on GCC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer -fsanitize=undefined -fno-sanitize-recover") - link_libraries("asan") - elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") - message(STATUS "Address Sanitizer available on MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") + + set(USE_ASAN_COMPILE_OPTIONS $,$>) + if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") + message(STATUS "Using Address Sanitizer compile options for MSVC frontend") + add_compile_options( + $<${USE_ASAN_COMPILE_OPTIONS}:/fsanitize=address> + $<${USE_ASAN_COMPILE_OPTIONS}:/Oy-> + ) + elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" OR "${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") + message(STATUS "Using Address Sanitizer compile options for GCC/Clang") + add_compile_options( + $<${USE_ASAN_COMPILE_OPTIONS}:-fsanitize=address,undefined> + $<${USE_ASAN_COMPILE_OPTIONS}:-fno-omit-frame-pointer> + $<${USE_ASAN_COMPILE_OPTIONS}:-fno-sanitize-recover=null> + ) + link_libraries("asan" "ubsan") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() @@ -337,7 +321,7 @@ if(NOT LibArchive_FOUND) pkg_check_modules(libarchive REQUIRED IMPORTED_TARGET libarchive) endif() -find_package(tomlplusplus 3.2.0 REQUIRED) +find_package(tomlplusplus 3.2.0) # fallback to pkgconfig, important especially as many distros package toml++ built with meson if(NOT tomlplusplus_FOUND) find_package(PkgConfig REQUIRED) @@ -399,6 +383,52 @@ if(UNIX AND APPLE) # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) + find_program(ACTOOL_EXE actool DOC "Path to the apple asset catalog compiler") + if(ACTOOL_EXE) + execute_process( + COMMAND xcodebuild -version + OUTPUT_VARIABLE XCODE_VERSION_OUTPUT + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + string(REGEX MATCH "Xcode ([0-9]+\.[0-9]+)" XCODE_VERSION_MATCH "${XCODE_VERSION_OUTPUT}") + if(XCODE_VERSION_MATCH) + set(XCODE_VERSION ${CMAKE_MATCH_1}) + else() + set(XCODE_VERSION 0.0) + endif() + + if(XCODE_VERSION VERSION_GREATER_EQUAL 26.0) + set(ASSETS_OUT_DIR "${CMAKE_BINARY_DIR}/program_info") + set(GENERATED_ASSETS_CAR "${ASSETS_OUT_DIR}/Assets.car") + set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") + + add_custom_command( + OUTPUT "${GENERATED_ASSETS_CAR}" + COMMAND ${ACTOOL_EXE} "${ICON_SOURCE}" + --compile "${ASSETS_OUT_DIR}" + --output-partial-info-plist /dev/null + --app-icon PrismLauncher + --enable-on-demand-resources NO + --target-device mac + --minimum-deployment-target ${CMAKE_OSX_DEPLOYMENT_TARGET} + --platform macosx + DEPENDS "${ICON_SOURCE}" + COMMENT "Compiling asset catalog (${ICON_SOURCE})" + VERBATIM + ) + add_custom_target(compile_assets ALL DEPENDS "${GENERATED_ASSETS_CAR}") + install(FILES "${GENERATED_ASSETS_CAR}" DESTINATION "${RESOURCES_DEST_DIR}") + else() + message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") + endif() + + else() + message(WARNING "actool not found. Cannot compile macOS app icons.\n" + "Install Xcode command line tools: 'xcode-select --install'") + endif() + + elseif(UNIX) include(KDEInstallDirs) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 3a8c8fbfe..cfd671d68 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -21,7 +21,9 @@ CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} + ${Launcher_Name} + CFBundleIconName + ${Launcher_Name} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion diff --git a/flake.lock b/flake.lock index 27b7ddbb6..730f36a14 100644 --- a/flake.lock +++ b/flake.lock @@ -18,18 +18,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1765472234, - "narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", - "type": "github" + "lastModified": 1766473571, + "narHash": "sha256-QvjEJNgMVuOootbR+DEfbiW+zSK57U32CE0jmVdcNjQ=", + "rev": "76701a179d3a98b07653e2b0409847499b2a07d3", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.2403.76701a179d3a/nixexprs.tar.xz" }, "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" + "type": "tarball", + "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" } }, "root": { diff --git a/flake.nix b/flake.nix index 594a82d91..6594db522 100644 --- a/flake.nix +++ b/flake.nix @@ -9,7 +9,7 @@ }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs.url = "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz"; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 601ccaeff..67ddb53e8 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -371,25 +371,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } QString origcwdPath = QDir::currentPath(); -#if defined(Q_OS_LINUX) - const QString binFilePath = applicationFilePath(); - const bool isAppImage = binFilePath.startsWith("/tmp/.mount_"); - // Yes, this can technically trigger the logic below if someone makes an AppImage with an actual launcher exe named "ld-linux" - // Please don't :) - const bool executedFromLinker = QFileInfo(binFilePath).fileName().startsWith("ld-linux"); - - // NOTE(@getchoo): In order for `go-appimage` to generate self-contained AppImages, it executes apps from a bundled linker at - // /lib64 - // This is not the path to our actual binary, which we want - QString binPath; - if (isAppImage && executedFromLinker) { - binPath = FS::PathCombine(applicationDirPath(), "../usr/bin"); - } else { - binPath = applicationDirPath(); - } -#else QString binPath = applicationDirPath(); -#endif { // Root path is used for updates and portable data diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 02fc1321a..fce3bb177 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -26,13 +26,13 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp - archive/ArchiveReader.cpp - archive/ArchiveReader.h - archive/ArchiveWriter.cpp - archive/ArchiveWriter.h - archive/ExportToZipTask.cpp + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h + archive/ExportToZipTask.cpp archive/ExportToZipTask.h - archive/ExtractZipTask.cpp + archive/ExtractZipTask.cpp archive/ExtractZipTask.h StringUtils.h StringUtils.cpp @@ -621,10 +621,10 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp - archive/ArchiveReader.cpp - archive/ArchiveReader.h - archive/ArchiveWriter.cpp - archive/ArchiveWriter.h + archive/ArchiveReader.cpp + archive/ArchiveReader.h + archive/ArchiveWriter.cpp + archive/ArchiveWriter.h # Time MMCTime.h @@ -1300,6 +1300,16 @@ endif() include(CompilerWarnings) +######## Precompiled Headers ########### + +set(PRECOMPILED_HEADERS + include/base.pch.hpp + include/qtcore.pch.hpp + include/qtgui.pch.hpp +) + +####### Targets ######## + # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) set_project_warnings(Launcher_logic @@ -1308,6 +1318,7 @@ set_project_warnings(Launcher_logic "${Launcher_GCC_WARNINGS}") target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) +target_precompile_headers(Launcher_logic PRIVATE ${PRECOMPILED_HEADERS}) target_link_libraries(Launcher_logic systeminfo Launcher_murmur2 @@ -1389,6 +1400,7 @@ endif() target_link_libraries(Launcher_logic) add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) +target_precompile_headers(${Launcher_Name} REUSE_FROM Launcher_logic) target_link_libraries(${Launcher_Name} Launcher_logic) if(DEFINED Launcher_APP_BINARY_NAME) @@ -1412,14 +1424,15 @@ install(TARGETS ${Launcher_Name} ) # Deploy PDBs -if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) - install(FILES $ DESTINATION ${BINARY_DEST_DIR}) +if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() if(Launcher_BUILD_UPDATER) # Updater add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_precompile_headers(prism_updater_logic PRIVATE ${PRECOMPILED_HEADERS}) target_link_libraries(prism_updater_logic ${ZLIB_LIBRARIES} systeminfo @@ -1439,6 +1452,7 @@ if(Launcher_BUILD_UPDATER) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) + target_precompile_headers("${Launcher_Name}_updater" REUSE_FROM prism_updater_logic) if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") @@ -1455,8 +1469,8 @@ if(Launcher_BUILD_UPDATER) ) # Deploy PDBs - if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) - install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() @@ -1469,6 +1483,8 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) "${Launcher_GCC_WARNINGS}") target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_precompile_headers(filelink_logic PRIVATE ${PRECOMPILED_HEADERS}) + target_link_libraries(filelink_logic systeminfo BuildConfig @@ -1482,6 +1498,8 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + target_precompile_headers("${Launcher_Name}_filelink" REUSE_FROM filelink_logic) + # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests @@ -1506,8 +1524,8 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) ) # Deploy PDBs - if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) - install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + if(CMAKE_CXX_LINKER_SUPPORTS_PDB) + install(FILES $ DESTINATION ${BINARY_DEST_DIR} OPTIONAL) endif() endif() diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index b926dbca5..841c1399c 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 dada513 - * Copyright (C) 2025 Seth Flynn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -76,15 +75,6 @@ bool isFlatpak() #endif } -bool isSelfContained() -{ -#ifdef Q_OS_LINUX - return QFileInfo(QCoreApplication::applicationFilePath()).fileName().startsWith("ld-linux"); -#else - return false; -#endif -} - bool isSnap() { #ifdef Q_OS_LINUX diff --git a/launcher/DesktopServices.h b/launcher/DesktopServices.h index 5deb25872..6c6208e82 100644 --- a/launcher/DesktopServices.h +++ b/launcher/DesktopServices.h @@ -37,11 +37,6 @@ bool openUrl(const QUrl& url); */ bool isFlatpak(); -/** - * Determine whether the launcher is running in a self-contained Linux bundle - */ -bool isSelfContained(); - /** * Determine whether the launcher is running in a Snap environment */ diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index ab7c73493..30d0a9c4c 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -4,7 +4,6 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> - * Copyright (C) 2025 Seth Flynn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -773,34 +772,6 @@ QString ResolveExecutable(QString path) return pathInfo.absoluteFilePath(); } -std::unique_ptr createProcess(const QString& program, const QStringList& arguments) -{ - qDebug() << "Creating process for" << program; - auto proc = std::unique_ptr(new QProcess()); - -#if defined(Q_OS_LINUX) - if (DesktopServices::isSelfContained()) { - const auto linkerPath = QCoreApplication::applicationFilePath(); - qDebug() << "Wrapping" << program << "with self-contained linker at" << linkerPath; - - QStringList wrappedArguments; - wrappedArguments << "--inhibit-cache" << program; - wrappedArguments += arguments; - - proc->setProgram(linkerPath); - proc->setArguments(wrappedArguments); - } else { - proc->setProgram(program); - proc->setArguments(arguments); - } -#else - proc->setProgram(program); - proc->setArguments(arguments); -#endif - - return proc; -} - /** * Normalize path * diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index db8545fd2..f2676b147 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -4,7 +4,6 @@ * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 TheKodeToad * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> - * Copyright (C) 2025 Seth Flynn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,13 +42,11 @@ #include -#include #include #include #include #include #include -#include #include namespace FS { @@ -336,14 +333,6 @@ QString pathTruncate(const QString& path, int depth); */ QString ResolveExecutable(QString path); -/** - * Create a QProcess instance - * - * This wrapper is currently only required for wrapping binaries called in - * self-contained AppImages (like those created by `go-appimage`) - */ -std::unique_ptr createProcess(const QString& program, const QStringList& arguments); - /** * Normalize path * diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 151a8c765..6bc17e058 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -271,7 +271,7 @@ bool installIcon(QString root, QString instIconKey) if (iconList->iconFileExists(instIconKey)) { iconList->deleteIcon(instIconKey); } - iconList->installIcon(importIconPath, instIconKey + ".png"); + iconList->installIcon(importIconPath, instIconKey + "." + QFileInfo(importIconPath).suffix()); return true; } return false; diff --git a/launcher/Json.cpp b/launcher/Json.cpp index dd7287e00..688f9dae7 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -101,6 +101,21 @@ QJsonArray requireArray(const QJsonDocument& doc, const QString& what) return doc.array(); } +QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error, QString* garbage) +{ + auto doc = QJsonDocument::fromJson(json, error); + if (error->error == QJsonParseError::GarbageAtEnd) { + qsizetype offset = error->offset; + QByteArray validJson = json.left(offset); + doc = QJsonDocument::fromJson(validJson, error); + + if (garbage) + *garbage = json.right(json.size() - offset); + } + + return doc; +} + void writeString(QJsonObject& to, const QString& key, const QString& value) { if (!value.isEmpty()) { diff --git a/launcher/Json.h b/launcher/Json.h index 8a994d7bc..7a50af167 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -107,6 +107,9 @@ QJsonArray toJsonArray(const QList& container) ////////////////// READING //////////////////// +// Attempt to parse JSON up until garbage is encountered +QJsonDocument parseUntilGarbage(const QByteArray& json, QJsonParseError* error = nullptr, QString* garbage = nullptr); + /// @throw JsonException template T requireIsType(const QJsonValue& value, const QString& what = "Value"); diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 706d7022b..057171a27 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -18,89 +18,19 @@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" -# Set up env. -# Pass our custom variables separately so that the launcher can remove them for child processes -export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@" -export LAUNCHER_LD_PRELOAD="" -export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins" -export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts" +# Makes the launcher use portals for file picking +export QT_QPA_PLATFORMTHEME=xdgdesktopportal -export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH" -export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD" -export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH" -export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH" +# Just to be sure... +chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -# Detect missing dependencies... -DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'` -if [ "x$DEPS_LIST" = "x" ]; then - # We have all our dependencies. Run the launcher. - echo "No missing dependencies found." +ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") - # Just to be sure... - chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" - - ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") - - if [ -f portable.txt ]; then - ARGS+=("-d" "${LAUNCHER_DIR}") - fi - - ARGS+=("$@") - - # Run the launcher - exec -a "${ARGS[@]}" - - # Run the launcher in valgrind - # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" - - # Run the launcher with callgrind, delay instrumentation - # valgrind --log-file="valgrind.log" --tool=callgrind --instr-atstart=no "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" - # use callgrind_control -i on/off to profile actions - - # Exit with launcher's exit code. - # exit $? -else - # apt - if which apt-file &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" - # pacman - elif which pkgfile &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" - # dnf - elif which dnf &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do dnf whatprovides -q $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | grep -v 'Repo' | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo dnf install $COMMAND_LIBS" - # yum - elif which yum &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo yum install $COMMAND_LIBS" - # zypper - elif which zypper &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo zypper install $COMMAND_LIBS" - # emerge - elif which pfl &>/dev/null; then - LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*" | sort -u` - COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` - COMMAND_LIBS=`echo "$COMMAND_LIBS" | sort -u | awk -vORS=" " '{ print $1 }'` - INSTALL_CMD="sudo emerge $COMMAND_LIBS" - fi - - MESSAGE="Error: The launcher is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." - MESSAGE="$MESSAGE\n\nHint (please apply common sense): $INSTALL_CMD\n" - - printerror "$MESSAGE" - exit 1 +if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") fi + +ARGS+=("$@") + +# Run the launcher +exec -a "${ARGS[@]}" \ No newline at end of file diff --git a/launcher/include/base.pch.hpp b/launcher/include/base.pch.hpp new file mode 100644 index 000000000..c858857f9 --- /dev/null +++ b/launcher/include/base.pch.hpp @@ -0,0 +1,17 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_BASE_HEADERS_H +#define PRISM_PRECOMPILED_BASE_HEADERS_H + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#endif // PRISM_PRECOMPILED_BASE_HEADERS_H diff --git a/launcher/include/qtcore.pch.hpp b/launcher/include/qtcore.pch.hpp new file mode 100644 index 000000000..d7c24ddb6 --- /dev/null +++ b/launcher/include/qtcore.pch.hpp @@ -0,0 +1,59 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_QTCORE_HEADERS_H +#define PRISM_PRECOMPILED_QTCORE_HEADERS_H + +#include +#include +#include +#include + +#include + +#include + +// collections +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +#include +#include +#include + +#endif // PRISM_PRECOMPILED_QTCORE_HEADERS_H diff --git a/launcher/include/qtgui.pch.hpp b/launcher/include/qtgui.pch.hpp new file mode 100644 index 000000000..fb57cb33a --- /dev/null +++ b/launcher/include/qtgui.pch.hpp @@ -0,0 +1,47 @@ +#pragma once +#ifndef PRISM_PRECOMPILED_QTGUI_HEADERS_H +#define PRISM_PRECOMPILED_QTGUI_HEADERS_H + +#include + +#include + +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#endif // PRISM_PRECOMPILED_GUI_HEADERS_H diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index da57e9354..cdf670b0f 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -91,6 +91,7 @@ #include #include #include +#include #include #ifdef Q_OS_LINUX @@ -588,6 +589,16 @@ QStringList MinecraftInstance::javaArguments() "minecraft.exe.heapdump"); #endif + // LWJGL2 reads `LWJGL_DISABLE_XRANDR` to force disable xrandr usage and fall back to xf86videomode. + // It *SHOULD* check for the executable to exist before trying to use it for queries but it doesnt, + // so WE can and force disable xrandr if it is not available. +#ifdef Q_OS_LINUX + // LWJGL2 is "org.lwjgl" LWJGL3 is "org.lwjgl3" + if (m_components->getComponent("org.lwjgl") != nullptr && QStandardPaths::findExecutable("xrandr").isEmpty()) { + args << QString("-DLWJGL_DISABLE_XRANDR=true"); + } +#endif + int min = settings()->get("MinMemAlloc").toInt(); int max = settings()->get("MaxMemAlloc").toInt(); if (min < max) { diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 3cbbb2a74..cef79d200 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -669,7 +669,7 @@ void AccountList::beginActivity() void AccountList::endActivity() { if (m_activityCount == 0) { - qWarning() << m_name << " - Activity count would become below zero"; + qWarning() << "Activity count would become below zero"; return; } bool deactivating = m_activityCount == 1; diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index d3be6740e..2f1276312 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -111,7 +111,6 @@ class AccountList : public QAbstractListModel { void endActivity(); private: - const char* m_name; uint32_t m_activityCount = 0; signals: void listChanged(); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 785d3f7e5..4083a7088 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -207,7 +207,13 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat bool ResourceFolderModel::uninstallResource(const QString& file_name, bool preserve_metadata) { for (auto& resource : m_resources) { - if (resource->fileinfo().fileName() == file_name) { + auto resourceFileInfo = resource->fileinfo(); + auto resourceFileName = resource->fileinfo().fileName(); + if (!resource->enabled() && resourceFileName.endsWith(".disabled")) { + resourceFileName.chop(9); + } + + if (resourceFileName == file_name) { auto res = resource->destroy(indexDir(), preserve_metadata, false); update(); diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 9b4bb4a50..60f836b4d 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -168,10 +168,15 @@ bool processZIP(DataPack* pack, ProcessingLevel level) // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(DataPack* pack, QByteArray&& raw_data) { - try { - auto json_doc = QJsonDocument::fromJson(raw_data); - auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + QJsonParseError parse_error; + auto json_doc = Json::parseUntilGarbage(raw_data, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Failed to parse JSON:" << parse_error.errorString(); + return false; + } + try { + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); pack->setPackFormat(pack_obj["pack_format"].toInt()); pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); } catch (Json::JsonException& e) { diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index 789899388..ca2545e05 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2025 Trial97 + * Copyright (c) 2025 Rinth, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,29 +23,89 @@ #include "FileSystem.h" +static void setAlpha(QImage& image, const QRect& region, const int alpha) +{ + for (int y = region.top(); y < region.bottom(); ++y) { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = region.left(); x < region.right(); ++x) { + QRgb pixel = line[x]; + line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), alpha); + } + } +} + +static void doNotchTransparencyHack(QImage& image) +{ + for (int y = 0; y < 32; y++) { + QRgb* line = reinterpret_cast(image.scanLine(y)); + for (int x = 32; x < 64; x++) { + if (qAlpha(line[x]) < 128) { + return; + } + } + } + + setAlpha(image, { 32, 0, 32, 32 }, 0); +} + static QImage improveSkin(QImage skin) { + int height = skin.height(); + int width = skin.width(); + if (width != 64 || (height != 32 && height != 64)) { // this is no minecraft skin + return skin; + } // It seems some older skins may use this format, which can't be drawn onto // https://github.com/PrismLauncher/PrismLauncher/issues/4032 // https://doc.qt.io/qt-6/qpainter.html#begin if (skin.format() == QImage::Format_Indexed8) { - skin = skin.convertToFormat(QImage::Format_RGB32); + skin = skin.convertToFormat(QImage::Format_ARGB32); } - if (skin.size() == QSize(64, 32)) { // old format + + auto isLegacy = height == 32; // old format + if (isLegacy) { auto newSkin = QImage(QSize(64, 64), skin.format()); newSkin.fill(Qt::transparent); QPainter p(&newSkin); - p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head + p.drawImage(0, 0, skin); - auto leg = skin.copy(QRect(0, 16, 16, 16)); - p.drawImage(QPoint(16, 48), leg); // copy leg + auto copyRect = [&p, &newSkin](int startX, int startY, int offsetX, int offsetY, int sizeX, int sizeY) { + QImage region = newSkin.copy(startX, startY, sizeX, sizeY); + region = region.mirrored(true, false); - auto arm = skin.copy(QRect(40, 16, 16, 16)); - p.drawImage(QPoint(32, 48), arm); // copy arm - return newSkin; + p.drawImage(startX + offsetX, startY + offsetY, region); + }; + static const struct { + int x; + int y; + int offsetX; + int offsetY; + int width; + int height; + } faces[] = { + { 4, 16, 16, 32, 4, 4 }, { 8, 16, 16, 32, 4, 4 }, { 0, 20, 24, 32, 4, 12 }, { 4, 20, 16, 32, 4, 12 }, + { 8, 20, 8, 32, 4, 12 }, { 12, 20, 16, 32, 4, 12 }, { 44, 16, -8, 32, 4, 4 }, { 48, 16, -8, 32, 4, 4 }, + { 40, 20, 0, 32, 4, 12 }, { 44, 20, -8, 32, 4, 12 }, { 48, 20, -16, 32, 4, 12 }, { 52, 20, -8, 32, 4, 12 }, + }; + + for (const auto& face : faces) { + copyRect(face.x, face.y, face.offsetX, face.offsetY, face.width, face.height); + } + doNotchTransparencyHack(newSkin); + skin = newSkin; + } + static const QRect opaqueParts[] = { + { 0, 0, 32, 16 }, + { 0, 16, 64, 16 }, + { 16, 48, 32, 16 }, + }; + + for (const auto& p : opaqueParts) { + setAlpha(skin, p, 255); } return skin; } + static QImage getSkin(const QString path) { return improveSkin(QImage(path)); @@ -66,8 +127,8 @@ static QImage generatePreviews(QImage texture, bool slim) paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); // left leg - paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); auto armWidth = slim ? 3 : 4; auto armPosX = slim ? 1 : 0; @@ -89,8 +150,8 @@ static QImage generatePreviews(QImage texture, bool slim) paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); // left leg - paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); // right arm paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 128c7856b..699e12b40 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -126,7 +126,8 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q return {}; } for (auto mcVer : versionArray) { - file.mcVersion.append(ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString())); + file.mcVersion.append({ ModrinthAPI::mapMCVersionFromModrinth(mcVer.toString()), + mcVer.toString() }); // double this so we can check both strings when filtering } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index cecde236a..d665a1781 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -57,7 +57,12 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) { m_ui->setupUi(this); - m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); + if (SkinOpenGLWindow::hasOpenGL()) { + m_skinPreview = new SkinOpenGLWindow(this, palette().color(QPalette::Normal, QPalette::Base)); + } else { + m_skinPreviewLabel = new QLabel(this); + m_skinPreviewLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + } setWindowModality(Qt::WindowModal); @@ -92,7 +97,9 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SkinManageDialog::selectionChanged); connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() { - m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); + if (m_skinPreview) { + m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); + } on_capeCombo_currentIndexChanged(0); }); @@ -103,13 +110,19 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); + if (m_skinPreview) { + m_ui->skinLayout->insertWidget(0, QWidget::createWindowContainer(m_skinPreview, this)); + } else { + m_ui->skinLayout->insertWidget(0, m_skinPreviewLabel); + } } SkinManageDialog::~SkinManageDialog() { delete m_ui; - delete m_skinPreview; + if (m_skinPreview) { + delete m_skinPreview; + } } void SkinManageDialog::activated(QModelIndex index) @@ -131,7 +144,12 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, [[maybe_unused] if (!skin) return; - m_skinPreview->updateScene(skin); + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } m_ui->capeCombo->setCurrentIndex(m_capesIdx.value(skin->getCapeId())); m_ui->steveBtn->setChecked(skin->getModel() == SkinModel::CLASSIC); m_ui->alexBtn->setChecked(skin->getModel() == SkinModel::SLIM); @@ -165,17 +183,17 @@ void SkinManageDialog::on_fileBtn_clicked() QPixmap previewCape(QImage capeImage, bool elytra = false) { if (elytra) { - auto wing = capeImage.copy(34, 0, 12, 22); + auto wing = capeImage.copy(34, 2, 12, 20); QImage mirrored = wing.mirrored(true, false); - QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format()); + QImage combined(wing.width() * 2 + 1, wing.height() + 14, capeImage.format()); combined.fill(Qt::transparent); QPainter painter(&combined); - painter.drawImage(0, 0, wing); - painter.drawImage(wing.width() - 2, 0, mirrored); + painter.drawImage(0, 7, wing); + painter.drawImage(wing.width() + 1, 7, mirrored); painter.end(); - return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation)); + return QPixmap::fromImage(combined.scaled(84, 128, Qt::KeepAspectRatio, Qt::FastTransformation)); } return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } @@ -244,10 +262,17 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) } else { m_ui->capeImage->clear(); } - m_skinPreview->updateCape(cape); + if (m_skinPreview) { + m_skinPreview->updateCape(cape); + } if (auto skin = getSelectedSkin(); skin) { skin->setCapeId(id.toString()); - m_skinPreview->updateScene(skin); + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } } } @@ -255,7 +280,12 @@ void SkinManageDialog::on_steveBtn_toggled(bool checked) { if (auto skin = getSelectedSkin(); skin) { skin->setModel(checked ? SkinModel::CLASSIC : SkinModel::SLIM); - m_skinPreview->updateScene(skin); + if (m_skinPreview) { + m_skinPreview->updateScene(skin); + } else { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } } } @@ -545,6 +575,10 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) } else { m_ui->capeImage->clear(); } + if (auto skin = getSelectedSkin(); skin && !m_skinPreview) { + m_skinPreviewLabel->setPixmap( + QPixmap::fromImage(skin->getPreview()).scaled(m_skinPreviewLabel->size(), Qt::KeepAspectRatio, Qt::FastTransformation)); + } } SkinModel* SkinManageDialog::getSelectedSkin() diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.h b/launcher/ui/dialogs/skins/SkinManageDialog.h index c6a6c9fcd..27bdb93a8 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.h +++ b/launcher/ui/dialogs/skins/SkinManageDialog.h @@ -20,6 +20,7 @@ #include #include +#include #include #include "minecraft/auth/MinecraftAccount.h" @@ -68,4 +69,5 @@ class SkinManageDialog : public QDialog, public SkinProvider { QHash m_capes; QHash m_capesIdx; SkinOpenGLWindow* m_skinPreview = nullptr; + QLabel* m_skinPreviewLabel = nullptr; }; diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp index 89a783622..ad51187ca 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.cpp +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -31,32 +31,50 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctio m_staticComponents = { // head new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), - new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), // body new opengl::BoxGeometry(QVector3D(8, 12, 4), QVector3D(0, -6, 0), QPoint(16, 16), QVector3D(8, 12, 4)), - new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), // right leg new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-1.9, -18, -0.1), QPoint(0, 16), QVector3D(4, 12, 4)), - new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), // left leg new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(1.9, -18, -0.1), QPoint(16, 48), QVector3D(4, 12, 4)), + }; + + m_staticComponentsOverlay = { + // head + new opengl::BoxGeometry(QVector3D(9, 9, 9), QVector3D(0, 4, 0), QPoint(32, 0), QVector3D(8, 8, 8)), + // body + new opengl::BoxGeometry(QVector3D(8.5, 12.5, 4.5), QVector3D(0, -6, 0), QPoint(16, 32), QVector3D(8, 12, 4)), + // right leg + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-1.9, -18, -0.1), QPoint(0, 32), QVector3D(4, 12, 4)), + // left leg new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(1.9, -18, -0.1), QPoint(0, 48), QVector3D(4, 12, 4)), }; + m_normalArms = { // Right Arm new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(-6, -6, 0), QPoint(40, 16), QVector3D(4, 12, 4)), - new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(4, 12, 4), QVector3D(6, -6, 0), QPoint(32, 48), QVector3D(4, 12, 4)), + }; + + m_normalArmsOverlay = { + // Right Arm + new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(-6, -6, 0), QPoint(40, 32), QVector3D(4, 12, 4)), + // Left Arm new opengl::BoxGeometry(QVector3D(4.5, 12.5, 4.5), QVector3D(6, -6, 0), QPoint(48, 48), QVector3D(4, 12, 4)), }; m_slimArms = { // Right Arm new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(-5.5, -6, 0), QPoint(40, 16), QVector3D(3, 12, 4)), - new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), // Left Arm new opengl::BoxGeometry(QVector3D(3, 12, 4), QVector3D(5.5, -6, 0), QPoint(32, 48), QVector3D(3, 12, 4)), + }; + + m_slimArmsOverlay = { + // Right Arm + new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(-5.5, -6, 0), QPoint(40, 32), QVector3D(3, 12, 4)), + // Left Arm new opengl::BoxGeometry(QVector3D(3.5, 12.5, 4.5), QVector3D(5.5, -6, 0), QPoint(48, 48), QVector3D(3, 12, 4)), }; @@ -88,7 +106,8 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctio } Scene::~Scene() { - for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) { + for (auto array : + { m_staticComponents, m_normalArms, m_slimArms, m_elytra, m_staticComponentsOverlay, m_normalArmsOverlay, m_slimArmsOverlay }) { for (auto g : array) { delete g; } @@ -106,7 +125,8 @@ void Scene::draw(QOpenGLShaderProgram* program) { m_skinTexture->bind(); program->setUniformValue("texture", 0); - for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms }) { + for (auto toDraw : { m_staticComponents, m_slim ? m_slimArms : m_normalArms, m_staticComponentsOverlay, + m_slim ? m_slimArmsOverlay : m_normalArmsOverlay }) { for (auto g : toDraw) { g->draw(program); } diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h index c9bba1f20..31526de45 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.h +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -38,6 +38,9 @@ class Scene : protected QOpenGLFunctions { QList m_staticComponents; QList m_normalArms; QList m_slimArms; + QList m_staticComponentsOverlay; + QList m_normalArmsOverlay; + QList m_slimArmsOverlay; BoxGeometry* m_cape = nullptr; QList m_elytra; QOpenGLTexture* m_skinTexture = nullptr; diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp index 962717a30..e4d1eae8d 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include "minecraft/skins/SkinModel.h" #include "rainbow.h" @@ -270,11 +271,12 @@ void SkinOpenGLWindow::updateCape(const QImage& cape) QColor calculateContrastingColor(const QColor& color) { - constexpr float contrast = 0.2; auto luma = Rainbow::luma(color); if (luma < 0.5) { + constexpr float contrast = 0.05; return Rainbow::lighten(color, contrast); } else { + constexpr float contrast = 0.2; return Rainbow::darken(color, contrast); } } @@ -282,11 +284,14 @@ QColor calculateContrastingColor(const QColor& color) QImage generateChessboardImage(int width, int height, int tileSize, QColor baseColor) { QImage image(width, height, QImage::Format_RGB888); - auto white = baseColor; - auto black = calculateContrastingColor(baseColor); + bool isDarkBase = Rainbow::luma(baseColor) < 0.5; + float contrast = isDarkBase ? 0.05 : 0.45; + auto contrastFunc = std::bind(isDarkBase ? Rainbow::lighten : Rainbow::darken, std::placeholders::_1, contrast, 1.0); + auto white = contrastFunc(baseColor); + auto black = contrastFunc(calculateContrastingColor(baseColor)); for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { - bool isWhite = ((x / tileSize) % 2) == ((y / tileSize) % 2); + bool isWhite = ((x / tileSize) + (y / tileSize)) % 2 == 0; image.setPixelColor(x, y, isWhite ? white : black); } } @@ -325,3 +330,9 @@ void SkinOpenGLWindow::setElytraVisible(bool visible) if (m_scene) m_scene->setElytraVisible(visible); } + +bool SkinOpenGLWindow::hasOpenGL() +{ + QOpenGLContext ctx; + return ctx.create(); +} diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h index 7b88fca9c..6ddc345cf 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -45,6 +45,8 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { void updateCape(const QImage& cape); void setElytraVisible(bool visible); + static bool hasOpenGL(); + protected: void mousePressEvent(QMouseEvent* e) override; void mouseReleaseEvent(QMouseEvent* e) override; diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index abc19f006..834eebb96 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -10,7 +10,7 @@ 620 - + 0 diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index eba2ac1de..9174d6ee4 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -113,6 +113,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared m_model->loadColumns(ui->treeView); connect(ui->treeView->header(), &QHeaderView::sectionResized, this, [this] { m_model->saveColumns(ui->treeView); }); connect(ui->filterEdit, &QLineEdit::textChanged, this, &ExternalResourcesPage::filterTextChanged); + updateActions(); } ExternalResourcesPage::~ExternalResourcesPage() diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp index 11b3a22d1..25c167122 100644 --- a/launcher/ui/pages/instance/McClient.cpp +++ b/launcher/ui/pages/instance/McClient.cpp @@ -80,7 +80,13 @@ void McClient::parseResponse() Q_UNUSED(readVarInt(m_resp)); // json length // 'resp' should now be the JSON string - QJsonDocument doc = QJsonDocument::fromJson(m_resp); + QJsonParseError parseError; + QJsonDocument doc = Json::parseUntilGarbage(m_resp, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qDebug() << "Failed to parse JSON:" << parseError.errorString(); + emitFail(parseError.errorString()); + return; + } emitSucceed(doc.object()); } diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp index cd2ff31d8..f88865afc 100644 --- a/launcher/ui/pages/instance/ServerPingTask.cpp +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -1,13 +1,19 @@ #include #include +#include "Exception.h" #include "McClient.h" #include "McResolver.h" #include "ServerPingTask.h" unsigned getOnlinePlayers(QJsonObject data) { - return Json::requireInteger(Json::requireObject(data, "players"), "online"); + try { + return Json::requireInteger(Json::requireObject(data, "players"), "online"); + } catch (Exception& e) { + qWarning() << "server ping failed to parse response" << e.what(); + return 0; + } } void ServerPingTask::executeTask() diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 337c3e474..0fa6b6f23 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -19,10 +19,10 @@ - Note: If your FTB instances are not in the default location, select it using the button next to search. + Note: Many recent FTB modpacks are also available from CurseForge! Also, if your FTB instances are not in the default location, select it using the button next to search. - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -47,8 +47,7 @@ - - .. + true @@ -82,7 +81,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index a29b1ae36..73685d3ba 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -203,7 +203,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI ++it; #endif for (const auto& version : m_current->versions) { - m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.addonId)); + m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.fileId)); } QVariant current_updated; @@ -227,9 +227,9 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI for (auto version : m_current->versions) { if (!version.version.contains(version.version)) m_ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.version, version.version_number), - QVariant(version.addonId)); + QVariant(version.fileId)); else - m_ui->versionSelectionBox->addItem(version.version, QVariant(version.addonId)); + m_ui->versionSelectionBox->addItem(version.version, QVariant(version.fileId)); } suggestCurrent(); @@ -312,7 +312,7 @@ void ModrinthPage::suggestCurrent() } for (auto& ver : m_current->versions) { - if (ver.addonId == m_selectedVersion) { + if (ver.fileId == m_selectedVersion) { QMap extra_info; extra_info.insert("pack_id", m_current->addonId.toString()); extra_info.insert("pack_version_id", ver.fileId.toString()); diff --git a/launcher/updater/PrismExternalUpdater.cpp b/launcher/updater/PrismExternalUpdater.cpp index 6b237d947..69774dc04 100644 --- a/launcher/updater/PrismExternalUpdater.cpp +++ b/launcher/updater/PrismExternalUpdater.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +35,6 @@ #include "StringUtils.h" #include "BuildConfig.h" -#include "FileSystem.h" #include "ui/dialogs/UpdateAvailableDialog.h" @@ -97,9 +97,14 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) progress.show(); QCoreApplication::processEvents(); + QProcess proc; auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); #else exe_name = QString("bin/%1").arg(exe_name); #endif @@ -108,21 +113,15 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) if (priv->allowBeta) args.append("--pre-release"); - auto proc = FS::createProcess(priv->appDir.absoluteFilePath(exe_name), args); -#if defined Q_OS_WIN32 - auto env = QProcessEnvironment::systemEnvironment(); - env.insert("__COMPAT_LAYER", "RUNASINVOKER"); - proc->setProcessEnvironment(env); -#endif - proc->start(proc->program(), proc->arguments()); - auto result_start = proc->waitForStarted(5000); + proc.start(priv->appDir.absoluteFilePath(exe_name), args); + auto result_start = proc.waitForStarted(5000); if (!result_start) { - auto err = proc->error(); + auto err = proc.error(); qDebug() << "Failed to start updater after 5 seconds." - << "reason:" << err << proc->errorString(); + << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), - tr("Failed to start after 5 seconds\nReason: %1.").arg(proc->errorString()), QMessageBox::Ok, priv->parent); + tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setMinimumWidth(460); msgBox.adjustSize(); msgBox.exec(); @@ -134,16 +133,16 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) } QCoreApplication::processEvents(); - auto result_finished = proc->waitForFinished(60000); + auto result_finished = proc.waitForFinished(60000); if (!result_finished) { - proc->kill(); - auto err = proc->error(); - auto output = proc->readAll(); + proc.kill(); + auto err = proc.error(); + auto output = proc.readAll(); qDebug() << "Updater failed to close after 60 seconds." - << "reason:" << err << proc->errorString(); + << "reason:" << err << proc.errorString(); auto msgBox = QMessageBox(QMessageBox::Information, tr("Update Check Failed"), - tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc->errorString()), QMessageBox::Ok, priv->parent); + tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); msgBox.setDetailedText(output); msgBox.setMinimumWidth(460); msgBox.adjustSize(); @@ -155,10 +154,10 @@ void PrismExternalUpdater::checkForUpdates(bool triggeredByUser) return; } - auto exit_code = proc->exitCode(); + auto exit_code = proc.exitCode(); - auto std_output = proc->readAllStandardOutput(); - auto std_error = proc->readAllStandardError(); + auto std_output = proc.readAllStandardOutput(); + auto std_error = proc.readAllStandardError(); progress.hide(); QCoreApplication::processEvents(); @@ -336,9 +335,14 @@ void PrismExternalUpdater::offerUpdate(const QString& version_name, const QStrin void PrismExternalUpdater::performUpdate(const QString& version_tag) { + QProcess proc; auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); #if defined Q_OS_WIN32 exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); #else exe_name = QString("bin/%1").arg(exe_name); #endif @@ -347,16 +351,9 @@ void PrismExternalUpdater::performUpdate(const QString& version_tag) if (priv->allowBeta) args.append("--pre-release"); - auto proc = FS::createProcess(exe_name, args); -#if defined Q_OS_WIN32 - auto env = QProcessEnvironment::systemEnvironment(); - env.insert("__COMPAT_LAYER", "RUNASINVOKER"); - proc->setProcessEnvironment(env); -#endif - - auto result = proc->startDetached(priv->appDir.absoluteFilePath(exe_name), args); + auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args); if (!result) { - qDebug() << "Failed to start updater:" << proc->error() << proc->errorString(); + qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); } QCoreApplication::exit(); } diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index cc28bfc1d..b84bab13a 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -124,19 +124,7 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar logToConsole = parser.isSet("debug"); QString origCwdPath = QDir::currentPath(); -#if defined(Q_OS_LINUX) - // NOTE(@getchoo): In order for `go-appimage` to generate self-contained AppImages, it executes apps from a bundled linker at - // /lib64 - // This is not the path to our actual binary, which we want - QString binPath; - if (DesktopServices::isSelfContained()) { - binPath = FS::PathCombine(applicationDirPath(), "../usr/bin"); - } else { - binPath = applicationDirPath(); - } -#else QString binPath = applicationDirPath(); -#endif { // find data director // Root path is used for updates and portable data @@ -819,16 +807,13 @@ QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset) bool PrismUpdaterApp::callAppImageUpdate() { auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + QProcess proc = QProcess(); qDebug() << "Calling: AppImageUpdate" << appimage_path; - const auto program = FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage"); - auto proc = FS::createProcess(program, { appimage_path }); - if (!proc) { - qCritical() << "Unable to create process:" << program; - return false; - } - auto result = proc->startDetached(); + proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate.AppImage")); + proc.setArguments({ appimage_path }); + auto result = proc.startDetached(); if (!result) - qDebug() << "Failed to start AppImageUpdate reason:" << proc->errorString(); + qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); return result; } diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 4ca2d7b3b..00752a8c4 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -63,6 +63,7 @@ symlinkJoin { buildInputs = [ kdePackages.qtbase + kdePackages.qtimageformats kdePackages.qtsvg ] ++ lib.optional ( diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index 23eb840b1..064b1a540 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -32,6 +32,7 @@ set(Launcher_MetaInfo "program_info/${Launcher_AppID}.metainfo.xml" PARENT_SCOPE set(Launcher_PNG_256 "program_info/${Launcher_AppID}_256.png" PARENT_SCOPE) set(Launcher_SVG "program_info/${Launcher_SVGFileName}" PARENT_SCOPE) set(Launcher_Branding_ICNS "program_info/prismlauncher.icns" PARENT_SCOPE) +set(Launcher_Branding_MAC_ICON "program_info/PrismLauncher.icon" PARENT_SCOPE) set(Launcher_Branding_ICO "program_info/prismlauncher.ico") set(Launcher_Branding_ICO "${Launcher_Branding_ICO}" PARENT_SCOPE) set(Launcher_Branding_WindowsRC "program_info/prismlauncher.rc" PARENT_SCOPE) diff --git a/program_info/PrismLauncher.icon/Assets/block.svg b/program_info/PrismLauncher.icon/Assets/block.svg new file mode 100644 index 000000000..08a80d8bd --- /dev/null +++ b/program_info/PrismLauncher.icon/Assets/block.svg @@ -0,0 +1,91 @@ + + + Prism Launcher Logo + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/PrismLauncher.icon/Assets/rainbow.svg b/program_info/PrismLauncher.icon/Assets/rainbow.svg new file mode 100644 index 000000000..d47bb615a --- /dev/null +++ b/program_info/PrismLauncher.icon/Assets/rainbow.svg @@ -0,0 +1,95 @@ + + + Prism Launcher Logo + + + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/program_info/PrismLauncher.icon/icon.json b/program_info/PrismLauncher.icon/icon.json new file mode 100644 index 000000000..131c7de4c --- /dev/null +++ b/program_info/PrismLauncher.icon/icon.json @@ -0,0 +1,72 @@ +{ + "color-space-for-untagged-svg-colors" : "display-p3", + "fill" : { + "solid" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "block.svg", + "name" : "block", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + }, + { + "blur-material-specializations" : [ + { + "value" : 0.5 + }, + { + "appearance" : "dark", + "value" : null + } + ], + "layers" : [ + { + "blend-mode" : "normal", + "fill" : "automatic", + "glass" : true, + "hidden" : false, + "image-name" : "rainbow.svg", + "name" : "rainbow", + "position" : { + "scale" : 19.28, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : [ + "macOS" + ] + } +} \ No newline at end of file diff --git a/tools/ninjatracing.py b/tools/ninjatracing.py new file mode 100644 index 000000000..418124855 --- /dev/null +++ b/tools/ninjatracing.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# Copyright 2018 Nico Weber +# Patched to work with v7 logs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Converts one (or several) .ninja_log files into chrome's about:tracing format. + +If clang -ftime-trace .json files are found adjacent to generated files they +are embedded. + +Usage: + ninja -C $BUILDDIR + ninjatracing $BUILDDIR/.ninja_log > trace.json +""" + +import json +import os +import optparse +import re +import sys + + +class Target: + """Represents a single line read for a .ninja_log file. Start and end times + are milliseconds.""" + def __init__(self, start, end): + self.start = int(start) + self.end = int(end) + self.targets = [] + + +def read_targets(log, show_all): + """Reads all targets from .ninja_log file |log_file|, sorted by start + time""" + header = log.readline() + m = re.search(r'^# ninja log v(\d+)\n$', header) + assert m, "unrecognized ninja log version %r" % header + version = int(m.group(1)) + assert 5 <= version <= 7, "unsupported ninja log version %d" % version + if version >= 6: + # Skip header line + next(log) + + targets = {} + last_end_seen = 0 + for line in log: + if line.startswith('#'): + continue + start, end, _, name, cmdhash = line.strip().split('\t') # Ignore restat. + if not show_all and int(end) < last_end_seen: + # An earlier time stamp means that this step is the first in a new + # build, possibly an incremental build. Throw away the previous data + # so that this new build will be displayed independently. + targets = {} + last_end_seen = int(end) + targets.setdefault(cmdhash, Target(start, end)).targets.append(name) + return sorted(targets.values(), key=lambda job: job.end, reverse=True) + + +class Threads: + """Tries to reconstruct the parallelism from a .ninja_log""" + def __init__(self): + self.workers = [] # Maps thread id to time that thread is occupied for. + + def alloc(self, target): + """Places target in an available thread, or adds a new thread.""" + for worker in range(len(self.workers)): + if self.workers[worker] >= target.end: + self.workers[worker] = target.start + return worker + self.workers.append(target.start) + return len(self.workers) - 1 + + +def read_events(trace, options): + """Reads all events from time-trace json file |trace|.""" + trace_data = json.load(trace) + + def include_event(event, options): + """Only include events if they are complete events, are longer than + granularity, and are not totals.""" + return ((event['ph'] == 'X') and + (event['dur'] >= options['granularity']) and + (not event['name'].startswith('Total'))) + + return [x for x in trace_data['traceEvents'] if include_event(x, options)] + + +def trace_to_dicts(target, trace, options, pid, tid): + """Read a file-like object |trace| containing -ftime-trace data and yields + about:tracing dict per eligible event in that log.""" + ninja_time = (target.end - target.start) * 1000 + for event in read_events(trace, options): + # Check if any event duration is greater than the duration from ninja. + if event['dur'] > ninja_time: + print("Inconsistent timing found (clang time > ninja time). Please" + " ensure that timings are from consistent builds.") + sys.exit(1) + + # Set tid and pid from ninja log. + event['pid'] = pid + event['tid'] = tid + + # Offset trace time stamp by ninja start time. + event['ts'] += (target.start * 1000) + + yield event + + +def embed_time_trace(ninja_log_dir, target, pid, tid, options): + """Produce time trace output for the specified ninja target. Expects + time-trace file to be in .json file named based on .o file.""" + for t in target.targets: + o_path = os.path.join(ninja_log_dir, t) + json_trace_path = os.path.splitext(o_path)[0] + '.json' + try: + with open(json_trace_path, 'r') as trace: + for time_trace_event in trace_to_dicts(target, trace, options, + pid, tid): + yield time_trace_event + except IOError: + pass + + +def log_to_dicts(log, pid, options): + """Reads a file-like object |log| containing a .ninja_log, and yields one + about:tracing dict per command found in the log.""" + threads = Threads() + for target in read_targets(log, options['showall']): + tid = threads.alloc(target) + + yield { + 'name': '%0s' % ', '.join(target.targets), 'cat': 'targets', + 'ph': 'X', 'ts': (target.start * 1000), + 'dur': ((target.end - target.start) * 1000), + 'pid': pid, 'tid': tid, 'args': {}, + } + if options.get('embed_time_trace', False): + # Add time-trace information into the ninja trace. + try: + ninja_log_dir = os.path.dirname(log.name) + except AttributeError: + continue + for time_trace in embed_time_trace(ninja_log_dir, target, pid, + tid, options): + yield time_trace + + +def main(argv): + usage = __doc__ + parser = optparse.OptionParser(usage) + parser.add_option('-a', '--showall', action='store_true', dest='showall', + default=False, + help='report on last build step for all outputs. Default ' + 'is to report just on the last (possibly incremental) ' + 'build') + parser.add_option('-g', '--granularity', type='int', default=50000, + dest='granularity', + help='minimum length time-trace event to embed in ' + 'microseconds. Default: %default') + parser.add_option('-e', '--embed-time-trace', action='store_true', + default=False, dest='embed_time_trace', + help='embed clang -ftime-trace json file found adjacent ' + 'to a target file') + (options, args) = parser.parse_args() + + if len(args) == 0: + print('Must specify at least one .ninja_log file') + parser.print_help() + return 1 + + entries = [] + for pid, log_file in enumerate(args): + with open(log_file, 'r') as log: + entries += list(log_to_dicts(log, pid, vars(options))) + json.dump(entries, sys.stdout) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:]))