├── .circleci └── config.yml ├── .gitignore ├── __main__.py ├── build_dmg.py ├── download_qt.sh ├── patch_qt.sh └── templates └── Info.plist.tmpl /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | macos: 5 | xcode: "9.0.1" 6 | working_directory: /Users/distiller/project 7 | steps: 8 | - checkout 9 | - run: 10 | name: Update brew 11 | command: brew update && brew upgrade python 12 | - run: 13 | name: Install brew dependencies 14 | command: brew install cmake p7zip libzip libuv protobuf 15 | - run: 16 | name: Install python dependencies 17 | command: pip3 install jinja2 ds_store 18 | - run: 19 | name: Download Qt 20 | command: ./download_qt.sh 21 | - run: 22 | name: Build 23 | command: python3 __main__.py --qt-path qt/*/*/ --update-url https://mcpelauncher.mrarm.io/ver/osx --build-id $CIRCLE_BUILD_NUM 24 | - run: 25 | name: Build .dmg 26 | command: python3 build_dmg.py 27 | - store_artifacts: 28 | path: output/Minecraft Bedrock Launcher.dmg 29 | destination: /Minecraft Bedrock Launcher.dmg 30 | - run: 31 | name: Upload the .dmg to website 32 | command: 'curl -X POST -u "ci:$WEBSITE_CI_UPLOAD_PASS" --data-binary "@output/Minecraft Bedrock Launcher.dmg" -H "X-Type: osx-dmg" https://mcpelauncher.mrarm.io/api/v1/upload' 33 | - run: 34 | name: Update version number 35 | command: 'curl -X POST -u "ci:$WEBSITE_CI_UPLOAD_PASS" -F "channel=osx" -F "build_id=$CIRCLE_BUILD_NUM" https://mcpelauncher.mrarm.io/api/v1/version' 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | source/ 3 | qt/ 4 | qt_tmp/ 5 | .vscode/ 6 | .idea/ 7 | __pycache__/ 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | from os import makedirs, path, cpu_count, listdir 2 | from subprocess import check_call as call 3 | import subprocess 4 | from shutil import rmtree, copyfile, copytree 5 | import shutil 6 | import argparse 7 | from jinja2 import Template 8 | 9 | # Change for each release 10 | VERSION = '0.0.1' 11 | 12 | TEMPLATES_DIR = 'templates' 13 | OUTPUT_DIR = 'output' 14 | SOURCE_DIR = 'source' 15 | APP_OUTPUT_NAME = 'Minecraft Bedrock Launcher.app' 16 | APP_OUTPUT_DIR = path.join(OUTPUT_DIR, APP_OUTPUT_NAME) 17 | 18 | ENABLE_COLORS=True 19 | 20 | 21 | def display_stage(name): 22 | if ENABLE_COLORS: 23 | print("\x1B[1m\x1B[32m=> " + name + "\x1B[0m") 24 | else: 25 | print(name) 26 | 27 | 28 | parser = argparse.ArgumentParser() 29 | parser.add_argument('--qt-path', help='Specify the Qt installation path', required=True) 30 | parser.add_argument('--update-url', help='Enable checking updates in the metalauncher from the specified URL') 31 | parser.add_argument('--build-id', help='Specify the build ID for update checking purposes') 32 | parser.add_argument('--force', help='Always remove the output directory', action='store_true') 33 | args = parser.parse_args() 34 | 35 | if path.exists(path.join(OUTPUT_DIR)): 36 | if not args.force: 37 | print('Removing `{}/`! Click enter to continue, or ^C to exit'.format(OUTPUT_DIR)) 38 | input() 39 | rmtree(OUTPUT_DIR) 40 | 41 | display_stage("Initializing") 42 | makedirs(path.join(APP_OUTPUT_DIR, 'Contents', 'Resources')) 43 | makedirs(path.join(APP_OUTPUT_DIR, 'Contents', 'Frameworks')) 44 | makedirs(path.join(APP_OUTPUT_DIR, 'Contents', 'MacOS')) 45 | if not path.isdir(SOURCE_DIR): 46 | makedirs(SOURCE_DIR) 47 | 48 | # Download .icns file 49 | ICON_FILE = path.join(SOURCE_DIR, 'minecraft.icns') 50 | if not path.exists(ICON_FILE): 51 | display_stage("Downloading icons file") 52 | call(['curl', '-sL', '-o', ICON_FILE, 'https://github.com/minecraft-linux/mcpelauncher-proprietary/raw/master/minecraft.icns']) 53 | copyfile(ICON_FILE, path.join(APP_OUTPUT_DIR, 'Contents', 'Resources', 'minecraft.icns')) 54 | 55 | # Download the sources 56 | def clone_repo(name, url): 57 | display_stage("Cloning repository: " + url) 58 | directory = path.join(SOURCE_DIR, name) 59 | if not path.isdir(directory): 60 | call(['git', 'clone', '--recursive', url, directory]) 61 | else: 62 | call(['git', 'pull'], cwd=directory) 63 | call(['git', 'submodule', 'update'], cwd=directory) 64 | 65 | display_stage("Downloading sources") 66 | clone_repo('msa', 'https://github.com/minecraft-linux/msa-manifest.git') 67 | clone_repo('mcpelauncher', 'https://github.com/minecraft-linux/mcpelauncher-manifest.git') 68 | clone_repo('mcpelauncher-ui', 'https://github.com/minecraft-linux/mcpelauncher-ui-manifest.git') 69 | 70 | # Build 71 | # QT_INSTALL_PATH = subprocess.check_output(['brew', '--prefix', 'qt']).decode('utf-8').strip() 72 | QT_INSTALL_PATH = path.abspath(args.qt_path) 73 | CMAKE_INSTALL_PREFIX = path.abspath(path.join(SOURCE_DIR, "install")) 74 | CMAKE_QT_EXTRA_OPTIONS = ["-DCMAKE_PREFIX_PATH=" + QT_INSTALL_PATH, '-DCMAKE_INSTALL_RPATH_USE_LINK_PATH=ON'] 75 | 76 | if not path.isdir(CMAKE_INSTALL_PREFIX): 77 | makedirs(CMAKE_INSTALL_PREFIX) 78 | 79 | def build_component(name, cmake_opts): 80 | display_stage("Building: " + name) 81 | source_dir = path.abspath(path.join(SOURCE_DIR, name)) 82 | build_dir = path.join(SOURCE_DIR, "build", name) 83 | if not path.isdir(build_dir): 84 | makedirs(build_dir) 85 | call(['cmake', source_dir, '-DCMAKE_INSTALL_PREFIX=' + CMAKE_INSTALL_PREFIX] + cmake_opts, cwd=build_dir) 86 | call(['make', '-j' + str(cpu_count()), 'install'], cwd=build_dir) 87 | 88 | VERSION_OPTS = [] 89 | if args.update_url and args.build_id: 90 | VERSION_OPTS = ["-DENABLE_UPDATE_CHECK=ON", "-DUPDATE_CHECK_URL=" + args.update_url, "-DUPDATE_CHECK_BUILD_ID=" + args.build_id] 91 | 92 | display_stage("Building") 93 | build_component("msa", ['-DENABLE_MSA_QT_UI=ON', '-DMSA_UI_PATH_DEV=OFF'] + CMAKE_QT_EXTRA_OPTIONS) 94 | build_component("mcpelauncher", ['-DMSA_DAEMON_PATH=.', '-DENABLE_DEV_PATHS=OFF']) 95 | build_component("mcpelauncher-ui", ['-DGAME_LAUNCHER_PATH=.'] + VERSION_OPTS + CMAKE_QT_EXTRA_OPTIONS) 96 | 97 | display_stage("Copying files") 98 | def copy_installed_files(from_path, to_path): 99 | for f in listdir(from_path): 100 | print("Copying file: " + f) 101 | if path.isdir(path.join(from_path, f)): 102 | copytree(path.join(from_path, f), path.join(to_path, f)) 103 | else: 104 | shutil.copy2(path.join(from_path, f), path.join(to_path, f)) 105 | 106 | copy_installed_files(path.join(CMAKE_INSTALL_PREFIX, 'bin'), path.join(APP_OUTPUT_DIR, 'Contents', 'MacOS')) 107 | copy_installed_files(path.join(CMAKE_INSTALL_PREFIX, 'share'), path.join(APP_OUTPUT_DIR, 'Contents', 'Resources')) 108 | 109 | display_stage("Building Info.plist file") 110 | with open(path.join(TEMPLATES_DIR, 'Info.plist.tmpl'), 'r') as raw: 111 | info = Template(raw.read()) 112 | output = info.render( 113 | cf_bundle_identifier = 'io.mrarm.mcpelauncher.ui', 114 | cf_bundle_executable = 'mcpelauncher-ui-qt', 115 | cf_bundle_get_info_string = 'Minecraft Bedrock Launcher', 116 | cf_bundle_icon_file = 'minecraft', 117 | cf_bundle_name = 'Minecraft Bedrock Launcher', 118 | cf_bundle_version = VERSION 119 | ) 120 | 121 | f = open(path.join(APP_OUTPUT_DIR, 'Contents', 'Info.plist'), 'w') 122 | f.write(output) 123 | f.close() 124 | 125 | display_stage("Copying Qt libraries") 126 | QT_DEPLOY_OPTIONS = [path.join(QT_INSTALL_PATH, 'bin', 'macdeployqt'), APP_OUTPUT_DIR] 127 | QT_DEPLOY_OPTIONS.append('-qmldir=' + path.join(SOURCE_DIR, 'mcpelauncher-ui', 'mcpelauncher-ui-qt')) 128 | QT_DEPLOY_OPTIONS.append('-executable=' + path.abspath(path.join(APP_OUTPUT_DIR, 'Contents', 'MacOS', 'mcpelauncher-ui-qt'))) 129 | QT_DEPLOY_OPTIONS.append('-executable=' + path.abspath(path.join(APP_OUTPUT_DIR, 'Contents', 'MacOS', 'msa-ui-qt'))) 130 | call(QT_DEPLOY_OPTIONS) 131 | 132 | display_stage('App bundle has been built at {}!'.format(path.join(OUTPUT_DIR, APP_OUTPUT_NAME))) 133 | -------------------------------------------------------------------------------- /build_dmg.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_call as call 2 | from os import path, makedirs, symlink, rmdir, remove, rename 3 | from shutil import copyfile, copytree 4 | from ds_store import DSStore 5 | from pprint import pprint 6 | from mac_alias import Alias 7 | import os 8 | 9 | SOURCE_DIR = 'source' 10 | OUTPUT_DIR = 'output' 11 | DMG_OUTPUT_NAME = 'Minecraft Bedrock Launcher.dmg' 12 | DMG_OUTPUT_PATH = path.join(OUTPUT_DIR, DMG_OUTPUT_NAME) 13 | DMG_MOUNT_PATH = '/Volumes/Minecraft Bedrock Launcher/' 14 | APP_OUTPUT_NAME = 'Minecraft Bedrock Launcher.app' 15 | APP_OUTPUT_DIR = path.join(OUTPUT_DIR, APP_OUTPUT_NAME) 16 | 17 | VOL_NAME = 'Minecraft Bedrock Launcher' 18 | 19 | if path.exists(DMG_OUTPUT_PATH): 20 | remove(DMG_OUTPUT_PATH) 21 | 22 | BG_FILE = path.join(SOURCE_DIR, 'dmg-background.tif') 23 | if not path.exists(BG_FILE): 24 | call(['curl', '-sL', '-o', BG_FILE, 'https://mrarm.io/u/dmg-background.tif']) 25 | 26 | # we assume here that sectors are 512B 27 | IMAGE_SECTOR_SIZE = 512 28 | 29 | 30 | def calc_size(p): 31 | ret = 0 32 | for entry in os.scandir(p): 33 | if not entry.is_symlink(): 34 | ret += (entry.stat().st_size + IMAGE_SECTOR_SIZE - 1) // IMAGE_SECTOR_SIZE 35 | ret += 16 + 4 * 8 36 | if entry.is_dir(): 37 | ret += calc_size(entry.path) 38 | return ret 39 | 40 | image_sectors = calc_size(APP_OUTPUT_DIR) + 1024 * 1024 * 4 // IMAGE_SECTOR_SIZE 41 | try: 42 | makedirs('empty') 43 | call(['hdiutil', 'create', '-fs', 'HFS+', '-format', 'UDRW', '-sectors', str(image_sectors), '-volname', VOL_NAME, 44 | '-srcfolder', 'empty', '-quiet', DMG_OUTPUT_PATH]) 45 | finally: 46 | rmdir('empty') 47 | call(['hdiutil', 'attach', '-noautoopen', '-mountpoint', DMG_MOUNT_PATH, '-quiet', DMG_OUTPUT_PATH]) 48 | 49 | symlink("/Applications", path.join(DMG_MOUNT_PATH, "Applications")) 50 | copytree(APP_OUTPUT_DIR, path.join(DMG_MOUNT_PATH, APP_OUTPUT_NAME), symlinks=True) 51 | copyfile(BG_FILE, path.join(DMG_MOUNT_PATH, ".background.tif")) 52 | 53 | with DSStore.open(path.join(DMG_MOUNT_PATH, '.DS_Store'), 'w+') as d: 54 | d['Minecraft Bedrock Launcher.app']['Iloc'] = (152, 220 - 15) 55 | d['Applications']['Iloc'] = (488, 220 - 10) 56 | d['.']['bwsp'] = { 57 | 'ShowStatusBar': False, 58 | 'ShowPathbar': False, 59 | 'ShowToolbar': False, 60 | 'ShowTabView': False, 61 | 'ContainerShowSidebar': False, 62 | 'WindowBounds': '{{100, 350}, {640, 422}}', 63 | 'ShowSidebar': False 64 | } 65 | d['.']['icvp'] = { 66 | 'backgroundColorRed': 1.0, 67 | 'backgroundColorGreen': 1.0, 68 | 'backgroundColorBlue': 1.0, 69 | 'iconSize': 64.0, 70 | 'backgroundImageAlias': Alias.for_file(path.join(DMG_MOUNT_PATH, '.background.tif')).to_bytes(), 71 | 'textSize': 13.0, 72 | 'backgroundType': 2, 73 | 'gridOffsetX': 0.0, 74 | 'gridOffsetY': 0.0, 75 | 'showItemInfo': False, 76 | 'viewOptionsVersion': 1, 77 | 'arrangeBy': 'none', 78 | 'labelOnBottom': True, 79 | 'showIconPreview': True, 80 | 'gridSpacing': 50.0 81 | } 82 | 83 | call(['hdiutil', 'detach', '-quiet', DMG_MOUNT_PATH]) 84 | rename(DMG_OUTPUT_PATH, DMG_OUTPUT_PATH + '.tmp') 85 | call(['hdiutil', 'convert', '-format', 'UDZO', '-imagekey', 'zlib-level=6', '-o', DMG_OUTPUT_PATH, '-quiet', 86 | DMG_OUTPUT_PATH + '.tmp']) 87 | remove(DMG_OUTPUT_PATH + '.tmp') 88 | -------------------------------------------------------------------------------- /download_qt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | QT_BASE_URL=http://download.qt.io/online/qtsdkrepository/mac_x64/desktop/qt5_5112/ 4 | QT_VERSION_SHORT=5.11.2 5 | QT_VERSION=5.11.2-0-201809141947 6 | QT_PACKAGE_PREFIX=qt.qt5.5112. 7 | QT_PACKAGE_SUFFIX=clang_64 8 | QT_PREBUILT_SUFFIX=-MacOS-MacOS_10_12-Clang-MacOS-MacOS_10_12-X86_64 9 | 10 | COLOR_STATUS=$'\033[1m\033[32m' 11 | COLOR_RESET=$'\033[0m' 12 | 13 | function install_module() { 14 | echo "${COLOR_STATUS}Downloading $2${COLOR_RESET}" 15 | remote_sha1=$(curl "$2.sha1") 16 | curl -L -C - -o $1 $2 17 | local_sha1=$(shasum $1 | cut -d " " -f 1) 18 | if [[ "$remote_sha1" != "$local_sha1" ]]; then 19 | echo "$1: sha1 mismatch - local: $local_sha1; remote: $remote_sha1" 20 | exit 1 21 | fi 22 | 7z x -y -oqt/ $1 23 | } 24 | function install_module_main() { 25 | url_base="$QT_BASE_URL$QT_PACKAGE_PREFIX$QT_PACKAGE_SUFFIX/$QT_VERSION$1$QT_PREBUILT_SUFFIX" 26 | install_module "qt_tmp/$1.7z" "$url_base.7z" 27 | } 28 | function install_module_extra() { 29 | url_base="$QT_BASE_URL$QT_PACKAGE_PREFIX$1.$QT_PACKAGE_SUFFIX/$QT_VERSION$2$QT_PREBUILT_SUFFIX" 30 | install_module "qt_tmp/$2.7z" "$url_base.7z" 31 | } 32 | 33 | mkdir -p qt 34 | mkdir -p qt_tmp 35 | 36 | install_module_main qtbase 37 | install_module_main qtdeclarative 38 | install_module_main qtgraphicaleffects 39 | install_module_main qtsvg 40 | install_module_main qtquickcontrols 41 | install_module_main qtquickcontrols2 42 | install_module_main qtwebchannel 43 | install_module_main qttools 44 | install_module_main qtlocation 45 | install_module_extra qtwebengine qtwebengine 46 | 47 | echo "${COLOR_STATUS}Patching$2${COLOR_RESET}" 48 | cd qt/$QT_VERSION_SHORT/clang_64/ 49 | ../../../patch_qt.sh 50 | -------------------------------------------------------------------------------- /patch_qt.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # TODO: Qt's installer also patches the paths in several other places - should we also do that? 4 | 5 | printf "[Paths]\nPrefix=..\n" > bin/qt.conf 6 | 7 | sed -i.bak 's/QT_EDITION = .*/QT_EDITION = OpenSource/' mkspecs/qconfig.pri 8 | sed -i.bak 's/QT_LICHECK = licheck_mac/QT_LICHECK =/' mkspecs/qconfig.pri 9 | rm mkspecs/qconfig.pri.bak 10 | -------------------------------------------------------------------------------- /templates/Info.plist.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSHighResolutionCapable 6 | 7 | CFBundleIdentifier 8 | {{ cf_bundle_identifier }} 9 | CFBundleExecutable 10 | {{ cf_bundle_executable }} 11 | CFBundleGetInfoString 12 | {{ cf_bundle_get_info_string }} 13 | CFBundleIconFile 14 | {{ cf_bundle_icon_file }} 15 | CFBundleName 16 | {{ cf_bundle_name }} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleVersion 20 | {{ cf_bundle_version }} 21 | 22 | 23 | --------------------------------------------------------------------------------