Build self-contained macOS client and verify native beta path
This commit is contained in:
Executable
+104
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run the exported native 40250 macOS client from an unrelated working directory.
|
||||
# Credentials come only from MT_LIVE_LOGIN / MT_LIVE_PASSWORD in the environment.
|
||||
set -euo pipefail
|
||||
set +x
|
||||
cd "$(dirname "$0")/.."
|
||||
repo="$PWD"
|
||||
app="${1:-}"
|
||||
output="${2:-$repo/build/beta-probe-$(date +%Y%m%d-%H%M%S)-$$}"
|
||||
if [ -z "$app" ] || [ ! -d "$app" ]; then echo 'BETA_PROBE: BLOCKED provide an exported .app' >&2; exit 2; fi
|
||||
if [ -z "${MT_LIVE_HOST:-}" ] || [ -z "${MT_LIVE_LOGIN:-}" ] || [ -z "${MT_LIVE_PASSWORD:-}" ]; then
|
||||
echo 'BETA_PROBE: BLOCKED MT_LIVE_HOST, MT_LIVE_LOGIN and MT_LIVE_PASSWORD are required' >&2
|
||||
exit 2
|
||||
fi
|
||||
app="$(cd "$app" && pwd)"
|
||||
if [ -e "$output" ] && [ -n "$(find "$output" -mindepth 1 -print -quit)" ]; then
|
||||
echo 'BETA_PROBE: BLOCKED output directory is not empty' >&2
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "$output"
|
||||
output="$(cd "$output" && pwd)"
|
||||
engine="$app/Contents/MacOS/mtgodot-poc"
|
||||
client="$app/Contents/Resources/40250/Client"
|
||||
if [ ! -x "$engine" ] || [ ! -s "$client/pack/Index" ] || ! codesign --verify --strict "$app" >/dev/null 2>&1; then
|
||||
echo 'BETA_PROBE: BLOCKED bundle executable, 40250 pack or signature is invalid' >&2
|
||||
exit 2
|
||||
fi
|
||||
bash script/playable_build_info.sh "$app" >"$output/candidate.json"
|
||||
python3 - "$client" "$app/Contents/Resources/beta-build.json" "$output/candidate.json" <<'PY'
|
||||
import hashlib, json, pathlib, sys
|
||||
client, build_manifest, output = map(pathlib.Path, sys.argv[1:])
|
||||
digest = hashlib.sha256()
|
||||
for path in sorted(p for p in client.rglob('*') if p.is_file()):
|
||||
relative = path.relative_to(client).as_posix().encode()
|
||||
digest.update(len(relative).to_bytes(4, 'big'))
|
||||
digest.update(relative)
|
||||
with path.open('rb') as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
|
||||
digest.update(chunk)
|
||||
candidate = json.loads(output.read_text())
|
||||
candidate.update(json.loads(build_manifest.read_text()))
|
||||
candidate['client_resources_sha256'] = digest.hexdigest()
|
||||
output.write_text(json.dumps(candidate, indent=2) + '\n')
|
||||
PY
|
||||
|
||||
# Python owns exactly this child, enforces the wall-clock deadline, and writes only the process
|
||||
# code and status to its report. The password is inherited by the child, never placed in argv.
|
||||
python3 - "$engine" "$output" <<'PY'
|
||||
import datetime, json, os, pathlib, platform, re, subprocess, sys, tempfile
|
||||
engine, output = sys.argv[1:]
|
||||
root = pathlib.Path(output)
|
||||
env = os.environ.copy()
|
||||
for key in ('MT_40250_CLIENT', 'MT_ASSETS', 'MT_ASSETS_ZIP', 'MT_PYTHON_STDLIB'):
|
||||
env.pop(key, None)
|
||||
env['MT_TEST_MODE'] = 'beta_probe'
|
||||
env['MT_BETA_OUTPUT'] = output
|
||||
env['MT_BETA_REQUIRE_BUNDLE'] = '1'
|
||||
timed_out = False
|
||||
with tempfile.TemporaryDirectory(prefix='mt-beta-cwd-') as cwd, (root / 'client.log').open('wb') as log:
|
||||
proc = subprocess.Popen([engine], cwd=cwd, env=env, stdout=log, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
code = proc.wait(timeout=180)
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
proc.terminate()
|
||||
try:
|
||||
code = proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
code = proc.wait()
|
||||
client = None
|
||||
try:
|
||||
client = json.loads((root / 'client-report.json').read_text())
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
log_path = root / 'client.log'
|
||||
log_text = log_path.read_text(errors='replace')
|
||||
# Native trace output is outside the probe's control; keep account credentials out of artifacts.
|
||||
for secret in (env.get('MT_LIVE_PASSWORD', ''), env.get('MT_LIVE_LOGIN', '')):
|
||||
if secret:
|
||||
log_text = log_text.replace(secret, '[REDACTED]')
|
||||
log_path.write_text(log_text)
|
||||
errors = []
|
||||
if timed_out: errors.append('wall_clock_timeout')
|
||||
if code != 0: errors.append('client_exit_nonzero')
|
||||
if not isinstance(client, dict) or client.get('route') != 'native_40250' or client.get('status') != 'PASS':
|
||||
errors.append('native_client_report_missing_or_failed')
|
||||
if not (root / 'game.png').is_file(): errors.append('screenshot_missing')
|
||||
if re.search(r'SCRIPT ERROR|Parse Error|^ERROR:|leaked at exit|shaders of type .* were never freed', log_text, re.M):
|
||||
errors.append('client_log_error')
|
||||
if re.search(r'Cannot open property pack|CArea::LoadObject Property\(\d+\) Load ERROR', log_text):
|
||||
errors.append('map_property_load_error')
|
||||
candidate = json.loads((root / 'candidate.json').read_text())
|
||||
report = {'schema_version': 1, 'status': 'PASS' if not errors else 'FAIL',
|
||||
'tested_at_utc': datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
'device': {'macos': platform.mac_ver()[0], 'arch': platform.machine()},
|
||||
'candidate': candidate, 'process_code': code, 'timed_out': timed_out,
|
||||
'cases': client.get('cases', []) if isinstance(client, dict) else [],
|
||||
'map': client.get('map', '') if isinstance(client, dict) else '',
|
||||
'errors': errors}
|
||||
(root / 'report.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
print('BETA_PROBE: ' + report['status'] + ' report=' + str(root / 'report.json'))
|
||||
sys.exit(0 if not errors else 1)
|
||||
PY
|
||||
Reference in New Issue
Block a user