diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c31fce..57cca5f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -371,9 +371,124 @@ jobs: name: PollyMC-Continued-Linux-AppImage path: PollyMC-Continued-Linux-x86_64.AppImage + build-macos: + name: macOS (Universal) + runs-on: macos-26 + permissions: + contents: read + env: + ARTIFACT_NAME: macOS-universal + ARTIFACT_ARCH: universal + BUILD_PLATFORM: macOS-universal + BUILD_TYPE: Release + EXPECTED_ARCHS: arm64 x86_64 + EXPECTED_MIN_MACOS: "12.0" + MACOSX_DEPLOYMENT_TARGET: "12.0" + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install build tools + run: | + brew update + brew install ninja extra-cmake-modules temurin@17 mono + echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" + echo "VCPKG_ROOT=$VCPKG_INSTALLATION_ROOT" >> "$GITHUB_ENV" + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + aqtversion: "==3.1.*" + version: 6.9.3 + modules: qtimageformats qtnetworkauth + cache: true + + - name: Configure + run: | + cmake --preset macos_universal \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$MACOSX_DEPLOYMENT_TARGET" \ + -DLauncher_BUILD_RELEASE=ON + + - name: Read and validate version + id: version + run: | + VERSION=$(cmake -N -LA build | sed -n 's/^Launcher_VERSION:STRING=//p') + if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Invalid Launcher_VERSION: $VERSION" >&2 + exit 1 + fi + if [[ "$GITHUB_REF" == refs/tags/v* && "v$VERSION" != "$GITHUB_REF_NAME" ]]; then + echo "Tag $GITHUB_REF_NAME does not match launcher version $VERSION" >&2 + exit 1 + fi + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Build + run: cmake --build --preset macos_universal --config "$BUILD_TYPE" + + - name: Test + run: ctest --test-dir build --build-config "$BUILD_TYPE" --output-on-failure + + - name: Import signing certificate + env: + APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} + run: | + if [[ -z "$APPLE_CODESIGN_CERT" && -z "$APPLE_CODESIGN_ID" ]]; then + echo "No Apple signing certificate configured; the artifact will be ad-hoc signed." + exit 0 + fi + if [[ -z "$APPLE_CODESIGN_CERT" || -z "$APPLE_CODESIGN_ID" || -z "$APPLE_CODESIGN_PASSWORD" ]]; then + echo "Apple signing secrets are only partially configured." >&2 + exit 1 + fi + KEYCHAIN_PATH="$RUNNER_TEMP/pollymc-build.keychain-db" + CERTIFICATE_PATH="$RUNNER_TEMP/pollymc-codesign.p12" + printf '%s' "$APPLE_CODESIGN_CERT" | base64 --decode > "$CERTIFICATE_PATH" + security create-keychain -p "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" + security default-keychain -s "$KEYCHAIN_PATH" + security unlock-keychain -p "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" + security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CODESIGN_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$APPLE_CODESIGN_PASSWORD" "$KEYCHAIN_PATH" + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + + - name: Package app, ZIP, and DMG + env: + APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} + APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} + run: | + packaging/macos/package.sh \ + build \ + install-macos \ + dist-macos \ + "${{ steps.version.outputs.version }}" \ + "$BUILD_TYPE" + + - name: Upload artifacts + uses: actions/upload-artifact@v7 + with: + name: PollyMC-Continued-macOS-${{ steps.version.outputs.version }} + path: | + dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-universal.zip + dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-universal.dmg + dist-macos/PollyMC-Continued-${{ steps.version.outputs.version }}-macOS-universal.sha256 + if-no-files-found: error + + - name: Remove temporary signing keychain + if: ${{ always() }} + run: | + if [[ -n "${KEYCHAIN_PATH:-}" && -f "$KEYCHAIN_PATH" ]]; then + security delete-keychain "$KEYCHAIN_PATH" + fi + release: name: Publish Release - needs: [build-windows, build-linux] + needs: [build-windows, build-linux, build-macos] runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' permissions: @@ -425,3 +540,6 @@ jobs: PollyMC-Continued-*-Setup.exe PollyMC-Continued-*-Windows-portable.zip PollyMC-Continued-Linux-x86_64.AppImage + PollyMC-Continued-*-macOS-universal.zip + PollyMC-Continued-*-macOS-universal.dmg + PollyMC-Continued-*-macOS-universal.sha256 diff --git a/.gitignore b/.gitignore index 037dd3f..3efaed8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ build # Install dirs install /install-* +/dist-* # Ctags File tags diff --git a/CMakeLists.txt b/CMakeLists.txt index ddd691d..91f1345 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,14 @@ cmake_minimum_required(VERSION 3.25) # Required for features like `CMAKE_MSVC_DEBUG_INFORMATION_FORMAT` -project(Launcher LANGUAGES C CXX) +set(Launcher_VERSION "9.0.8" CACHE STRING "Launcher version in MAJOR.MINOR.PATCH format") +if(Launcher_VERSION STREQUAL "") + set(Launcher_VERSION "9.0.8" CACHE STRING "Launcher version in MAJOR.MINOR.PATCH format" FORCE) +endif() +if(NOT Launcher_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$") + message(FATAL_ERROR "Launcher_VERSION must use MAJOR.MINOR.PATCH format (got '${Launcher_VERSION}')") +endif() + +project(Launcher VERSION "${Launcher_VERSION}" LANGUAGES C CXX) if(APPLE) enable_language(OBJC OBJCXX) endif() @@ -157,6 +165,7 @@ if(ENABLE_LTO) endif() option(BUILD_TESTING "Build the testing tree." ON) +option(Launcher_BUILD_RELEASE "Hide Git branch metadata from public release artifacts." OFF) find_package(ECM NO_MODULE REQUIRED) set(CMAKE_MODULE_PATH "${ECM_MODULE_PATH};${CMAKE_MODULE_PATH}") @@ -178,10 +187,6 @@ set(Launcher_LOGIN_CALLBACK_URL "https://login.live.com/oauth20_desktop.srf" CAC set(Launcher_LEGACY_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for legacy (<=1.5.2) FML Libraries.") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 9) -set(Launcher_VERSION_MINOR 0) -set(Launcher_VERSION_PATCH 0) - set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") @@ -275,6 +280,12 @@ include(GetGitRevisionDescription) git_get_exact_tag(Launcher_GIT_TAG) get_git_head_revision(Launcher_GIT_REFSPEC Launcher_GIT_COMMIT) +if(Launcher_BUILD_RELEASE) + # Use stable metadata in packaged builds. + set(Launcher_GIT_REFSPEC "refs/heads/stable") + set(Launcher_GIT_TAG "${Launcher_VERSION_NAME}") +endif() + message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") message(STATUS "Git tag: ${Launcher_GIT_TAG}") message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") @@ -287,7 +298,7 @@ set(Launcher_BUILD_TIMESTAMP "${TODAY}") # Find the required Qt parts if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 6.4 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL WebSockets) + find_package(Qt6 6.4 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml NetworkAuth OpenGL Svg WebSockets) find_package(Qt6 COMPONENTS DBus) list(APPEND Launcher_QT_DBUS Qt6::DBus) else() @@ -360,11 +371,11 @@ endif() if(UNIX AND APPLE) set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") - set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") - set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") + set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/PlugIns") set(FRAMEWORK_DEST_DIR "${Launcher_Name}.app/Contents/Frameworks") set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") - set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") + set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/Resources/jars") # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") @@ -390,7 +401,8 @@ if(UNIX AND APPLE) 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) + set(ICON_SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/${Launcher_Branding_MAC_ICON}") + if(ACTOOL_EXE AND EXISTS "${ICON_SOURCE}") execute_process( COMMAND xcodebuild -version OUTPUT_VARIABLE XCODE_VERSION_OUTPUT @@ -407,7 +419,6 @@ if(UNIX AND APPLE) 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}" @@ -428,7 +439,8 @@ if(UNIX AND APPLE) else() message(WARNING "Xcode ${XCODE_VERSION} is too old. Minimum required version is 26.0. Not compiling liquid glass icons.") endif() - + elseif(NOT EXISTS "${ICON_SOURCE}") + message(STATUS "Modern macOS icon source '${ICON_SOURCE}' is not present; using the bundled ICNS icon") else() message(WARNING "actool not found. Cannot compile macOS app icons.\n" "Install Xcode command line tools: 'xcode-select --install'") diff --git a/README.md b/README.md index 18fac98..190f524 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ PollyMC-Continued lets you play Minecraft **without needing a Microsoft account* ## Download -Grab the latest release from the [Releases](https://github.com/SharathGames1/PollyMC/releases) page. +Grab the latest release from the [Releases](https://github.com/corecommit/PollyMC-Continued/releases) page. ## Building @@ -49,6 +49,31 @@ cmake --install . --prefix C:/pollymc_build "/c/Program Files (x86)/NSIS/makensis.exe" pollymc_installer.nsi ``` +### macOS + +Install the dependencies: + +```bash +brew install cmake ninja qt extra-cmake-modules cmark qrencode libarchive tomlplusplus +brew install --cask temurin@17 +``` + +Build, test and package the native version for the current Mac: + +```bash +bash packaging/macos/build-local.sh +``` + +The script uses the current macOS major version as the default deployment target, avoiding invalid or incompatible local bundles built against Homebrew libraries. Override it when needed: + +```bash +MACOSX_DEPLOYMENT_TARGET=15.0 bash packaging/macos/build-local.sh +``` + +The resulting `dist-macos` directory contains a ready-to-run `PollyMC.app`, ZIP, DMG and SHA-256 checksums. The locally generated DMG file receives the same Finder icon as the application. The DMG also contains `PollyMC.app` and an `Applications` shortcut for normal drag-and-drop installation. + +The custom icon attached to the local DMG file is stored in a macOS resource fork. Release hosting services may discard that metadata, while the icon of the mounted DMG volume remains embedded in the disk image. + ## Credits - [Prism Launcher](https://github.com/PrismLauncher/PrismLauncher) — the base launcher diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index eb40bac..9b1d6a4 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -13,11 +13,13 @@ NSPrincipalClass NSApplication NSHighResolutionCapable - True + CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleDisplayName + ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile @@ -42,8 +44,8 @@ ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped - LSRequiresCarbon - + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} LSApplicationCategoryType public.app-category.games NSHumanReadableCopyright diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 54cad5c..4e0cc97 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1890,6 +1890,9 @@ QString Application::getJarPath(QString jarFile) QStringList potentialPaths = { #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) FS::PathCombine(m_rootPath, "share", BuildConfig.LAUNCHER_NAME), +#endif +#if defined(Q_OS_MACOS) + FS::PathCombine(applicationDirPath(), "..", "Resources", "jars"), #endif FS::PathCombine(m_rootPath, "jars"), FS::PathCombine(applicationDirPath(), "jars"), FS::PathCombine(applicationDirPath(), "..", "jars") // from inside build dir, for debuging diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6e68b7c..7a963e1 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1374,6 +1374,7 @@ target_link_libraries(Launcher_logic Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::NetworkAuth Qt${QT_VERSION_MAJOR}::OpenGL + Qt${QT_VERSION_MAJOR}::Svg Qt${QT_VERSION_MAJOR}::WebSockets ${Launcher_QT_DBUS} ${Launcher_QT_LIBS} @@ -1418,7 +1419,10 @@ endif() target_link_libraries(${Launcher_Name} Launcher_logic) -if(DEFINED Launcher_APP_BINARY_NAME) +if(APPLE) + # Match the app bundle name. + set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_Name}") +elseif(DEFINED Launcher_APP_BINARY_NAME) set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}") endif() if(DEFINED Launcher_BINARY_RPATH) @@ -1583,6 +1587,24 @@ endif() if(WIN32 OR (UNIX AND APPLE)) if(WIN32) set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") + elseif(APPLE) + # Homebrew may install optional Qt modules in separate prefixes. + set(QT_DEPLOY_TOOL_OPTIONS "-no-codesign") + set(_qt_optional_components Svg Pdf Qml Quick VirtualKeyboard) + set(_qt_deploy_lib_dirs) + foreach(_qt_component IN LISTS _qt_optional_components) + find_package(Qt6 QUIET COMPONENTS ${_qt_component}) + set(_qt_component_dir_var "Qt6${_qt_component}_DIR") + if(DEFINED ${_qt_component_dir_var}) + get_filename_component(_qt_component_prefix "${${_qt_component_dir_var}}/../../.." ABSOLUTE) + set(_qt_component_lib_dir "${_qt_component_prefix}/lib") + list(APPEND _qt_deploy_lib_dirs "${_qt_component_lib_dir}") + endif() + endforeach() + list(REMOVE_DUPLICATES _qt_deploy_lib_dirs) + foreach(_qt_deploy_lib_dir IN LISTS _qt_deploy_lib_dirs) + string(APPEND QT_DEPLOY_TOOL_OPTIONS " -libpath=${_qt_deploy_lib_dir}") + endforeach() endif() qt_generate_deploy_script( @@ -1597,29 +1619,32 @@ if(WIN32 OR (UNIX AND APPLE)) PLUGINS_DIR ${PLUGIN_DEST_DIR} NO_OVERWRITE NO_TRANSLATIONS + NO_COMPILER_RUNTIME DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} )" ) - # Bundle our linked dependencies - install( - RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET - COMPONENT bundle - DIRECTORIES - ${CMAKE_SYSTEM_LIBRARY_PATH} - ${QT_LIBS_DIR} - ${QT_LIBEXECS_DIR} - PRE_EXCLUDE_REGEXES - "^(api-ms-win|ext-ms)-.*\\.dll$" - # FIXME: Why aren't these caught by the below regex??? - "^azure.*\\.dll$" - "^vcruntime.*\\.dll$" - POST_EXCLUDE_REGEXES - "system32" - LIBRARY DESTINATION ${LIBRARY_DEST_DIR} - RUNTIME DESTINATION ${BINARY_DEST_DIR} - FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} - ) + if(WIN32) + # macdeployqt handles runtime dependencies on macOS. + install( + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + COMPONENT bundle + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + # FIXME: Why aren't these caught by the below regex??? + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + POST_EXCLUDE_REGEXES + "system32" + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} + RUNTIME DESTINATION ${BINARY_DEST_DIR} + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} + ) + endif() # Deploy Qt plugins install( SCRIPT ${QT_DEPLOY_SCRIPT} @@ -1694,7 +1719,7 @@ if(WIN32 OR (UNIX AND APPLE)) # Add qtlogging.ini as a config file install( FILES "qtlogging.ini" - DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} + DESTINATION ${RESOURCES_DEST_DIR} COMPONENT bundle ) endif() diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 82d95a6..82cd53d 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -569,7 +569,7 @@ Close the current window - QAction::QuitRole + QAction::MenuRole::NoRole diff --git a/packaging/macos/build-local.sh b/packaging/macos/build-local.sh new file mode 100644 index 0000000..0711998 --- /dev/null +++ b/packaging/macos/build-local.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +source_root=$(cd "$script_dir/../.." && pwd) +cd "$source_root" + +fail() { + echo "Local macOS build failed: $*" >&2 + exit 1 +} + +command -v git >/dev/null 2>&1 || fail "Git is required" +command -v brew >/dev/null 2>&1 || fail "Homebrew is required" +command -v cmake >/dev/null 2>&1 || fail "CMake is required (brew install cmake)" +command -v ninja >/dev/null 2>&1 || fail "Ninja is required (brew install ninja)" +xcode-select -p >/dev/null 2>&1 || fail "Xcode Command Line Tools are required (xcode-select --install)" + +java_home=$(/usr/libexec/java_home -v 17 2>/dev/null) || fail "JDK 17 is required (brew install --cask temurin@17)" +export JAVA_HOME=$java_home + +formulae=(qt libarchive tomlplusplus cmark qrencode extra-cmake-modules) +prefixes=() +for formula in "${formulae[@]}"; do + prefix=$(brew --prefix "$formula" 2>/dev/null) || fail "Missing Homebrew dependency: $formula" + prefixes+=("$prefix") +done +if [[ -n ${CMAKE_PREFIX_PATH:-} ]]; then + prefixes+=("$CMAKE_PREFIX_PATH") +fi +export CMAKE_PREFIX_PATH=$(IFS=';'; echo "${prefixes[*]}") + +deployment_target=${MACOSX_DEPLOYMENT_TARGET:-$(sw_vers -productVersion | awk -F. '{print $1 ".0"}')} +architecture=$(uname -m) +build_dir=${BUILD_DIR:-build-macos} +install_dir=${INSTALL_DIR:-install-macos} +output_dir=${OUTPUT_DIR:-dist-macos} + +for directory in "$build_dir" "$install_dir" "$output_dir"; do + case "$directory" in + ''|/|.|..|/*|../*|*/../*|*/..) fail "Working directories must be relative paths inside the repository: $directory" ;; + esac +done + +git submodule update --init --recursive +cmake -E rm -rf "$build_dir" "$install_dir" "$output_dir" + +cmake -S . -B "$build_dir" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="$CMAKE_PREFIX_PATH" \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$deployment_target" \ + -DLauncher_BUILD_RELEASE=ON \ + -DLauncher_BUILD_PLATFORM="macOS-$architecture" + +cmake --build "$build_dir" --parallel +ctest --test-dir "$build_dir" --output-on-failure + +version=$(cmake -N -LA "$build_dir" | sed -n 's/^Launcher_VERSION:STRING=//p') +[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "Could not read Launcher_VERSION" + +ARTIFACT_ARCH=$architecture \ +EXPECTED_ARCHS=$architecture \ +EXPECTED_MIN_MACOS=$deployment_target \ + "$script_dir/package.sh" "$build_dir" "$install_dir" "$output_dir" "$version" + +standalone_app="$output_dir/PollyMC.app" +cmake -E rm -rf "$standalone_app" +ditto --rsrc --extattr --noqtn --noacl "$install_dir/PollyMC.app" "$standalone_app" + +source_branch=$(git symbolic-ref --short -q HEAD 2>/dev/null || true) +EXPECTED_VERSION=$version \ +EXPECTED_ARCHS=$architecture \ +EXPECTED_MIN_MACOS=$deployment_target \ +SOURCE_ROOT=$source_root \ +SOURCE_BRANCH=$source_branch \ + "$script_dir/verify_bundle.sh" "$standalone_app" + +dmg_path="$output_dir/PollyMC-Continued-${version}-macOS-${architecture}.dmg" +app_icon="$standalone_app/Contents/Resources/PollyMC.icns" +[[ -f $dmg_path ]] || fail "DMG was not created: $dmg_path" +[[ -s $app_icon ]] || fail "Application icon is missing: $app_icon" + +rez=$(xcrun --find Rez) || fail "Rez is required from Xcode Command Line Tools" +setfile=$(xcrun --find SetFile) || fail "SetFile is required from Xcode Command Line Tools" +getfileinfo=$(xcrun --find GetFileInfo) || fail "GetFileInfo is required from Xcode Command Line Tools" +icon_resource=$(mktemp "${TMPDIR:-/tmp}/pollymc-dmg-icon.XXXXXX") +trap 'cmake -E rm -f "$icon_resource"' EXIT +escaped_icon=${app_icon//\\/\\\\} +escaped_icon=${escaped_icon//\"/\\\"} +printf "read 'icns' (-16455) \"%s\";\n" "$escaped_icon" > "$icon_resource" +"$rez" -append "$icon_resource" -o "$dmg_path" +"$setfile" -a C "$dmg_path" +touch "$dmg_path" + +attributes=$("$getfileinfo" -a "$dmg_path") +[[ $attributes == *C* ]] || fail "The DMG file custom icon flag was not set" +[[ -s "$dmg_path/..namedfork/rsrc" ]] || fail "The DMG file icon resource is missing" + +echo +echo "Created local macOS artifacts in $source_root/$output_dir:" +echo " PollyMC.app" +echo " $(basename "$dmg_path")" +echo " PollyMC-Continued-${version}-macOS-${architecture}.zip" +echo " PollyMC-Continued-${version}-macOS-${architecture}.sha256" +echo +echo "The DMG file icon is a macOS resource fork and is intended for local builds." diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh new file mode 100755 index 0000000..2b659d9 --- /dev/null +++ b/packaging/macos/package.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -lt 4 || $# -gt 5 ]]; then + echo "Usage: $0 BUILD_DIR INSTALL_DIR OUTPUT_DIR VERSION [BUILD_CONFIG]" >&2 + exit 2 +fi + +build_dir=$1 +install_dir=$2 +output_dir=$3 +version=$4 +build_config=${5:-Release} + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +source_root=$(cd "$script_dir/../.." && pwd) + +case "$version" in + ''|*[!0-9.]*) echo "VERSION must use MAJOR.MINOR.PATCH format" >&2; exit 2 ;; +esac +[[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "VERSION must use MAJOR.MINOR.PATCH format" >&2; exit 2; } + +[[ $(uname -s) == Darwin ]] || { echo "macOS packaging must run on macOS" >&2; exit 1; } +[[ -d $build_dir ]] || { echo "Build directory not found: $build_dir" >&2; exit 1; } + +for directory in "$install_dir" "$output_dir"; do + [[ -n $directory && $directory != / ]] || { echo "Refusing unsafe output directory: $directory" >&2; exit 1; } +done + +cmake -E rm -rf "$install_dir" +cmake -E make_directory "$install_dir" "$output_dir" +cmake --install "$build_dir" --config "$build_config" --prefix "$install_dir" + +app="$install_dir/PollyMC.app" +[[ -d $app ]] || { echo "Installed application not found: $app" >&2; exit 1; } +app_executable="$app/Contents/MacOS/PollyMC" + +# Remove plugins for unused Qt modules. +cmake -E rm -f \ + "$app/Contents/PlugIns/imageformats/libqpdf.dylib" \ + "$app/Contents/PlugIns/platforminputcontexts/libqtvirtualkeyboardplugin.dylib" + +find "$app" \( -name '.DS_Store' -o -name '._*' \) -delete +xattr -cr "$app" +chmod +x "$app_executable" + +# Remove build-machine search paths. +while IFS= read -r -d '' candidate; do + file "$candidate" | grep -q 'Mach-O' || continue + while IFS= read -r rpath; do + case "$rpath" in + @*) ;; + *) install_name_tool -delete_rpath "$rpath" "$candidate" ;; + esac + done < <(otool -l "$candidate" | awk '/cmd LC_RPATH/ { getline; getline; print $2 }') +done < <(find "$app/Contents" -type f -print0) + +if [[ -n ${APPLE_CODESIGN_ID:-} ]]; then + signing_identity=$APPLE_CODESIGN_ID + entitlements="$source_root/program_info/App.entitlements" + timestamp_args=(--timestamp) +else + signing_identity=- + entitlements="$source_root/program_info/AdhocSignedApp.entitlements" + timestamp_args=(--timestamp=none) +fi + +source_branch=$(git -C "$source_root" symbolic-ref --short -q HEAD 2>/dev/null || true) + +sign_code() { + codesign \ + --force \ + --sign "$signing_identity" \ + --options runtime \ + "${timestamp_args[@]}" \ + "$1" +} + +# Sign nested code before the app bundle. +while IFS= read -r -d '' candidate; do + file "$candidate" | grep -q 'Mach-O' || continue + [[ $candidate == "$app_executable" ]] && continue + [[ $candidate == *.framework/* ]] && continue + sign_code "$candidate" +done < <(find "$app/Contents" -type f -print0) + +while IFS= read -r -d '' framework; do + sign_code "$framework" +done < <(find "$app/Contents/Frameworks" -type d -name '*.framework' -prune -print0) + +codesign \ + --force \ + --sign "$signing_identity" \ + --entitlements "$entitlements" \ + --options runtime \ + "${timestamp_args[@]}" \ + "$app" + +EXPECTED_VERSION=$version \ +EXPECTED_ARCHS="${EXPECTED_ARCHS:-}" \ +EXPECTED_MIN_MACOS="${EXPECTED_MIN_MACOS:-}" \ +SOURCE_ROOT=$source_root \ +SOURCE_BRANCH=$source_branch \ + "$script_dir/verify_bundle.sh" "$app" + +artifact_arch=${ARTIFACT_ARCH:-$(uname -m)} +artifact_base="PollyMC-Continued-${version}-macOS-${artifact_arch}" +zip_path="$output_dir/$artifact_base.zip" +dmg_path="$output_dir/$artifact_base.dmg" + +notarize_values=( + "${APPLE_NOTARIZE_APPLE_ID:-}" + "${APPLE_NOTARIZE_TEAM_ID:-}" + "${APPLE_NOTARIZE_PASSWORD:-}" +) +notarize_count=0 +for value in "${notarize_values[@]}"; do + [[ -z $value ]] || notarize_count=$((notarize_count + 1)) +done + +if [[ $notarize_count -ne 0 && $notarize_count -ne 3 ]]; then + echo "All notarization credentials must be provided together" >&2 + exit 1 +fi + +if [[ $notarize_count -eq 3 ]]; then + notarize_zip="$output_dir/.notarize.zip" + ditto -c -k --sequesterRsrc --keepParent "$app" "$notarize_zip" + xcrun notarytool submit "$notarize_zip" \ + --wait \ + --apple-id "$APPLE_NOTARIZE_APPLE_ID" \ + --team-id "$APPLE_NOTARIZE_TEAM_ID" \ + --password "$APPLE_NOTARIZE_PASSWORD" + xcrun stapler staple "$app" + xcrun stapler validate "$app" + cmake -E rm -f "$notarize_zip" +fi + +cmake -E rm -f "$zip_path" "$dmg_path" +ditto -c -k --norsrc --noextattr --noqtn --noacl --keepParent "$app" "$zip_path" + +if unzip -Z1 "$zip_path" | grep -Eq '(^|/)__MACOSX/|(^|/)\._'; then + echo "ZIP contains Finder or AppleDouble metadata" >&2 + exit 1 +fi + +zip_check_dir="$install_dir/zip-check" +cmake -E rm -rf "$zip_check_dir" +cmake -E make_directory "$zip_check_dir" +ditto -x -k "$zip_path" "$zip_check_dir" +EXPECTED_VERSION=$version \ +EXPECTED_ARCHS="${EXPECTED_ARCHS:-}" \ +EXPECTED_MIN_MACOS="${EXPECTED_MIN_MACOS:-}" \ +SOURCE_ROOT=$source_root \ +SOURCE_BRANCH=$source_branch \ + "$script_dir/verify_bundle.sh" "$zip_check_dir/PollyMC.app" +cmake -E rm -rf "$zip_check_dir" + +dmg_root="$install_dir/dmg-root" +dmg_rw="$output_dir/$artifact_base-rw.dmg" +dmg_mount="$install_dir/dmg-mount" +dmg_check="$install_dir/dmg-check" +cmake -E rm -rf "$dmg_root" +cmake -E make_directory "$dmg_root" +ditto --norsrc --noextattr --noqtn --noacl "$app" "$dmg_root/PollyMC.app" +ln -s /Applications "$dmg_root/Applications" +cp "$app/Contents/Resources/PollyMC.icns" "$dmg_root/.VolumeIcon.icns" + +setfile=$(xcrun --find SetFile) +getfileinfo=$(xcrun --find GetFileInfo) + +hdiutil create \ + -volname "PollyMC-Continued $version" \ + -srcfolder "$dmg_root" \ + -format UDRW \ + -fs APFS \ + -ov \ + "$dmg_rw" + +cmake -E rm -rf "$dmg_mount" +cmake -E make_directory "$dmg_mount" +hdiutil attach -nobrowse -readwrite -mountpoint "$dmg_mount" "$dmg_rw" >/dev/null +if ! "$setfile" -a C "$dmg_mount"; then + hdiutil detach "$dmg_mount" >/dev/null || true + exit 1 +fi +sync +hdiutil detach "$dmg_mount" >/dev/null + +hdiutil convert \ + "$dmg_rw" \ + -format UDZO \ + -imagekey zlib-level=9 \ + -o "$dmg_path" +cmake -E rm -f "$dmg_rw" +hdiutil verify "$dmg_path" + +cmake -E rm -rf "$dmg_check" +cmake -E make_directory "$dmg_check" +hdiutil attach -nobrowse -readonly -mountpoint "$dmg_check" "$dmg_path" >/dev/null +dmg_attributes=$("$getfileinfo" -a "$dmg_check") +if [[ $dmg_attributes != *C* || ! -s "$dmg_check/.VolumeIcon.icns" || ! -L "$dmg_check/Applications" ]]; then + hdiutil detach "$dmg_check" >/dev/null || true + echo "DMG layout or volume icon is invalid" >&2 + exit 1 +fi +if ! EXPECTED_VERSION=$version \ + EXPECTED_ARCHS="${EXPECTED_ARCHS:-}" \ + EXPECTED_MIN_MACOS="${EXPECTED_MIN_MACOS:-}" \ + SOURCE_ROOT=$source_root \ + SOURCE_BRANCH=$source_branch \ + "$script_dir/verify_bundle.sh" "$dmg_check/PollyMC.app"; then + hdiutil detach "$dmg_check" >/dev/null || true + exit 1 +fi +hdiutil detach "$dmg_check" >/dev/null +cmake -E rm -rf "$dmg_check" + +( + cd "$output_dir" + shasum -a 256 "$(basename "$zip_path")" "$(basename "$dmg_path")" > "$artifact_base.sha256" +) + +echo "Created $zip_path" +echo "Created $dmg_path" diff --git a/packaging/macos/verify_bundle.sh b/packaging/macos/verify_bundle.sh new file mode 100755 index 0000000..08022c8 --- /dev/null +++ b/packaging/macos/verify_bundle.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $# -ne 1 ]]; then + echo "Usage: $0 /path/to/PollyMC.app" >&2 + exit 2 +fi + +app=$1 +contents="$app/Contents" +plist="$contents/Info.plist" + +fail() { + echo "macOS bundle verification failed: $*" >&2 + exit 1 +} + +[[ $(uname -s) == Darwin ]] || fail "this check must run on macOS" +[[ -d $app ]] || fail "application bundle not found: $app" +[[ -f $plist ]] || fail "Info.plist is missing" +plutil -lint "$plist" >/dev/null || fail "Info.plist is invalid" + +executable_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$plist") +executable="$contents/MacOS/$executable_name" +[[ -x $executable ]] || fail "bundle executable is missing or not executable" + +bundle_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleName' "$plist") +display_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleDisplayName' "$plist") +package_type=$(/usr/libexec/PlistBuddy -c 'Print :CFBundlePackageType' "$plist") +minimum_system_version=$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$plist") +[[ -n $bundle_name && $display_name == "$bundle_name" ]] || fail "bundle display name is missing or inconsistent" +[[ $package_type == APPL ]] || fail "bundle package type is not APPL" +[[ $minimum_system_version =~ ^[0-9]+(\.[0-9]+){1,2}$ ]] || fail "minimum macOS version is missing or invalid" + +bundle_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist") +if [[ -n ${EXPECTED_VERSION:-} && $bundle_version != "$EXPECTED_VERSION" ]]; then + fail "bundle version is $bundle_version, expected $EXPECTED_VERSION" +fi + +icon_name=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$plist") +icon="$contents/Resources/$icon_name" +[[ -f $icon ]] || icon="$icon.icns" +[[ -s $icon ]] || fail "application icon is missing: $icon_name" + +icon_check_dir=$(mktemp -d) +trap 'rm -rf "$icon_check_dir"' EXIT +iconutil -c iconset "$icon" -o "$icon_check_dir/AppIcon.iconset" +[[ -s "$icon_check_dir/AppIcon.iconset/icon_512x512@2x.png" ]] || fail "application icon has no 1024px representation" + +[[ -f "$contents/PlugIns/platforms/libqcocoa.dylib" ]] || fail "Qt Cocoa platform plugin is missing" +[[ -f "$contents/PlugIns/iconengines/libqsvgicon.dylib" ]] || fail "Qt SVG icon engine is missing" +[[ -f "$contents/Frameworks/QtSvg.framework/Versions/A/QtSvg" ]] || fail "Qt SVG framework is missing" +[[ -f "$contents/Resources/qt.conf" ]] || fail "qt.conf is missing" + +for jar in JavaCheck.jar NewLaunch.jar NewLaunchLegacy.jar SkinAgent.jar; do + [[ -s "$contents/Resources/jars/$jar" ]] || fail "runtime component is missing: $jar" +done + +if [[ -n ${EXPECTED_ARCHS:-} ]]; then + actual_archs=" $(lipo -archs "$executable") " + for expected_arch in $EXPECTED_ARCHS; do + [[ $actual_archs == *" $expected_arch "* ]] || fail "executable does not contain $expected_arch (has:${actual_archs})" + done +fi + +executable_dir=$(dirname "$executable") +executable_rpaths=() +while IFS= read -r rpath; do + [[ -z $rpath ]] || executable_rpaths+=("$rpath") +done < <(otool -l "$executable" | awk '/cmd LC_RPATH/ { getline; getline; print $2 }') + +expand_runtime_path() { + local runtime_path=$1 + local loader_dir=$2 + runtime_path=${runtime_path//@executable_path/$executable_dir} + runtime_path=${runtime_path//@loader_path/$loader_dir} + printf '%s\n' "$runtime_path" +} + +version_is_at_most() { + awk -v actual="$1" -v maximum="$2" 'BEGIN { + actual_count = split(actual, actual_parts, ".") + maximum_count = split(maximum, maximum_parts, ".") + count = actual_count > maximum_count ? actual_count : maximum_count + for (i = 1; i <= count; i++) { + a = actual_parts[i] + 0 + b = maximum_parts[i] + 0 + if (a < b) exit 0 + if (a > b) exit 1 + } + exit 0 + }' +} + +if [[ -n ${EXPECTED_MIN_MACOS:-} && $minimum_system_version != "$EXPECTED_MIN_MACOS" ]]; then + fail "Info.plist requires macOS $minimum_system_version, expected $EXPECTED_MIN_MACOS" +fi + +dependency_errors=0 +while IFS= read -r -d '' candidate; do + file "$candidate" | grep -q 'Mach-O' || continue + install_name=$(otool -D "$candidate" 2>/dev/null | awk 'NR == 2 { print $1 }') + loader_dir=$(dirname "$candidate") + candidate_rpaths=() + + if [[ -n ${EXPECTED_ARCHS:-} ]]; then + candidate_archs=" $(lipo -archs "$candidate") " + for expected_arch in $EXPECTED_ARCHS; do + if [[ $candidate_archs != *" $expected_arch "* ]]; then + echo "Missing architecture $expected_arch in $candidate (has:${candidate_archs})" >&2 + dependency_errors=1 + fi + done + fi + + if [[ -n ${EXPECTED_MIN_MACOS:-} ]]; then + while IFS= read -r minimum_macos; do + if ! version_is_at_most "$minimum_macos" "$EXPECTED_MIN_MACOS"; then + echo "$candidate requires macOS $minimum_macos (maximum allowed is $EXPECTED_MIN_MACOS)" >&2 + dependency_errors=1 + fi + done < <(vtool -show-build "$candidate" 2>/dev/null | awk '$1 == "minos" { print $2 }') + fi + + while IFS= read -r rpath; do + [[ -z $rpath ]] && continue + candidate_rpaths+=("$rpath") + case "$rpath" in + @*) ;; + *) + echo "External rpath in $candidate: $rpath" >&2 + dependency_errors=1 + ;; + esac + done < <(otool -l "$candidate" | awk '/cmd LC_RPATH/ { getline; getline; print $2 }') + + while IFS= read -r dependency; do + [[ -n $install_name && $dependency == "$install_name" ]] && continue + case "$dependency" in + @executable_path/*|@loader_path/*) + resolved_dependency=$(expand_runtime_path "$dependency" "$loader_dir") + if [[ ! -e $resolved_dependency ]]; then + echo "Unresolved dependency in $candidate: $dependency" >&2 + dependency_errors=1 + fi + ;; + @rpath/*) + dependency_suffix=${dependency#@rpath/} + dependency_found=0 + for rpath in "${candidate_rpaths[@]}" "${executable_rpaths[@]}"; do + [[ -z $rpath ]] && continue + expanded_rpath=$(expand_runtime_path "$rpath" "$loader_dir") + if [[ -e "$expanded_rpath/$dependency_suffix" ]]; then + dependency_found=1 + break + fi + done + if [[ $dependency_found -eq 0 ]]; then + echo "Unresolved dependency in $candidate: $dependency" >&2 + dependency_errors=1 + fi + ;; + /System/Library/*|/usr/lib/*) ;; + @*) + echo "Unsupported loader token in $candidate: $dependency" >&2 + dependency_errors=1 + ;; + *) + echo "External dependency in $candidate: $dependency" >&2 + dependency_errors=1 + ;; + esac + done < <(otool -L "$candidate" | awk 'NR > 1 { print $1 }') +done < <(find "$contents" -type f -print0) +[[ $dependency_errors -eq 0 ]] || fail "bundle contains dependencies outside the application or macOS" + +while IFS= read -r -d '' symlink; do + [[ -e $symlink ]] || fail "bundle contains a broken symlink: $symlink" +done < <(find "$app" -type l -print0) + +for private_path in "${SOURCE_ROOT:-}" "${HOME:-}"; do + [[ -n $private_path && $private_path != / ]] || continue + if LC_ALL=C grep -R -a -F -q -- "$private_path" "$app"; then + fail "bundle contains a private build path" + fi +done + +if [[ -n ${SOURCE_BRANCH:-} ]] && LC_ALL=C grep -R -a -F -q -- "$SOURCE_BRANCH" "$app"; then + fail "bundle contains the local source branch name" +fi + +if find "$app" \( -name '.DS_Store' -o -name '._*' \) -print -quit | grep -q .; then + fail "bundle contains Finder or AppleDouble metadata" +fi + +if find "$app" \ + \( -iname '*.log' \ + -o -iname 'accounts.json' \ + -o -iname 'launcher_accounts.json' \ + -o -iname 'pollymc.cfg' \ + -o -iname 'prismlauncher.cfg' \ + -o -iname 'instance.cfg' \ + -o -type d -iname 'logs' \ + -o -type d -iname 'instances' \ + -o -type d -iname 'crash-reports' \) \ + -print -quit | grep -q .; then + fail "bundle contains local launcher data or logs" +fi + +codesign --verify --deep --strict --verbose=2 "$app" +echo "Verified portable macOS bundle: $app ($bundle_version)" diff --git a/program_info/AdhocSignedApp.entitlements b/program_info/AdhocSignedApp.entitlements new file mode 100644 index 0000000..032308a --- /dev/null +++ b/program_info/AdhocSignedApp.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + diff --git a/program_info/App.entitlements b/program_info/App.entitlements new file mode 100644 index 0000000..73bf832 --- /dev/null +++ b/program_info/App.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + + diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 0000000..2081163 --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,20 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "2d6a6cf3ac9a7cc93942c3d289a2f9c661a6f4a7", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ], + "overlay-ports": [ + "./cmake/vcpkg-ports" + ], + "overlay-triplets": [ + "./cmake/vcpkg-triplets" + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..aad72d6 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,30 @@ +{ + "dependencies": [ + { + "name": "ecm", + "host": true + }, + { + "name": "libqrencode", + "default-features": false + }, + { + "name": "pkgconf", + "host": true + }, + "cmark", + { + "name": "libarchive", + "default-features": false, + "features": [ + "bzip2", + "lz4", + "lzma", + "lzo", + "zstd" + ] + }, + "tomlplusplus", + "zlib" + ] +}