[build] unify minimum Xmake version across platforms

This commit is contained in:
dijunkun
2026-10-04 14:55:49 +08:00
parent 7f7b91851e
commit 050394135a
18 changed files with 141 additions and 53 deletions
+3 -4
View File
@@ -35,7 +35,7 @@
- JDK 17 或更新版本(可使用 Android Studio 自带 JBR)。
- Android SDK Platform 36、Build Tools 36.0.0、NDK `28.2.13676358`。
- Xmake **3.1.1**、Python 3.9+、Git、CMake 和主机编译工具。
- Xmake **3.1.1 或更新版本**、Python 3.9+、Git、CMake 和主机编译工具。
- Python 的 setuptools、wheel,用于构建依赖的宿主工具。
先在仓库根目录初始化子模块:
@@ -49,8 +49,7 @@ macOS 命令行示例:
```sh
export ANDROID_HOME="$HOME/Library/Android/sdk"
export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
# 若 PATH 中的 Xmake 不是 3.1.1,通过 XMAKE_BIN 指定对应可执行文件。
# 构建器也识别 ~/.cache/crossdesk/toolchains/xmake-3.1.1/xmake。
# 使用 PATH 中的 Xmake(最低 3.1.1),或通过 XMAKE_BIN 指定其他可执行文件。
cd apps/android
./gradlew :app:assembleDebug
```
@@ -94,7 +93,7 @@ git push origin android-v1.6.0-20261002
已有 Release 需要补传服务器或刷新下载信息时,可手动运行 `Update version.json from Release` 并填写已有 `source_tag`,无需重新构建。
工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c 和 Xmake 3.1.1。Gradle 使用工程内的 Wrapper,原生构建器固定 Xmake 包仓库版本。SDK/NDK 使用独立目录缓存,按宿主系统、架构和工具链版本区分;命中缓存时跳过 SDK 安装,未命中时在安装完成后立即保存,即使后续 APK 构建失败,下次也可复用。Xmake 可执行环境、固定版本的包仓库和 pip 下载也有各自的缓存;Ubuntu 系统工具只安装缺失项。
工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c,并使用最低支持的 Xmake 3.1.1 验证构建;本地允许使用更新的 Xmake。Gradle 使用工程内的 Wrapper,原生构建器固定 Xmake 包仓库版本。SDK/NDK 使用独立目录缓存,按宿主系统、架构和工具链版本区分;命中缓存时跳过 SDK 安装,未命中时在安装完成后立即保存,即使后续 APK 构建失败,下次也可复用。Xmake 可执行环境、固定版本的包仓库和 pip 下载也有各自的缓存;Ubuntu 系统工具只安装缺失项。
Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
+1
View File
@@ -1,4 +1,5 @@
set_project("crossdesk_android")
set_xmakever("3.1.1")
set_languages("c++17")
add_rules("mode.release", "mode.debug")
set_policy("package.precompiled", false)
+14 -7
View File
@@ -12,6 +12,19 @@ ROOT = ANDROID.parents[1]
SOURCE = ROOT / 'deps/submodules/minirtc'
CACHE = ANDROID / '.native'
PACKAGE_REVISION = 'eda39de3fb99b420c168f1ab9ab2d1791e11b662'
XMAKE_MIN_VERSION = (3, 1, 1)
def find_xmake():
xmake = os.environ.get('XMAKE_BIN') or shutil.which('xmake')
minimum = '.'.join(map(str, XMAKE_MIN_VERSION))
if not xmake:
raise SystemExit(f'Xmake {minimum} or newer is required; install it or set XMAKE_BIN.')
output = subprocess.check_output([xmake, '--version'], text=True)
version = re.search(r'xmake v(\d+)\.(\d+)\.(\d+)(?:\+|[,\s]|$)', output)
if not version or tuple(map(int, version.groups())) < XMAKE_MIN_VERSION:
raise SystemExit(f'Use Xmake {minimum} or newer (set XMAKE_BIN to its executable).')
return xmake
def main():
@@ -20,13 +33,7 @@ def main():
ndk = sdk / 'ndk/28.2.13676358'
if not ndk.is_dir():
raise SystemExit('Install Android NDK 28.2.13676358 using SDK Manager.')
bundled = Path.home() / '.cache/crossdesk/toolchains/xmake-3.1.1/xmake'
xmake = os.environ.get('XMAKE_BIN') or (str(bundled) if bundled.is_file() else shutil.which('xmake'))
if not xmake:
raise SystemExit('xmake is required; install it or set XMAKE_BIN.')
version = subprocess.check_output([xmake, '--version'], text=True)
if not re.search(r'xmake v3\.1\.1(?:\+|[,\s])', version):
raise SystemExit('Use Xmake 3.1.1 (set XMAKE_BIN to its executable).')
xmake = find_xmake()
if not (SOURCE / 'xmake.lua').is_file():
raise SystemExit('Initialize the MiniRTC submodule before building.')
env = os.environ.copy()
+57
View File
@@ -0,0 +1,57 @@
"""Check Xmake discovery and the minimum supported version before native builds."""
import importlib.util
import os
from pathlib import Path
import unittest
from unittest.mock import patch
source = Path(__file__).resolve().parents[1] / "scripts/build_native.py"
spec = importlib.util.spec_from_file_location("build_native", source)
build_native = importlib.util.module_from_spec(spec)
spec.loader.exec_module(build_native)
class XmakeSelectionTest(unittest.TestCase):
def setUp(self):
for patcher in (
patch.dict(os.environ, {}, clear=True),
patch.object(build_native.shutil, "which", return_value="/tools/xmake"),
patch.object(build_native.subprocess, "check_output", return_value="xmake v3.1.1\n"),
):
patcher.start()
self.addCleanup(patcher.stop)
def test_path_tool_accepts_minimum_and_newer_versions(self):
for output in ("xmake v3.1.1", "xmake v3.1.2\n", "xmake v3.1.10, A build utility\n",
"xmake v3.2.0\n", "xmake v3.10.0\n", "xmake v4.0.0\n",
"\x1b[1mxmake v3.1.1+HEAD.3ba37a0d4, A build utility\x1b[0m\n"):
with self.subTest(output=output):
build_native.subprocess.check_output.return_value = output
self.assertEqual(build_native.find_xmake(), "/tools/xmake")
def test_explicit_tool_takes_precedence_over_path(self):
os.environ["XMAKE_BIN"] = "/custom/xmake"
self.assertEqual(build_native.find_xmake(), "/custom/xmake")
build_native.shutil.which.assert_not_called()
build_native.subprocess.check_output.assert_called_once_with(
["/custom/xmake", "--version"], text=True)
def test_missing_tool_reports_minimum_requirement(self):
build_native.shutil.which.return_value = None
with self.assertRaisesRegex(SystemExit, "Xmake 3.1.1 or newer is required"):
build_native.find_xmake()
build_native.subprocess.check_output.assert_not_called()
def test_older_or_unrecognized_versions_are_rejected(self):
for output in ("xmake v3.1.0\n", "xmake v3.0.99\n", "xmake v2.99.99\n",
"xmake version unknown\n", "xmake v3.1\n", "xmake v3.1.1invalid\n", ""):
with self.subTest(output=output):
build_native.subprocess.check_output.return_value = output
with self.assertRaisesRegex(SystemExit, "Use Xmake 3.1.1 or newer"):
build_native.find_xmake()
if __name__ == "__main__":
unittest.main()
+7 -6
View File
@@ -7,7 +7,7 @@ There is no `WKWebView` or browser runtime.
## Requirements
- Xcode 16 or newer
- Xmake 3.1.1 available on `PATH` (or selected with `XMAKE_BIN`)
- Xmake 3.1.1 or newer available on `PATH` (or selected with `XMAKE_BIN`)
- A physical arm64 iPhone/iPad running iOS 16 or newer, or an iOS simulator on Apple Silicon
## Build
@@ -27,7 +27,7 @@ licenses** when cloning: `git clone --branch <source-tag> --recurse-submodules
https://github.com/kunkundi/crossdesk.git`. The iOS marketing version is independent
of the repository tag; use the source tag or commit recorded in the app.
Install [Xmake 3.1.1](https://github.com/xmake-io/xmake/releases/tag/v3.1.1),
Install [Xmake](https://xmake.io/guide/quick-start.html) 3.1.1 or newer,
CMake, and a current Python 3 with setuptools/wheel. Pin the package repository
in a separate Xmake global directory before building:
@@ -41,7 +41,7 @@ git -C "$XMAKE_GLOBALDIR/.xmake/repositories/xmake-repo" checkout --detach 12110
Keep `XMAKE_GLOBALDIR` set for command-line Xcode builds. For builds launched
from the Xcode app, set it as a user-defined build setting on the target, or
pin the default `~/.xmake/repositories/xmake-repo` instead. The build checks
the tool version, package repository commit and reviewed recipe hashes.
the minimum Xmake version, package repository commit and reviewed recipe hashes.
Application dependencies are built from source, with Xmake precompiled
package downloads disabled.
@@ -105,9 +105,10 @@ or relevant build configuration also trigger it. The main
workflow on branch/tag pushes and manual runs, and waits for its result before
publishing a version-tag release.
The workflow retains the pinned Xcode/SDK and Xmake versions, dependency cache,
license and privacy checks, unsigned arm64 Release build, and app-bundle
verification. It uploads `crossdesk-ios-arm64-unsigned-v<marketing-version>-<date>.zip`,
The workflow tests the minimum supported Xmake version (3.1.1) and retains the
pinned Xcode/SDK, dependency cache, license and privacy checks, unsigned arm64
Release build, and app-bundle verification. It uploads
`crossdesk-ios-arm64-unsigned-v<marketing-version>-<date>.zip`,
including the app, source metadata, third-party source catalog, rights notice
and any generated dSYM. The version comes from the iOS target; dated repository
tags provide the build date, otherwise the date uses the Asia/Shanghai timezone.
+1 -1
View File
@@ -900,7 +900,7 @@
],
"bundle_sha256": "f54e5e2168bf6e91d81a413931a052d29f59e0c221df5e1de3f51bd27c662c3a",
"toolchain": {
"xmake_version": "3.1.1",
"xmake_min_version": "3.1.1",
"xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955"
}
}
@@ -137,9 +137,10 @@ def generate(lock, cache):
def verify(lock, inputs, target_info, xmake_repository=None, xmake_version=None):
checked(OUTPUT.read_bytes(), lock["bundle_sha256"], str(OUTPUT))
if xmake_version is not None:
version = re.search(r"xmake v(\d+\.\d+\.\d+)", xmake_version.read_text())
if not version or version[1] != lock["toolchain"]["xmake_version"]:
raise ValueError("Xmake version differs from the reviewed iOS toolchain; see apps/ios/README.md")
minimum = lock["toolchain"]["xmake_min_version"]
version = re.search(r"xmake v(\d+)\.(\d+)\.(\d+)(?:\+|[,\s]|$)", xmake_version.read_text())
if not version or tuple(map(int, version.groups())) < tuple(map(int, minimum.split("."))):
raise ValueError(f"Xmake {minimum} or newer is required; see apps/ios/README.md")
if xmake_repository is not None:
revision = subprocess.check_output(
["git", "-C", str(xmake_repository), "rev-parse", "HEAD"], text=True).strip()
+17 -5
View File
@@ -52,7 +52,7 @@ class LicenseValidationTest(unittest.TestCase):
self.target.write_text(str(self.root / "packages/a/asio/1.0/abcd/include") + "\n")
self.lock = {
"bundle_sha256": licenses.digest(self.bundle.read_bytes()),
"toolchain": {"xmake_version": "3.1.1"},
"toolchain": {"xmake_min_version": "3.1.1"},
"components": [
{
"id": "minirtc", "version": self.reviewed_revision,
@@ -149,11 +149,23 @@ class LicenseValidationTest(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "Source checksum mismatch: .*libopus.a"):
self.verify()
def test_unreviewed_toolchain_is_rejected(self):
def test_minimum_and_newer_xmake_versions_are_accepted(self):
version = self.root / "xmake-version.txt"
version.write_text("xmake v3.1.2\n")
with self.assertRaisesRegex(ValueError, "Xmake version differs"):
self.verify(xmake_version=version)
for output in ("xmake v3.1.1", "xmake v3.1.2\n", "xmake v3.1.10, A build utility\n",
"xmake v3.2.0\n", "xmake v3.10.0\n", "xmake v4.0.0\n",
"\x1b[1mxmake v3.1.1+HEAD.3ba37a0d4, A build utility\x1b[0m\n"):
with self.subTest(output=output):
version.write_text(output)
self.verify(xmake_version=version)
def test_older_or_unrecognized_xmake_versions_are_rejected(self):
version = self.root / "xmake-version.txt"
for output in ("xmake v3.1.0\n", "xmake v3.0.99\n", "xmake v2.99.99\n",
"xmake version unknown\n", "xmake v3.1\n", "xmake v3.1.1invalid\n", ""):
with self.subTest(output=output):
version.write_text(output)
with self.assertRaisesRegex(ValueError, "Xmake 3.1.1 or newer is required"):
self.verify(xmake_version=version)
class ComponentSourceTest(unittest.TestCase):