mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[CI, Android] (Disaggregated): ship and verify inproc APKs on disaggregation branches
This commit is contained in:
@@ -10,6 +10,7 @@ on:
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
- codex/fix-github-ci
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -25,6 +26,7 @@ jobs:
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -81,7 +83,13 @@ jobs:
|
||||
run: python update_glslang_sources.py
|
||||
|
||||
- name: Build plugin APK
|
||||
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -123,12 +131,74 @@ jobs:
|
||||
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
|
||||
|
||||
- name: Build retrace APK
|
||||
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
|
||||
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
|
||||
- name: Prove disaggregated native code in both APKs
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import hashlib, json, os, re, zipfile
|
||||
from pathlib import Path
|
||||
proofs = {}
|
||||
for path in Path('.').rglob('compile_commands.json'):
|
||||
rows = json.loads(path.read_text())
|
||||
production = [row for row in rows if row['file'].replace('\\', '/').endswith('/MobileGL/ConfigLoader.cpp')]
|
||||
if not production:
|
||||
continue
|
||||
cache = path.with_name('CMakeCache.txt').read_text()
|
||||
abi = re.search(r'^ANDROID_ABI:[^=]+=(.+)$', cache, re.MULTILINE)
|
||||
if not abi:
|
||||
raise SystemExit(f'no Android ABI identity beside {path}')
|
||||
for row in production:
|
||||
command = row.get('command') or ' '.join(row['arguments'])
|
||||
for macro in ('MOBILEGL_BUILD_DISAGGREGATED', 'MOBILEGL_PIPE_PUSH'):
|
||||
if not re.search(r'-D' + macro + r'=1(?:\s|$)', command):
|
||||
raise SystemExit(f'{path}: production command lacks {macro}=1')
|
||||
proofs.setdefault(abi[1].strip(), []).append(str(path))
|
||||
if not proofs:
|
||||
raise SystemExit('no production compile commands prove a disaggregated APK build')
|
||||
artifacts = []
|
||||
marker = b'Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream'
|
||||
for flavor in ('plugin', 'trace'):
|
||||
stem = 'MobileGL-plugin-' + ('trace-' if flavor == 'trace' else '')
|
||||
apk = Path(f'android-plugin/app/build/outputs/apk/{flavor}/release/{stem}release-{os.environ["GITHUB_SHA"]}.apk')
|
||||
libraries = []
|
||||
with zipfile.ZipFile(apk) as archive:
|
||||
for name in archive.namelist():
|
||||
if not re.fullmatch(r'lib/[^/]+/libMobileGL\.so', name):
|
||||
continue
|
||||
abi = name.split('/')[1]
|
||||
library = archive.read(name)
|
||||
if abi not in proofs or marker not in library:
|
||||
raise SystemExit(f'{apk}:{name}: missing compile or compiled transport evidence')
|
||||
libraries.append({'abi': abi, 'path': name, 'sha256': hashlib.sha256(library).hexdigest()})
|
||||
required = {'arm64-v8a', 'x86_64'} if flavor == 'trace' else {'arm64-v8a'}
|
||||
if not required.issubset({library['abi'] for library in libraries}):
|
||||
raise SystemExit(f'{apk}: requested ABI coverage is missing: {required}')
|
||||
artifacts.append({'apk': str(apk), 'sha256': hashlib.sha256(apk.read_bytes()).hexdigest(), 'libraries': libraries})
|
||||
Path('apk-build-mode.json').write_text(json.dumps({'mode': 'disaggregated-inproc', 'source_sha': os.environ['GITHUB_SHA'],
|
||||
'production_compile_commands_by_abi': proofs, 'artifacts': artifacts}, indent=2))
|
||||
PY
|
||||
|
||||
- name: Upload APK build-mode evidence
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-apk-build-mode-${{ github.sha }}
|
||||
path: apk-build-mode.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Show ccache stats
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
@@ -338,6 +408,7 @@ jobs:
|
||||
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||
timeout-minutes: 75
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
@@ -437,6 +508,12 @@ jobs:
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
extra_retrace_args=()
|
||||
if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--require-inproc
|
||||
)
|
||||
fi
|
||||
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
||||
# min-filter downgrading on ANGLE llvmpipe.
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
|
||||
|
||||
@@ -33,6 +33,15 @@ fun Project.mobileGlAbiFilters(): List<String> {
|
||||
fun Project.mobileGlCmakeCompilerLauncher(): String =
|
||||
(findProperty("mobilegl.cmakeCompilerLauncher") ?: System.getenv("MOBILEGL_CMAKE_COMPILER_LAUNCHER") ?: "").toString().trim()
|
||||
|
||||
// Match the native module's two switches, including INPROC implying the parent
|
||||
// option. A D/P plugin must ask its launcher to select the transport at runtime;
|
||||
// compiling MG_Remote alone leaves ConfigLoader's default on monolith.
|
||||
fun Project.mobileGlDisaggregatedEnabled(): Boolean {
|
||||
fun enabled(value: Any?): Boolean = value?.toString()?.trim()?.uppercase().orEmpty() in setOf("1", "ON", "YES", "TRUE", "Y")
|
||||
return enabled(findProperty("mobilegl.buildDisaggregated") ?: System.getenv("MOBILEGL_BUILD_DISAGGREGATED")) ||
|
||||
enabled(findProperty("mobilegl.buildDisaggregatedInproc") ?: System.getenv("MOBILEGL_BUILD_DISAGGREGATED_INPROC"))
|
||||
}
|
||||
|
||||
// Optional application-id suffix, so a development build can sit next to an
|
||||
// already-installed plugin instead of having to replace it - a differently
|
||||
// signed APK cannot upgrade one in place, and uninstalling costs the user their
|
||||
@@ -73,6 +82,7 @@ val mobileGlMonthlyRevision = runGit(
|
||||
val mobileGlApkSuffix = (findProperty("mobilegl.apkSuffix") ?: System.getenv("MOBILEGL_APK_SUFFIX") ?: mobileGlGitShortHash)
|
||||
.toString()
|
||||
.ifBlank { "nogit" }
|
||||
val mobileGlDisaggregated = mobileGlDisaggregatedEnabled()
|
||||
|
||||
val pluginRendererConfig = buildJsonValue {
|
||||
renderer(
|
||||
@@ -83,6 +93,7 @@ val pluginRendererConfig = buildJsonValue {
|
||||
dlopenLibPaths = emptyList(),
|
||||
env = buildEnvs {
|
||||
normal("LIBGL_ES", "3")
|
||||
if (mobileGlDisaggregated) normal("MOBILEGL_TRANSPORT", "inproc")
|
||||
selectable(
|
||||
key = "MOBILEGL_BACKEND_TYPE",
|
||||
title = RendererConfig.MetaString("mobilegl_backend_type_title"),
|
||||
@@ -127,11 +138,13 @@ android {
|
||||
put("LIBGL_ES", "3")
|
||||
put("POJAV_RENDERER", "opengles3")
|
||||
put("MOBILEGL_BACKEND_TYPE", "DirectGLES")
|
||||
if (mobileGlDisaggregated) put("MOBILEGL_TRANSPORT", "inproc")
|
||||
}
|
||||
pojavEnv {
|
||||
put("LIBGL_ES", "3")
|
||||
put("POJAV_RENDERER", "opengles3")
|
||||
put("MOBILEGL_BACKEND_TYPE", "DirectGLES")
|
||||
if (mobileGlDisaggregated) put("MOBILEGL_TRANSPORT", "inproc")
|
||||
}
|
||||
})
|
||||
manifestPlaceholders["appLabel"] = "MobileGL"
|
||||
|
||||
@@ -33,6 +33,7 @@ Usage:
|
||||
[--coherent-as-flush] \
|
||||
[--dump-texture-2d CALL,TEXTURE,LEVEL,DIR] \
|
||||
[--env "K=V;K=V"] \
|
||||
[--require-inproc] \
|
||||
[--benchmark] \
|
||||
[--benchmark-tail-frames N] \
|
||||
[--benchmark-finish 0|1] \
|
||||
@@ -66,6 +67,9 @@ variables to the replay process. They are applied last, immediately before
|
||||
libMobileGL.so is loaded, so they override every flag above; an entry with no "="
|
||||
unsets the variable instead. This is the generic passthrough: a MOBILEGL_* knob
|
||||
that has no flag of its own needs no plumbing to be forwarded.
|
||||
Pass --require-inproc to require the library's real transport-resolution log,
|
||||
strict errors and separate role state, plus zero Fatal records. This checks the
|
||||
APK process after replay; a host environment echo or an SSIM-only pass is not proof.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -126,6 +130,7 @@ avoid_angle_llvmpipe_explicit_lod_bias=0
|
||||
coherent_as_flush=0
|
||||
texture_2d_dumps=""
|
||||
env_overrides="${MOBILEGL_TRACE_ENV:-}"
|
||||
require_inproc=0
|
||||
benchmark=0
|
||||
benchmark_tail_frames=200
|
||||
benchmark_finish=1
|
||||
@@ -172,6 +177,7 @@ while [ "$#" -gt 0 ]; do
|
||||
--coherent-as-flush) coherent_as_flush=1; shift 1 ;;
|
||||
--dump-texture-2d) texture_2d_dumps="$(next_arg "$@")"; shift 2 ;;
|
||||
--env) env_overrides="$(next_arg "$@")"; shift 2 ;;
|
||||
--require-inproc) require_inproc=1; shift 1 ;;
|
||||
--benchmark) benchmark=1; shift 1 ;;
|
||||
--benchmark-tail-frames) benchmark_tail_frames="$(next_arg "$@")"; shift 2 ;;
|
||||
--benchmark-finish) benchmark_finish="$(next_arg "$@")"; shift 2 ;;
|
||||
@@ -518,7 +524,37 @@ run_retrace() {
|
||||
fi
|
||||
fi
|
||||
|
||||
"${PYTHON}" -c 'import json, sys; result = json.load(open(sys.argv[1], encoding="utf-8")); sys.exit(0 if result.get("passed") else f"trace replay failed: {result}")' "${result_dir}/result.json"
|
||||
"${PYTHON}" -c 'import json, sys; result = json.load(open(sys.argv[1], encoding="utf-8")); sys.exit(0 if result.get("passed") else f"trace replay failed: {result}")' "${result_dir}/result.json" || return "$?"
|
||||
if [ "${require_inproc}" -eq 1 ]; then
|
||||
"${PYTHON}" - "${result_dir}/mobilegl.log" "${result_dir}/transport-proof.json" <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
log_path, proof_path = map(Path, sys.argv[1:])
|
||||
text = log_path.read_text(encoding="utf-8", errors="replace") if log_path.is_file() else ""
|
||||
# ConfigLoader's InProcess arm emits this sentence only after selecting the
|
||||
# compiled transport. "Accepted env variable: MOBILEGL_TRANSPORT=inproc" is not it.
|
||||
marker = "Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream"
|
||||
configs = re.findall(r"Config: IPC[^\r\n]*", text)
|
||||
fatals = re.findall(r"^.*Fatal\{.*$", text, re.MULTILINE)
|
||||
required = {"strict": "1", "role-split-state": "1", "run-ahead": "1"}
|
||||
config_ok = bool(configs) and all(
|
||||
all(re.search(r"\b" + re.escape(key) + "=" + value + r"\b", line) for key, value in required.items())
|
||||
for line in configs
|
||||
)
|
||||
passed = marker in text and config_ok and not fatals
|
||||
proof = {"required_transport": "inproc", "library_log": str(log_path),
|
||||
"transport_resolution_marker": marker in text, "ipc_config_lines": configs,
|
||||
"required_ipc_settings": required, "ipc_settings_match": config_ok,
|
||||
"fatal_count": len(fatals), "fatal_lines": fatals, "passed": passed}
|
||||
proof_path.write_text(json.dumps(proof, indent=2), encoding="utf-8")
|
||||
if not passed:
|
||||
sys.exit("trace replay failed actual inproc/strict/role/zero-Fatal proof: " + json.dumps(proof))
|
||||
print("Android retrace: real inproc transport, strict role state and zero Fatal records confirmed")
|
||||
PY
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "${fixture_root}" "${result_root}"
|
||||
|
||||
+12
-8
@@ -15,14 +15,17 @@ def mobileGlBuildServerSpike = {
|
||||
(rootProject.findProperty('mobilegl.buildServerSpike') ?: System.getenv('MOBILEGL_BUILD_SERVER_SPIKE') ?: 'OFF') as String
|
||||
}
|
||||
|
||||
// P2 A/B opt-in. OFF by default, so the plugin flavour and every trace APK nobody asked for
|
||||
// keep the pull shape; the paired two-device A/B (docs/Disaggregated ROADMAP GO/NO-GO items
|
||||
// 3-4) builds one trace APK with it ON and compares it against the OFF build. Either
|
||||
// ./gradlew :app:assembleTraceDebug -Pmobilegl.pipePush=ON
|
||||
// or MOBILEGL_PIPE_PUSH_APK=ON in the environment.
|
||||
// Shared native shape for BOTH plugin and trace APKs. Defaults keep existing
|
||||
// callers on pull; disaggregated CI explicitly selects all three switches.
|
||||
def mobileGlPipePush = {
|
||||
(rootProject.findProperty('mobilegl.pipePush') ?: System.getenv('MOBILEGL_PIPE_PUSH_APK') ?: 'OFF') as String
|
||||
}
|
||||
def mobileGlBuildDisaggregated = {
|
||||
(rootProject.findProperty('mobilegl.buildDisaggregated') ?: System.getenv('MOBILEGL_BUILD_DISAGGREGATED') ?: 'OFF') as String
|
||||
}
|
||||
def mobileGlBuildDisaggregatedInproc = {
|
||||
(rootProject.findProperty('mobilegl.buildDisaggregatedInproc') ?: System.getenv('MOBILEGL_BUILD_DISAGGREGATED_INPROC') ?: mobileGlBuildDisaggregated()) as String
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'top.mobilegl.mobilegl'
|
||||
@@ -36,6 +39,10 @@ android {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DMOBILEGL_LOG_ACTIVE_LEVEL=${mobileGlLogActiveLevel()}"
|
||||
arguments '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON'
|
||||
arguments "-DMOBILEGL_PIPE_PUSH=${mobileGlPipePush()}"
|
||||
arguments "-DMOBILEGL_BUILD_DISAGGREGATED=${mobileGlBuildDisaggregated()}"
|
||||
arguments "-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=${mobileGlBuildDisaggregatedInproc()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,9 +86,6 @@ android {
|
||||
// grow a binary nothing loads. See mobileGlBuildServerSpike
|
||||
// above for the two ways to enable it.
|
||||
arguments "-DMOBILEGL_BUILD_SERVER_SPIKE=${mobileGlBuildServerSpike()}"
|
||||
// P2: the push-shaped library for the paired A/B, same flavour, same
|
||||
// signing, one extra CMake option. See mobileGlPipePush above.
|
||||
arguments "-DMOBILEGL_PIPE_PUSH=${mobileGlPipePush()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user