diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f07faabc3..de6f30412 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,11 +4,11 @@ on: push: tags: - 'v*' + workflow_dispatch: env: - IS_STABLE: ${{ !contains(github.ref, '-') }} + IS_STABLE: ${{ github.ref_type == 'tag' && !contains(github.ref, '-') }} IS_PRERELEASE: ${{ contains(github.ref_name, 'pre') }} - SHOULD_RELEASE: ${{ !contains(github.ref, '-') || contains(github.ref_name, 'pre') }} GRADLE_OPTS: -Dorg.gradle.vfs.watch=false FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true SENTRY_BACKEND: none @@ -16,6 +16,9 @@ env: jobs: build: runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash strategy: fail-fast: false matrix: @@ -100,7 +103,25 @@ jobs: if: matrix.platform == 'linux' run: | sudo apt-get update - sudo apt-get install -y libcurl4-openssl-dev ninja-build libgtk-3-dev rpm + sudo apt-get install -y \ + libcurl4-openssl-dev \ + ninja-build \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + libkeybinder-3.0-dev \ + locate \ + p7zip \ + ${{ matrix.arch == 'amd64' && 'rpm patchelf libfuse2' || '' }} + + _arch=${{ matrix.arch == 'amd64' && 'x86_64' || 'aarch64' }} + wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${_arch}.AppImage + chmod +x appimagetool + sudo mv appimagetool /usr/local/bin/ + + - name: Install Dependencies (macOS) + if: matrix.platform == 'macos' + run: | + npm install -g appdmg - name: Setup Golang uses: actions/setup-go@v6 @@ -136,7 +157,14 @@ jobs: run: | flutter pub get dart run build_runner build -d - dart setup.dart ${{ matrix.platform }} --arch ${{ matrix.arch }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} ${{ matrix.compatible == true && '--compatible' || '' }} + dart setup.dart \ + ${{ matrix.platform }} \ + --arch ${{ matrix.arch }} \ + ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} \ + ${{ matrix.compatible == true && '--compatible' || '' }} \ + ${{ matrix.platform == 'linux' && '--targets' || ''}} \ + ${{ matrix.platform == 'linux' && matrix.arch == 'amd64' && 'deb,rpm,appimage' || '' }} \ + ${{ matrix.platform == 'linux' && matrix.arch == 'arm64' && 'deb' || '' }} - name: Upload Artifacts uses: actions/upload-artifact@v6 @@ -178,7 +206,7 @@ jobs: fi - name: Release - if: ${{ env.SHOULD_RELEASE == 'true' }} + if: ${{ env.IS_STABLE == 'true' || env.IS_PRERELEASE == 'true' }} uses: softprops/action-gh-release@v3.0.0 with: files: ./dist/* diff --git a/.gitignore b/.gitignore index b49b58431..e9a07d9f4 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,7 @@ migrate_working_dir/ /specs/ /dist/ .test/ -.vscode/ +.vscode/* /services/helper/target/ # Symbolication related @@ -60,7 +60,14 @@ app.*.map.json #jniLibs /android/app/src/main/jniLibs/ +/android/core/src/main/jniLibs/ # FVM Version Cache .fvm/ -.fvmrc \ No newline at end of file + +# Version Managers +.fvmrc +mise.toml + +# VS Code Configs +!.vscode/launch.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..c2d5640eb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,34 @@ +{ + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Bettbox", + "request": "launch", + "type": "dart", + "args": [ + "--dart-define-from-file=libclash/windows/.core_sha256" + ] + }, + { + "name": "Bettbox (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "args": [ + "--dart-define-from-file=libclash/windows/.core_sha256" + ] + }, + { + "name": "Bettbox (release mode)", + "request": "launch", + "type": "dart", + "flutterMode": "release", + "args": [ + "--dart-define-from-file=libclash/windows/.core_sha256" + ] + } + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index 627966f05..101908c8f 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,56 @@ -android_arm64: - dart ./setup.dart android --arch arm64 -macos_arm64: +macos_app: + dart ./setup.dart macos + +macos_app_arm64: dart ./setup.dart macos --arch arm64 -android_app: + +macos_app_amd64: + dart ./setup.dart macos --arch amd64 + +macos_app_universal: + dart .setup.dart macos --arch universal + +macos_core: + dart ./setup.dart macos --out core + +macos_core_arm64: + dart ./setup.dart macos --arch arm64 --out core + +macos_core_amd64: + dart ./setup.dart macos --arch amd64 --out core + +android_app_all: dart ./setup.dart android -android_arm64_core: + +android_app_arm64: + dart ./setup.dart android --arch arm64 + +android_app_universal: + dart ./setup.dart android --arch universal + +android_core_arm64: dart ./setup.dart android --arch arm64 --out core -macos_arm64_core: - dart ./setup.dart macos --arch arm64 --out core \ No newline at end of file + +windows_core_and_helper: + dart ./setup.dart windows --out core,helper + +windows_core_and_helper_amd64_compatible: + dart ./setup.dart windows --out core,helper --arch amd64 --compatible + +windows_app: + dart ./setup.dart windows + +windows_app_amd64_compatible: + dart ./setup.dart windows --arch amd64 --compatible + +linux: + dart ./setup.dart linux --build-only + +linux_amd64_compatible: + dart ./setup.dart linux --arch amd64 --compatible --build-only + +linux_core: + dart ./setup.dart linux --out core + +linux_core_amd64_compatible: + dart ./setup.dart linux --out core --arch amd64 --compatible diff --git a/README.md b/README.md index dad1fda2f..cd8f50f6f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

⚡ Bettbox

+

icon Bettbox

Another Better Mihomo Client

@@ -13,30 +13,16 @@ Bettbox意为: Better Experience, Out of the box,卓越体验,亦可开箱 --- - - -## 🛠️ 安装与下载 - -请前往 [Releases](https://github.com/appshubcc/Bettbox/releases) 页面下载最新适合您平台和系统的安装包 - -* **桌面端**: Windows (x64/arm64), macOS (Intel/Apple Silicon), Linux (x64/arm64) -* **安卓端**: Android (ARMv8/ ARMv7/ x86_64/ Universal) -* **Android TV**: 已支持,可选ARMv7 32位 -* **Windows7**: 请配合 [[VxKex]](https://github.com/i486/VxKex/releases) 使用 -* **鸿蒙NEXT**: 请配合 [[卓易通]](https://harmonyos.cool/android-app) 使用 - ---- -### ✈️ Telegram 社区交流 - - +## 💬 社区交流
[![Telegram Group](https://img.shields.io/badge/Appshub-Chat-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_chat) [![Telegram Channel](https://img.shields.io/badge/Appshub-Channel-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white)](https://t.me/appshub_channel) ----
+--- + ## 🚀 核心特性 ### 深度体验优化 @@ -63,7 +49,285 @@ Bettbox意为: Better Experience, Out of the box,卓越体验,亦可开箱 --- -## 常见问题 +## 🛠️ 安装与下载 + +请前往 [Releases](https://github.com/appshubcc/Bettbox/releases) 页面下载最新适合您平台和系统的安装包 + +* **桌面端**: Windows (x64/arm64), macOS (Intel/Apple Silicon), Linux (x64/arm64) +* **安卓端**: Android (ARMv8/ ARMv7/ x86_64/ Universal) +* **Android TV**: 已支持,可选ARMv7 32位 +* **Windows7**: 请配合 [[VxKex]](https://github.com/i486/VxKex/releases) 使用 +* **鸿蒙NEXT**: 请配合 [[卓易通]](https://harmonyos.cool/android-app) 使用 + +--- + +## 🏗️ 从源码构建 + +
+ +### 开发环境 + +| |版本|备注| +|---|---|---| +|Flutter|3.44.0|| +|Go|1.25.x|1.20.x for old CPUs| +|Java|temurin-17.x|| +|Android SDK|36.1|| +|Android NDK|27.0.12077973
28.2.13676358|| +|Rust|Latest stable|For Windows helper service| + +### Windows + +* Toolchain: Flutter, Golang, Cargo(for helper service), Visual C++ Build Tools +* 打包: Inno Setup + +1. 克隆本仓库 + + ```powershell + git clone https://github.com/appshubcc/Bettbox.git + cd Bettbox + ``` + +2. 配置开发环境 + + * Flutter + + * 从 git 仓库安装 + + ```powershell + $installPath = "C:\tools" # 根据你的喜好替换 + cd $installPath + git clone -b 3.44.0 https:github.com/flutter/flutter.git + $newPath = "$env:Path;$installPath\flutter\bin" + [Environment]::SetEnvironmentVariable("Path", "$newPath", "User") + cd - + ``` + * 使用 [FVM](https://fvm.app/documentation/getting-started/installation) 或 [mise](https://mise.jdx.dev/installing-mise.html) 安装 + ```powershell + # FVM + fvm use 3.44.0 + # mise + mise use flutter@3.44.0 + ``` + * Golang + * 从[官网](https://go.dev/dl/)下载 + + * 使用 mise 安装 + + ```powershell + mise use go@1.20 # For compatibility for old CPUs + ``` + * Rust (Cargo) + * 使用 winget 安装 + + ```powershell + winget install Rustlang.Rustup + ``` +3. 配置构建环境 + * Visual C++ Build Tools + + 下载 [Visual Studio 2022 (任意版本或生成工具) 引导程序](https://learn.microsoft.com/zh-cn/visualstudio/releases/2022/release-history#evergreen-bootstrappers) [^1] ,勾选“使用 C++ 的桌面开发”并安装 + + * Inno Setup (ISCC.exe) [^2] + + ```powershell + winget install JRSoftware.InnoSetup --scope machine + ``` + + * 运行 `flutter doctor` 检查构建环境完整性 + +4. 构建并打包 [^3] [^4] + + ```powershell + flutter pub get + dart run build_runner build -d + dart setup.dart windows --compatible + ``` + +### Linux + +* Toolchain: Flutter, Golang, Clang, Cmake, Ninja, pkg-config +* 依赖库: libcurl4, gtk3, libayatana-appindicator, libkeybinder3, libfuse2(for appimagetool) +* 打包: dpkg-deb, rpm, appimagetool, patchelf(for rpm), locate(for appimagetool) + +1. 克隆本仓库 + + ```bash + git clone https://github.com/appshubcc/Bettbox.git + cd Bettbox + ``` +2. 配置开发环境 + + * Flutter + * 使用 [FVM](https://fvm.app/documentation/getting-started/installation) 安装 + + ```bash + fvm use 3.44.0 + ``` + + * Golang + * 使用包管理器安装 + + ```bash + sudo apt install golang-go + ``` + + * 使用 [mise](https://mise.jdx.dev/installing-mise.html) 安装 + + ```bash + mise use go@1.20 # For compatibility for old CPUS + ``` + +3. 安装构建依赖 + + ```bash + # 按需安装 + sudo apt install build-essential clang cmake ninja-build + sudo apt install libcurl4-openssl-dev libgtk-3-dev lbayatana-appindicator3-dev libkeybinder-3.0-dev libfuse2 + sudo apt install dpkg-deb rpm patchelf locate + wget https://github.com/Appimage/AppimageKit/releases/download/containuous/appimagetool-x86_64.AppImage + chmod +x appimagetool + sudo mv appimagetool /usr/local/bin/ + ``` + +4. 构建并打包 [^3] [^4] + + ```bash + flutter pub get + dart run build_runner build -d + # 按需填写 "--targets" 参数,使用 "," 分隔 + dart setup.dart linux --targets=deb,rpm,appimage --compatible + ``` + +### Mac OS + +* Toolchain: Flutter, Golang, Xcode command-line tools, CocoaPods +* 打包: appdmg + +1. 克隆本仓库 + + ```zsh + git clone https://github.com/appshubcc/Bettbox.git + cd Bettbox + ``` +2. 配置开发环境 + + * Flutter + * 使用 [FVM](https://fvm.app/documentation/getting-started/installation) 安装 + + ```zsh + fvm use 3.44.0 + ``` + + * Golang + + * 使用包管理器安装 + + ```zsh + brew install go + ``` + * 使用 [mise](https://mise.jdx.dev/installing-mise.html) 安装 + + ```zsh + mise use go@1.20 # For compatibility for old CPUS + ``` + * Xcode, CocoaPods + + 参见 [Flutter 官方文档](https://docs.flutter.dev/platform-integration/macos/setup) + + * appdmg + + ```zsh + npm install appdmg + ``` + +3. 构建并打包 [^3] [^4] + + ```zsh + flutter pub get + dart run build_runner build -d + dart setup.dart macos + ``` + +### Android + +* Toolchain: Flutter, Golang, CMake, Android SDK, Android SDK Build-Tools, Android SDK Command-line Tools(optional: independent sdkmanager), Android SDK Platform-Tools, Android NDK + +1. 配置编程语言 + + * Flutter, Golang + + 参见上文 + + * Java + + * 使用 [mise](https://mise.jdx.dev/installing-mise.html) 安装 + + ```bash + mise use java@temurin-17 + ``` + + * 在 Android Studio 中配置 + +2. 配置构建环境 + + * Android SDK, NDK and Tools + + * 使用 Android Studio 配置 + + 参见 [Flutter 官方文档](https://docs.flutter.dev/platform-integration/android/setup) + + * 从命令行安装 (以 Linux 平台为例) + + ```bash + # 配置环境变量 + echo 'export ANDROID_HOME=$HOME/.local/opt/android-sdk' >> ~/.bashrc + echo 'export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools' >> ~/.bashrc + sourec ~/.bashrc + + # 安装 SDK, NDK, Tools + ## 使用 sdkmanager.py + sudo apt install sdkmanager + sdkmanager --install "build-tools;36.0.0" "cmdline-tools;latest" "platform-tools" "platforms;android-36.1" "ndk;27.0.12077973" "ndk;28.2.13676358" + + # 接受许可证 + flutter doctor --android-lincenses + ``` + + * 配置 Keystore + + 1. 生成新的 Keystore 或将已有 Keystore 导入至 `./android/app/keystore.jks` + 2. (无 Android Studio) 新建或打开 `./android/local.properties`, 添加以下参数 + + ```properties + keyAlias=<密钥别名> + storePassword= + keyPassword=<密钥密码> + ``` + * 运行 `flutter doctor` 检查构建环境完整性 + +3. 构建并打包 [^4] + + ```bash + flutter pub get + dart run build-runner build -d + dart setup.dart android --arch=universal + ``` + +[^1]: Flutter ≤3.38.2 与 Visual Studio 2026 不兼容,更高版本请忽略 + + +[^2]: 当前版本 flutter_distributor (v0.4.2) 硬编码了 Inno Setup 的安装路径 (`C:\Program Files (x86)\Inno Setup 6`), 务必以管理员权限安装 + +[^3]: 使用 `--compatible` 参数为[旧 CPU](https://go.dev/wiki/MinimumRequirements#amd64) 构建 + +[^4]: 运行 `dart setup.dart help ` 查看更多选项 + +
+ +--- + +## 💡 常见问题 1. 安装后无法启动? - 安卓端旧设备,请检查是否支持Bettbox的最低系统要求:Android 8.0+ @@ -85,23 +349,13 @@ Bettbox意为: Better Experience, Out of the box,卓越体验,亦可开箱 --- -## 开发构建 - -以Windows为例: - -* 你需要一台Windows设备(≥Windows 10) -* 必要的软件依赖:Visual Studio,Flutter SDK≥3.35,Golang,Inno Setup,Rust -* dart .\setup.dart windows --arch amd64 --compatible(可选兼容版本) - ---- - -## 赞助链接 +## ☕ 赞助链接 * **暂无**: 所有功能均免费无广告,您可以点击右上角的⭐Star,让开发者获得认同感也是一种支持方式 --- -## 致谢 +## 🤝 致谢 Bettbox 的诞生依赖以下根基项目: @@ -116,4 +370,4 @@ Bettbox 的诞生依赖以下根基项目: ## 📄 开源协议 -延续原项目 GPL-3.0 license 开源协议 +延续原项目 [GPL-3.0 license](LICENSE) 开源协议 diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 359ca5648..fab40bfd2 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -46,6 +46,8 @@ endif() function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_14) target_compile_options(${TARGET} PRIVATE -Wall -Werror) + # Delete this when https://github.com/leanflutter/tray_manager/issues/67 is resolved. + target_compile_options(${TARGET} PRIVATE -Wno-deprecated-declarations) target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") endfunction() diff --git a/pubspec.lock b/pubspec.lock index ff256d753..2b03c6d37 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -598,12 +598,13 @@ packages: source: hosted version: "0.2.3" hotkey_manager_linux: - dependency: transitive + dependency: "direct overridden" description: - name: hotkey_manager_linux - sha256: "83676bda8210a3377bc6f1977f193bc1dbdd4c46f1bdd02875f44b6eff9a8473" - url: "https://pub.dev" - source: hosted + path: "packages/hotkey_manager_linux" + ref: fix-build-failure + resolved-ref: "1b31e3ca4ab017ee72f54a7065b2073a88403d61" + url: "https://github.com/gepbird/hotkey_manager.git" + source: git version: "0.2.0" hotkey_manager_macos: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 5d32e996f..75123949a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,13 @@ dependencies: synchronized: ^3.4.0 flutter_displaymode: ^0.7.0 restart_app: ^1.8.3 +dependency_overrides: + # Fix compile failure: https://github.com/leanflutter/hotkey_manager/pull/66 + hotkey_manager_linux: + git: + url: https://github.com/gepbird/hotkey_manager.git + path: packages/hotkey_manager_linux + ref: fix-build-failure dev_dependencies: flutter_test: sdk: flutter diff --git a/setup.dart b/setup.dart index 1a06b8166..60def7a36 100644 --- a/setup.dart +++ b/setup.dart @@ -7,42 +7,32 @@ import 'package:args/command_runner.dart'; import 'package:crypto/crypto.dart'; import 'package:path/path.dart'; -enum Target { windows, linux, android, macos } +enum TargetPlatform { windows, linux, android, macos } -extension TargetExt on Target { +extension PlatformExt on TargetPlatform { String get os { - if (this == Target.macos) { + if (this == TargetPlatform.macos) { return 'darwin'; } return name; } - bool get same { - if (this == Target.android) { - return true; - } - if (Platform.isWindows && this == Target.windows) { - return true; - } - if (Platform.isLinux && this == Target.linux) { - return true; - } - if (Platform.isMacOS && this == Target.macos) { - return true; - } - return false; + bool get same => name == Platform.operatingSystem; + + bool get buildable { + return same || this == TargetPlatform.android; } String get dynamicLibExtensionName { final String extensionName; switch (this) { - case Target.android || Target.linux: + case TargetPlatform.android || TargetPlatform.linux: extensionName = '.so'; break; - case Target.windows: + case TargetPlatform.windows: extensionName = '.dll'; break; - case Target.macos: + case TargetPlatform.macos: extensionName = '.dylib'; break; } @@ -52,7 +42,7 @@ extension TargetExt on Target { String get executableExtensionName { final String extensionName; switch (this) { - case Target.windows: + case TargetPlatform.windows: extensionName = '.exe'; break; default: @@ -63,34 +53,163 @@ extension TargetExt on Target { } } -enum Mode { core, lib } +enum CoreMode { core, lib } enum Arch { amd64, arm64, arm } +extension ArchExt on Arch { + Map get archMap { + switch (Platform.operatingSystem) { + case 'windows': + return { + 'AMD64': 'amd64', + 'x86': 'amd32', + 'ARM64': 'arm64', + 'ARM': 'arm' + }; + case 'linux' || 'android': + return { + 'x86_64': 'amd64', + 'i386': 'amd32', + 'i486': 'amd32', + 'i586': 'amd32', + 'i686': 'amd32', + 'aarch64': 'arm64', + 'armv5l': 'arm', + 'armv6l': 'arm', + 'armv7l': 'arm' + }; + case 'macos': + return { + 'x86_64': 'amd64', + 'arm64': 'arm64', + 'arm64e': 'arm64' + }; + default: + throw 'Unsupported platform!'; + } + } + + bool get same { + final String hostArchName; + if (Platform.isWindows) { + hostArchName = Platform.environment['PROCESSOR_ARCHITECTURE']!; + } else { + var info = Process.runSync('uname', ['-m']); + hostArchName = info.stdout.toString().trim(); + } + final hostArch = archMap[hostArchName] ?? hostArchName; + return name == hostArch ? true : false; + } +} + class BuildItem { - Target target; - Arch? arch; + TargetPlatform platform; + Arch arch; String? archName; - BuildItem({required this.target, this.arch, this.archName}); + BuildItem({required this.platform, required this.arch, this.archName}); @override String toString() { - return 'BuildLibItem{target: $target, arch: $arch, archName: $archName}'; + return 'BuildLibItem{platform: $platform, arch: $arch, archName: $archName}'; + } +} + +Future checkDeps({ + List? commands, + Map? devLibs, + Map? rtLibs, + List? files, + List? ndks, +}) async { + final missing = []; + + if (devLibs != null && devLibs.isNotEmpty) { + final pkgConfigExists = (await Process.run('which', ['pkg-config'])).exitCode == 0; + if (!pkgConfigExists) { + missing.add('pkg-config'); + } else { + for (final entry in devLibs.entries) { + final result = await Process.run('pkg-config', ['--exists', entry.value]); + if (result.exitCode != 0) missing.add(entry.key); + } + } + } + + if (rtLibs != null && rtLibs.isNotEmpty) { + for (final entry in rtLibs.entries) { + final result = await Process.run('sh', ['-c', 'ldconfig -p | grep ${entry.value}']); + if (result.exitCode != 0) missing.add(entry.key); + } + } + + if (ndks != null && ndks.isNotEmpty) { + final sdkmanager = join(Platform.environment['ANDROID_HOME']!, 'cmdline-tools', 'latest', 'bin', 'sdkmanager'); + final cmdlineToolsExist = File(sdkmanager).existsSync(); + if (!cmdlineToolsExist) { + missing.add('Android SDK Command-line Tools'); + } else { + for (final ndkVersion in ndks) { + final result = await Process.run(sdkmanager, ['--list_installed']); + final pattern = RegExp('^\\s.${RegExp.escape('ndk;$ndkVersion')}', multiLine: true); + final installed = pattern.hasMatch(result.stdout); + if (!installed) { + missing.add('Android NDK $ndkVersion'); + } + } + } + } + + if (commands != null && commands.isNotEmpty) { + for (final cmd in commands) { + final result = Platform.isWindows + ? await Process.run('where.exe', [cmd]) + : await Process.run('which', [cmd]); + if (result.exitCode != 0) { + missing.add(cmd); + } + } + } + + + if (files != null && files.isNotEmpty) { + for (final filePath in files) { + if (!File(filePath).existsSync()) { + missing.add(basename(filePath)); + } + } + } + + if (missing.isNotEmpty) { + throw 'Missing required dependencies: ${missing.join(", ")}. ' + 'Please install them first. See README for details.'; } } class Build { static List get buildItems => [ - BuildItem(target: Target.macos, arch: Arch.arm64), - BuildItem(target: Target.macos, arch: Arch.amd64), - BuildItem(target: Target.linux, arch: Arch.arm64), - BuildItem(target: Target.linux, arch: Arch.amd64), - BuildItem(target: Target.windows, arch: Arch.amd64), - BuildItem(target: Target.windows, arch: Arch.arm64), - BuildItem(target: Target.android, arch: Arch.arm, archName: 'armeabi-v7a'), - BuildItem(target: Target.android, arch: Arch.arm64, archName: 'arm64-v8a'), - BuildItem(target: Target.android, arch: Arch.amd64, archName: 'x86_64'), + BuildItem(platform: TargetPlatform.macos, arch: Arch.arm64), + BuildItem(platform: TargetPlatform.macos, arch: Arch.amd64), + BuildItem(platform: TargetPlatform.linux, arch: Arch.arm64), + BuildItem(platform: TargetPlatform.linux, arch: Arch.amd64), + BuildItem(platform: TargetPlatform.windows, arch: Arch.amd64), + BuildItem(platform: TargetPlatform.windows, arch: Arch.arm64), + BuildItem( + platform: TargetPlatform.android, + arch: Arch.arm, + archName: 'armeabi-v7a', + ), + BuildItem( + platform: TargetPlatform.android, + arch: Arch.arm64, + archName: 'arm64-v8a', + ), + BuildItem( + platform: TargetPlatform.android, + arch: Arch.amd64, + archName: 'x86_64', + ), ]; static String get appName => 'Bettbox'; @@ -107,29 +226,9 @@ class Build { static String get distPath => join(current, 'dist'); - static String _getCc(BuildItem buildItem) { - final environment = Platform.environment; - if (buildItem.target == Target.android) { - final ndk = environment['ANDROID_NDK']; - assert(ndk != null); - final prebuiltDir = Directory( - join(ndk!, 'toolchains', 'llvm', 'prebuilt'), - ); - final prebuiltDirList = prebuiltDir.listSync(); - final map = { - 'armeabi-v7a': 'armv7a-linux-androideabi21-clang', - 'arm64-v8a': 'aarch64-linux-android21-clang', - 'x86': 'i686-linux-android21-clang', - 'x86_64': 'x86_64-linux-android21-clang', - }; - return join(prebuiltDirList.first.path, 'bin', map[buildItem.archName]); - } - return 'gcc'; - } - static String getTags(BuildItem buildItem) { final baseTags = 'with_gvisor'; - if (buildItem.target == Target.android && + if (buildItem.platform == TargetPlatform.android && buildItem.archName == 'armeabi-v7a') { return '$baseTags,with_low_memory'; } @@ -171,22 +270,25 @@ class Build { } static Future> buildCore({ - required Mode mode, - required Target target, + required CoreMode mode, + required TargetPlatform platform, Arch? arch, bool compatible = false, }) async { - final isLib = mode == Mode.lib; + final isLib = mode == CoreMode.lib; final items = buildItems.where((element) { - return element.target == target && - (arch == null ? true : element.arch == arch); + return element.platform == platform && (arch == null || element.arch == arch); }).toList(); - + final isMultiArch = items.length > 1; final List corePaths = []; - + final List macOSCorePaths = []; for (final item in items) { - final outFileDir = join(outDir, item.target.name, item.archName); + final outFileDir = join( + outDir, + item.platform.name, + item.platform == TargetPlatform.macos ? item.arch.name : item.archName + ); final file = File(outFileDir); if (file.existsSync()) { @@ -194,32 +296,54 @@ class Build { } final fileName = isLib - ? '$libName${item.target.dynamicLibExtensionName}' - : '$coreName${item.target.executableExtensionName}'; + ? '$libName${item.platform.dynamicLibExtensionName}' + : '$coreName${item.platform.executableExtensionName}'; final outPath = join(outFileDir, fileName); - corePaths.add(outPath); + if (item.platform == TargetPlatform.macos) { + macOSCorePaths.add(outPath); + } else { + corePaths.add(outPath); + } final Map env = {}; - env['GOOS'] = item.target.os; - if (item.arch != null) { - env['GOARCH'] = item.arch!.name; - } + env['GOOS'] = item.platform.os; + env['GOARCH'] = item.arch.name; if (item.arch == Arch.amd64 && - (item.target == Target.windows || - item.target == Target.linux || - item.target == Target.macos)) { + (item.platform == TargetPlatform.windows || + item.platform == TargetPlatform.linux || + item.platform == TargetPlatform.macos)) { env['GOAMD64'] = compatible ? 'v1' : 'v3'; } if (isLib) { env['CGO_ENABLED'] = '1'; - env['CC'] = _getCc(item); env['CFLAGS'] = '-O3 -Werror'; + if (item.platform == TargetPlatform.android) { + var ndkPath = Platform.environment['ANDROID_NDK']; + if (ndkPath == null) { + const ndkVersion = '27.0.12077973'; + final androidHome = Platform.environment['ANDROID_HOME']!; + await checkDeps(ndks: [ndkVersion]); + ndkPath = join(androidHome, 'ndk', ndkVersion); + } + final prebuiltDir = Directory(join(ndkPath, 'toolchains', 'llvm', 'prebuilt')); + final map = { + 'armeabi-v7a': 'armv7a-linux-androideabi21-clang', + 'arm64-v8a': 'aarch64-linux-android21-clang', + 'x86': 'i686-linux-android21-clang', + 'x86_64': 'x86_64-linux-android21-clang', + }; + env['CC'] = join(prebuiltDir.listSync().first.path, 'bin', map[item.archName]); + } else { + env['CC'] = 'gcc'; + await checkDeps(commands: ['gcc']); + } } else { env['CGO_ENABLED'] = '0'; } final buildTags = getTags(item); + await checkDeps(commands: ['go']); await exec( ['go', 'mod', 'tidy'], name: 'go mod tidy', @@ -231,7 +355,7 @@ class Build { 'go', 'build', '-trimpath', - '-ldflags=-w -s${item.target == Target.android && (item.arch == Arch.arm64 || item.arch == Arch.amd64) ? ' -extldflags "-Wl,-z,max-page-size=16384"' : ''}', + '-ldflags=-w -s${item.platform == TargetPlatform.android && (item.arch == Arch.arm64 || item.arch == Arch.amd64) ? ' -extldflags "-Wl,-z,max-page-size=16384"' : ''}', '-tags=$buildTags', if (isLib) '-buildmode=c-shared', '-o', @@ -239,16 +363,29 @@ class Build { ]; await exec( execLines, - name: 'build core', + name: 'build core ${isMultiArch ? "(${item.archName ?? item.arch.name})" : ""}', environment: env, workingDirectory: _coreDir, ); } + if (macOSCorePaths.isNotEmpty) { + final outFileDir = join(outDir, TargetPlatform.macos.name); + final fileName = isLib + ? '$libName${TargetPlatform.macos.dynamicLibExtensionName}' + : '$coreName${TargetPlatform.macos.executableExtensionName}'; + final outPath = join(outFileDir, fileName); + await exec( + macOSCorePaths.length > 1 + ? ['lipo', '-create', '-output', outPath, ...macOSCorePaths] + : ['cp', macOSCorePaths.first, outPath], + ); + corePaths.add(outPath); + } return corePaths; } - static Future buildHelper(Target target, String token) async { + static Future buildHelper(TargetPlatform platform, String token) async { await exec( ['cargo', 'build', '--release', '--features', 'windows-service'], environment: {'TOKEN': token}, @@ -259,14 +396,16 @@ class Build { _servicesDir, 'target', 'release', - 'helper${target.executableExtensionName}', + 'helper${platform.executableExtensionName}', ); final targetPath = join( outDir, - target.name, - 'BettboxHelperService${target.executableExtensionName}', + platform.name, + 'BettboxHelperService${platform.executableExtensionName}', ); await File(outPath).copy(targetPath); + final shaFile = File(join(outDir, platform.name, '.core_sha256')); + await shaFile.writeAsString('CORE_SHA256=$token'); } static List getExecutable(String command) { @@ -318,31 +457,52 @@ class Build { } class BuildCommand extends Command { - Target target; - - BuildCommand({required this.target}) { - if (target == Target.android || target == Target.linux) { - argParser.addOption( - 'arch', - valueHelp: arches.map((e) => e.name).join(','), - help: 'The $name build desc', + TargetPlatform platform; + + BuildCommand({required this.platform}) { + argParser.addOption( + 'arch', + abbr: 'a', + allowed: platform == TargetPlatform.android || platform == TargetPlatform.macos + ? [...arches.map((e) => e.name), 'universal'] + : arches.map((e) => e.name), + help: 'The architecture of $name build; omit this to select ' + '${platform != TargetPlatform.android ? "the host" : "every"} architecture', + ); + if (platform == TargetPlatform.linux) { + argParser.addMultiOption( + 'targets', + abbr: 't', + allowed: ['deb', 'rpm', 'appimage', 'zip'], + help: 'Linux package formats (multiple selections, seperated by ",")', ); - } else { - argParser.addOption('arch', help: 'The $name build archName'); } argParser.addOption( 'out', - valueHelp: [if (target.same) 'app', 'core'].join(','), - help: 'The $name build arch', + abbr: 'o', + allowed: [ + if (platform.buildable) 'app', + 'core', + if (platform == TargetPlatform.windows) 'core,helper', + ], + help: 'Build the full app; only the core; or core+helper (Windows)', + defaultsTo: platform.buildable ? 'app' : 'core', ); argParser.addOption( 'env', - valueHelp: ['pre', 'stable'].join(','), - help: 'The $name build env', + abbr: 'e', + allowed: ['pre', 'stable'], + help: 'The value of dart-define APP_ENV, used to identify the release channel', + defaultsTo: 'pre', ); argParser.addFlag( 'compatible', - help: 'Build with GOAMD64=v2 for broader compatibility on amd64', + help: 'Build with GOAMD64=v1 for broader compatibility on amd64', + ); + argParser.addFlag( + 'build-only', + abbr: 'B', + help: 'Skip packaging the app with flutter_distributor' ); } @@ -350,177 +510,149 @@ class BuildCommand extends Command { String get description => 'build $name application'; @override - String get name => target.name; + String get name => platform.name; List get arches => Build.buildItems - .where((element) => element.target == target && element.arch != null) - .map((e) => e.arch!) + .where((element) => element.platform == platform) + .map((e) => e.arch) .toList(); - Future _getLinuxDependencies(Arch arch) async { - await Build.exec(Build.getExecutable('sudo apt update -y')); - await Build.exec( - Build.getExecutable('sudo apt install -y ninja-build libgtk-3-dev'), - ); - await Build.exec( - Build.getExecutable('sudo apt install -y libayatana-appindicator3-dev'), - ); - await Build.exec( - Build.getExecutable('sudo apt-get install -y libkeybinder-3.0-dev'), - ); - await Build.exec(Build.getExecutable('sudo apt install -y locate')); - if (arch == Arch.amd64) { - await Build.exec( - Build.getExecutable('sudo apt install -y rpm patchelf libfuse2'), - ); - - final downloadName = arch == Arch.amd64 ? 'x86_64' : 'aarch64'; - await Build.exec( - Build.getExecutable( - 'wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage', - ), - ); - await Build.exec(Build.getExecutable('chmod +x appimagetool')); - await Build.exec( - Build.getExecutable('sudo mv appimagetool /usr/local/bin/'), - ); - } - } - - Future _getMacosDependencies() async { - await Build.exec(Build.getExecutable('npm install -g appdmg')); - } - - Future _setMacOSCompatibleBuild(bool enable) async { - final infoPlistPath = 'macos/Runner/Info.plist'; - final file = File(infoPlistPath); - - if (!await file.exists()) { - print('Warning: Info.plist not found at $infoPlistPath'); - return; - } - - var content = await file.readAsString(); - - // Check if FLTDisableImpeller key exists - if (content.contains('FLTDisableImpeller')) { - // Update existing key - if (enable) { - content = content.replaceAll( - RegExp(r'FLTDisableImpeller\s*<(?:true|false)/>'), - 'FLTDisableImpeller\n\t', - ); - } else { - content = content.replaceAll( - RegExp(r'FLTDisableImpeller\s*<(?:true|false)/>'), - 'FLTDisableImpeller\n\t', - ); - } - } else { - // Add new key before - final impellerEntry = enable - ? '\tFLTDisableImpeller\n\t\n' - : '\tFLTDisableImpeller\n\t\n'; - content = content.replaceFirst( - '\n', - '$impellerEntry\n', - ); - } - - await file.writeAsString(content); - print('macOS ${enable ? "Compatible" : "Standard"} build: FLTDisableImpeller set to $enable'); - } - Future _buildDistributor({ - required Target target, + required TargetPlatform platform, required String targets, String args = '', - required String env, + required String appEnv, + Map? buildEnv, + final bool buildOnly = false, }) async { final sentryDsn = Platform.environment['SENTRY_DSN'] ?? ''; - final sentryArg = sentryDsn.isNotEmpty - ? ' --build-dart-define=SENTRY_DSN=$sentryDsn' - : ''; + String sentryArg = ''; + if (sentryDsn.isNotEmpty) { + sentryArg = buildOnly + ? ' --dart-define=SENTRY_DSN=$sentryDsn' + : ' --build-dart-define=SENTRY_DSN=$sentryDsn'; + } - await Build.getDistributor(); + if (!buildOnly) await Build.getDistributor(); await Build.exec( - name: name, + name: description, + environment: buildEnv, Build.getExecutable( - 'flutter_distributor package --skip-clean --platform ${target.name} --targets $targets --flutter-build-args=verbose$args$sentryArg --build-dart-define=APP_ENV=$env', + buildOnly + ? 'flutter build ${platform == TargetPlatform.android ? "apk" : platform.name} --release --dart-define=APP_ENV=$appEnv --verbose$args$sentryArg' + : 'flutter_distributor package --skip-clean --platform ${platform.name} --targets $targets --flutter-build-args=verbose$args$sentryArg --build-dart-define=APP_ENV=$appEnv', ), ); } - Future get systemArch async { - if (Platform.isWindows) { - return Platform.environment['PROCESSOR_ARCHITECTURE']; - } else if (Platform.isLinux || Platform.isMacOS) { - final result = await Process.run('uname', ['-m']); - return result.stdout.toString().trim(); - } - return null; - } - @override Future run() async { - final mode = target == Target.android ? Mode.lib : Mode.core; - final String out = argResults?['out'] ?? (target.same ? 'app' : 'core'); - final archName = argResults?['arch']; - final env = argResults?['env'] ?? 'pre'; - final currentArches = arches - .where((element) => element.name == archName) - .toList(); - final arch = currentArches.isEmpty ? null : currentArches.first; - - if (arch == null && target != Target.android) { - throw 'Invalid arch parameter'; + final coreMode = platform == TargetPlatform.android ? CoreMode.lib : CoreMode.core; + final String out = argResults?['out'] ?? (platform.buildable ? 'app' : 'core'); + final String? archParam = argResults?['arch']; + final String appEnv = argResults?['env'] ?? 'pre'; + + Arch? arch; + if (archParam == null) { + if (platform != TargetPlatform.android) { + arch = arches.firstWhere((element) => element.same); + } + } else if (archParam == 'universal') { + if (platform != TargetPlatform.android && platform != TargetPlatform.macos) { + throw 'Invalid arch parameter!'; + } + } else { + arch = arches.where((element) => element.name == archParam).firstOrNull; + if (arch == null) throw 'Invalid arch parameter!'; } - + final bool compatible = argResults?['compatible'] ?? false; + bool buildOnly = argResults?['build-only'] ?? false; + if (platform == TargetPlatform.android && buildOnly) { + print('Warning: Ignored --build-only.'); + buildOnly = false; + } final corePaths = await Build.buildCore( - target: target, + platform: platform, arch: arch, - mode: mode, + mode: coreMode, compatible: compatible, ); - if (out != 'app') { + if (out == 'core') { + if (!platform.buildable) print('Platform incompatible, core built only!'); return; } - final String desc = compatible ? '$archName-compatible' : (archName ?? ''); + final String desc = '${archParam ?? arch?.name}${compatible ? "-compatible" : ""}'; + + String? buildTargets; + String buildArgs = ''; + Map? buildEnv; - switch (target) { - case Target.windows: - final token = target != Target.android + switch (platform) { + case TargetPlatform.windows: + if (!arch!.same) { + throw 'Corss-build to $name ${arch.name} target is not currently supported!'; + } + await checkDeps( + commands: ['cargo'], + files: buildOnly ? null : [r'C:\Program Files (x86)\Inno Setup 6\ISCC.exe'], + ); + final token = platform != TargetPlatform.android ? await Build.calcSha256(corePaths.first) : null; - Build.buildHelper(target, token!); - _buildDistributor( - target: target, - targets: 'exe', - args: ' --description $desc --build-dart-define=CORE_SHA256=$token', - env: env, - ); - return; - case Target.linux: + await Build.buildHelper(platform, token!); + if (out == 'core,helper') return; + buildTargets = 'exe'; + buildArgs = buildOnly + ? ' --dart-define=CORE_SHA256=$token' + : ' --description $desc --build-dart-define=CORE_SHA256=$token'; + break; + + case TargetPlatform.linux: final targetMap = {Arch.arm64: 'linux-arm64', Arch.amd64: 'linux-x64'}; - final targets = [ - 'deb', - if (arch == Arch.amd64) 'appimage', - if (arch == Arch.amd64) 'rpm', - ].join(','); + List targets = argResults?['targets']; + if (buildOnly) { + if (targets.isNotEmpty) { + print('Warning: --build-only is specified, ignoring targets parameter.'); + } + targets = []; + } else if (targets.isEmpty) { + throw 'Invalid targets parameter'; + } + if (!arch!.same) { + throw 'Corss-build to $name ${arch.name} target is not currently supported!'; + } final defaultTarget = targetMap[arch]; - await _getLinuxDependencies(arch!); - _buildDistributor( - target: target, - targets: targets, - args: ' --description $desc --build-target-platform $defaultTarget', - env: env, + + final requiredCmds = ['clang', 'cmake', 'ninja']; + Map requiredRtLibs = {}; + if (targets.contains('deb')) requiredCmds.add('dpkg-deb'); + if (targets.contains('rpm')) requiredCmds.addAll(['rpm', 'patchelf']); + if (targets.contains('appimage')) { + requiredCmds.addAll(['appimagetool', 'locate']); + requiredRtLibs.addAll({'libfuse2': 'libfuse.so.2'}); + } + await checkDeps( + commands: requiredCmds, + devLibs: { + 'gtk3': 'gtk+-3.0', + 'libayatana-appindicator': 'ayatana-appindicator3-0.1', + 'keybinder-3.0': 'keybinder-3.0', + 'libcurl': 'libcurl', + }, + rtLibs: requiredRtLibs, ); - return; - case Target.android: + + buildTargets = targets.join(','); + buildArgs = buildOnly + ? ' --target-platform $defaultTarget' + : ' --description $desc --build-target-platform $defaultTarget'; + break; + + case TargetPlatform.android: final targetMap = { Arch.arm: 'android-arm', Arch.arm64: 'android-arm64', @@ -532,41 +664,69 @@ class BuildCommand extends Command { .map((e) => targetMap[e]) .toList(); - final buildArgs = archName == 'universal' + buildArgs = archParam == 'universal' ? ' --build-target-platform ${defaultTargets.join(",")} --description universal' : ',split-per-abi --build-target-platform ${defaultTargets.join(",")}'; + buildTargets = 'apk'; + break; + + case TargetPlatform.macos: + if (!buildOnly) await checkDeps(commands: ['appdmg']); - _buildDistributor( - target: target, - targets: 'apk', - args: buildArgs, - env: env, - ); - return; - case Target.macos: - await _getMacosDependencies(); // For compatible build, disable Impeller and use Skia renderer - if (compatible) { - await _setMacOSCompatibleBuild(true); + final infoPlist = File('macos/Runner/Info.plist'); + if (await infoPlist.exists()) { + var content = await infoPlist.readAsString(); + + // Check if FLTDisableImpeller key exists + if (content.contains('FLTDisableImpeller')) { + content = content.replaceAll( + RegExp(r'FLTDisableImpeller\s*<(?:true|false)/>'), + 'FLTDisableImpeller\n\t<$compatible/>', + ); + } else { + // Add new key before + final impellerEntry = '\tFLTDisableImpeller\n\t<$compatible/>\n'; + content = content.replaceFirst( + '\n', + '$impellerEntry\n', + ); + } + await infoPlist.writeAsString(content); + print( + 'macOS ${compatible ? "Compatible" : "Standard"} build: FLTDisableImpeller set to $compatible', + ); } else { - await _setMacOSCompatibleBuild(false); + print('Warning: ${infoPlist.path} not found!'); } - _buildDistributor( - target: target, - targets: 'dmg', - args: ' --description $desc', - env: env, - ); - return; + + final archName = archParam == 'universal' + ? null + : arch?.archMap.keys.firstWhere( + (k) => arch?.archMap[k] == arch?.name, + ); + buildEnv = archName == null ? null : {'FLUTTER_XCODE_ARCHS': archName}; + buildTargets = 'dmg'; + buildArgs = buildOnly ? '' : ' --description $desc'; + break; } + + _buildDistributor( + platform: platform, + targets: buildTargets, + args: buildArgs, + appEnv: appEnv, + buildEnv: buildEnv, + buildOnly: buildOnly, + ); } } Future main(Iterable args) async { final runner = CommandRunner('setup', 'build Application'); - runner.addCommand(BuildCommand(target: Target.android)); - runner.addCommand(BuildCommand(target: Target.linux)); - runner.addCommand(BuildCommand(target: Target.windows)); - runner.addCommand(BuildCommand(target: Target.macos)); + runner.addCommand(BuildCommand(platform: TargetPlatform.android)); + runner.addCommand(BuildCommand(platform: TargetPlatform.linux)); + runner.addCommand(BuildCommand(platform: TargetPlatform.windows)); + runner.addCommand(BuildCommand(platform: TargetPlatform.macos)); runner.run(args); }