mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 08:43:17 +08:00
[fix] align Android versions with release tags, refs #107
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# CrossDesk Android
|
||||
|
||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。当前版本为 1.6.0,支持 Android 8.0(API 26)及以上的 arm64 手机和平板。
|
||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。当前版本为 1.6.1,支持 Android 8.0(API 26)及以上的 arm64 手机和平板。
|
||||
|
||||
## 当前功能
|
||||
|
||||
@@ -83,11 +83,11 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
|
||||
|
||||
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建及签名成功,并将已签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
|
||||
|
||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先提交 `app/build.gradle` 中需要发布的 `versionName`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||
单独发布使用 [`Release Mobile`](../../.github/workflows/release-mobile.yml)。先更新并提交 `gradle.properties` 中的 `crossdeskVersionName` 和递增的 `crossdeskVersionCode`,再为该提交创建 `android-v<versionName>-YYYYMMDD` 标签,例如:
|
||||
|
||||
```sh
|
||||
git tag android-v1.6.0-20261002
|
||||
git push origin android-v1.6.0-20261002
|
||||
git tag android-v1.6.1-20261010
|
||||
git push origin android-v1.6.1-20261010
|
||||
```
|
||||
|
||||
该标签只触发安卓构建,使用固定发布密钥签名并验证 Release APK 后,再发布到独立的 GitHub Release 和现有下载服务器。签名失败或缺少签名 Secrets 时停止发布,不回退到未签名 APK。标签版本必须与应用版本一致,日期必须有效。重试发布时,在 Actions 页面手动运行 `Release Mobile`,将已有标签填入 `source_tag`;即使从其他分支触发,也会检出该标签的源码。APK 的源码资料记录安卓标签及准确的应用、MiniRTC 提交。
|
||||
@@ -100,15 +100,15 @@ git push origin android-v1.6.0-20261002
|
||||
|
||||
Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。GitHub 托管 runner 每次启动仍是新环境;第一次运行或缓存失效时需要安装。不同分支和标签可读取默认分支的缓存,因此建议先在默认分支运行一次 `Build Android` 预热,再创建发布标签。
|
||||
|
||||
CI 仅构建和打包 Debug、Release APK,并校验 APK 的 arm64 ABI、16 KB ELF / ZIP 对齐、完整许可证及 Debug 签名。单元测试、本机 C++ 协议与画面队列测试、Lint 和设备测试改为按需在本地执行;设备测试使用下面的设备验证流程在 arm64 手机或模拟器上执行。
|
||||
CI 仅构建和打包 Debug、Release APK,并校验 APK 的包名、`versionName` / `versionCode`、arm64 ABI、16 KB ELF / ZIP 对齐、完整许可证及 Debug 签名。单元测试、本机 C++ 协议与画面队列测试、Lint 和设备测试改为按需在本地执行;设备测试使用下面的设备验证流程在 arm64 手机或模拟器上执行。
|
||||
|
||||
产物使用 Android 工程自己的 `versionName`,例如 `v1.6.0-20261002`,不跟随桌面版本号。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
||||
统一 `v*` 发布从标签提取数字版本,通过 Gradle 属性写入 APK 的 `versionName`;App 内版本展示读取同一包信息。产物版本、日期及可选热修复编号与桌面端保持一致,例如 `v1.6.1-20261010` 或 `v1.6.1-2-20261010`。分支构建和独立 `android-v*` 发布使用 `gradle.properties` 中的版本,独立发布会检查标签与配置一致。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
|
||||
|
||||
- `crossdesk-android-arm64-<版本>.apk`:已签名 Release APK,独立安卓发布和全平台标签发布收集此 APK,可直接安装。
|
||||
- `crossdesk-android-arm64-unsigned-<版本>.apk`:构建工作流的中间产物,也可用于自行签名和验证修改后的构建。
|
||||
- `crossdesk-android-arm64-debug-<版本>.apk`:可安装的调试应用,保留 14 天。
|
||||
|
||||
Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在发布新版本时递增 `app/build.gradle` 中的 `versionCode`。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
||||
Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在每次发布新版本(含统一发布及热修复)时递增 `gradle.properties` 中的 `crossdeskVersionCode`。该安装升级序号独立于显示版本,不从日期或 CI 运行编号推算。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
|
||||
|
||||
### Release 签名
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ android {
|
||||
applicationId 'cn.crossdesk.mobile'
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode 2
|
||||
versionName '1.6.0'
|
||||
versionCode providers.gradleProperty('crossdeskVersionCode').get().toInteger()
|
||||
versionName providers.gradleProperty('crossdeskVersionName').get()
|
||||
ndk { abiFilters 'arm64-v8a' }
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
crossdeskVersionName=1.6.1
|
||||
crossdeskVersionCode=3
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve Android package and artifact versions before building the APKs."""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
def resolve_version(version_name, version_code, tag="", patch="0", today=None):
|
||||
build_date = today or datetime.datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d")
|
||||
patch = patch if re.fullmatch(r"[0-9]+", patch) else "0"
|
||||
if tag:
|
||||
independent = tag.startswith("android-v")
|
||||
source_version = tag.removeprefix("android-")
|
||||
base = r"(?P<base>[0-9]+\.[0-9]+\.[0-9]+)"
|
||||
for pattern in (
|
||||
rf"v{base}-(?P<patch>[0-9]+)-(?P<date>[0-9]{{8}})",
|
||||
rf"v{base}-(?P<date>[0-9]{{8}})(?:-(?P<patch>[0-9]+))?",
|
||||
rf"v{base}(?:-(?P<patch>[0-9]+))?",
|
||||
):
|
||||
match = re.fullmatch(pattern, source_version)
|
||||
if match:
|
||||
fields = match.groupdict()
|
||||
if independent and fields["base"] != version_name:
|
||||
raise ValueError(f"Android source tag does not match the app version: {tag} vs {version_name}")
|
||||
version_name = fields["base"]
|
||||
build_date = fields.get("date") or build_date
|
||||
patch = fields.get("patch") or patch
|
||||
break
|
||||
else:
|
||||
raise ValueError(f"Invalid Android release tag: {tag!r}")
|
||||
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version_name):
|
||||
raise ValueError(f"Invalid Android versionName: {version_name!r}")
|
||||
if not re.fullmatch(r"[0-9]+", version_code) or not 0 < int(version_code) <= 2100000000:
|
||||
raise ValueError(f"Invalid Android versionCode: {version_code!r}")
|
||||
datetime.datetime.strptime(build_date, "%Y%m%d")
|
||||
suffix = f"-{patch}" if patch != "0" else ""
|
||||
artifact_version = f"v{version_name}{suffix}-{build_date}"
|
||||
if tag.startswith("android-v") and tag.removeprefix("android-") != artifact_version:
|
||||
raise ValueError(f"Android source tag does not match the artifact version: {tag} vs {artifact_version}")
|
||||
return {"ANDROID_VERSION_NAME": version_name, "ANDROID_VERSION_CODE": str(int(version_code)),
|
||||
"ANDROID_ARTIFACT_VERSION": artifact_version}
|
||||
|
||||
|
||||
def main():
|
||||
properties = dict(line.split("=", 1) for line in
|
||||
(Path(__file__).resolve().parents[1] / "gradle.properties").read_text().splitlines()
|
||||
if "=" in line and not line.lstrip().startswith("#"))
|
||||
tag = os.environ.get("CROSSDESK_SOURCE_TAG", "")
|
||||
if not tag and os.environ.get("GITHUB_REF_TYPE") == "tag":
|
||||
ref_name = os.environ["GITHUB_REF_NAME"]
|
||||
# Moving aliases such as "latest" are builds, not versioned releases.
|
||||
if ref_name.startswith(("v", "android-v")):
|
||||
tag = ref_name
|
||||
values = resolve_version(properties["crossdeskVersionName"], properties["crossdeskVersionCode"],
|
||||
tag, os.environ.get("PATCH_NUMBER", "0"))
|
||||
text = "".join(f"{key}={value}\n" for key, value in values.items())
|
||||
if os.environ.get("GITHUB_ENV"):
|
||||
with open(os.environ["GITHUB_ENV"], "a") as stream:
|
||||
stream.write(text)
|
||||
print(text, end="")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,13 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the shipped native ABI, ELF load alignment and APK zip alignment."""
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
|
||||
def verify_package_version(badging, version_name, version_code):
|
||||
package = next((line for line in badging.splitlines() if line.startswith('package: ')), '')
|
||||
attributes = dict(re.findall(r"(\w+)='([^']*)'", package))
|
||||
expected = {'name': 'cn.crossdesk.mobile', 'versionName': version_name, 'versionCode': str(version_code)}
|
||||
for key, value in expected.items():
|
||||
if attributes.get(key) != value:
|
||||
raise ValueError(f'Unexpected APK {key}: expected {value!r}, got {attributes.get(key)!r}')
|
||||
|
||||
|
||||
def verify(path):
|
||||
with zipfile.ZipFile(path) as archive, open(path, 'rb') as raw:
|
||||
libraries = [i for i in archive.infolist() if i.filename.endswith('.so')]
|
||||
@@ -59,4 +70,16 @@ def verify(path):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
verify(sys.argv[1])
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('apk', type=Path)
|
||||
parser.add_argument('--aapt2', type=Path)
|
||||
parser.add_argument('--version-name')
|
||||
parser.add_argument('--version-code', type=int)
|
||||
args = parser.parse_args()
|
||||
if any(value is not None for value in (args.aapt2, args.version_name, args.version_code)):
|
||||
if any(value is None for value in (args.aapt2, args.version_name, args.version_code)):
|
||||
parser.error('--aapt2, --version-name and --version-code must be supplied together')
|
||||
badging = subprocess.check_output([str(args.aapt2), 'dump', 'badging', str(args.apk)], text=True)
|
||||
verify_package_version(badging, args.version_name, args.version_code)
|
||||
print(f'PASS: {args.apk.name}: versionName={args.version_name}, versionCode={args.version_code}')
|
||||
verify(args.apk)
|
||||
|
||||
Reference in New Issue
Block a user