Support empty resources and project Android services
Sync changed files to Google Drive / sync (push) Successful in 1m2s

This commit is contained in:
admin_d committed 2026-09-14 23:49:46 +05:00
1 parent c6b5725c35
commit 19ee837986
5 files changed
+77 -6

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ Private runtime-resource contents live under `private-assets/`. `config/runtime-
The permanent application definition is `app/CastleEngineManifest.xml`: `kam_mobile`, `org.kammobile.kamremake`, and `GameInitialize`. The permanent application definition is `app/CastleEngineManifest.xml`: `kam_mobile`, `org.kammobile.kamremake`, and `GameInitialize`.
Project-owned Android service templates, when needed, belong in `app/android_services/<service_name>/`. They are copied into the transient application and exposed to Castle only by the normal disposable-container build; use `./build-android.sh`, not `--local`, when they are present.
## Optional device helper ## Optional device helper
After a successful current build, optionally run: After a successful current build, optionally run:
+4 -1
View File
@@ -1 +1,4 @@
{} {
"schema": "kam-runtime-resources/1",
"entries": []
}
+42 -1
View File
@@ -1,11 +1,12 @@
"""Generate isolated Android CGE targets. No KaM stubs or rewritten game loaders.""" """Generate isolated Android CGE targets. No KaM stubs or rewritten game loaders."""
from __future__ import annotations from __future__ import annotations
import hashlib, os, xml.etree.ElementTree as ET import hashlib, os, re, shutil, xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
from common import ROOT, ProbeError from common import ROOT, ProbeError
UPSTREAM_DEFINES=['DBG_SKIP_SECURE_AUTH'] # Existing upstream switch; private multiplayer authentication is out of scope. UPSTREAM_DEFINES=['DBG_SKIP_SECURE_AUTH'] # Existing upstream switch; private multiplayer authentication is out of scope.
COMPILER_OPTIONS=['-B','-CX-','-XX-','-k--no-undefined','-k--no-gc-sections'] COMPILER_OPTIONS=['-B','-CX-','-XX-','-k--no-undefined','-k--no-gc-sections']
_ANDROID_SERVICE_NAME=re.compile(r'[A-Za-z][A-Za-z0-9_]*\Z')
_PROJECT_ATTRIBUTES={ _PROJECT_ATTRIBUTES={
'name':'kam_mobile', 'caption':'KaM Remake Mobile', 'author':'KaM Mobile', 'name':'kam_mobile', 'caption':'KaM Remake Mobile', 'author':'KaM Mobile',
@@ -60,6 +61,33 @@ def _permanent_manifest() -> tuple[ET.ElementTree, ET.Element]:
raise ProbeError('Invalid permanent compiler options') raise ProbeError('Invalid permanent compiler options')
return tree,root return tree,root
def android_service_roots(root: Path) -> tuple[Path,...]:
"""Return validated immediate project-owned Android service directories."""
try:
if not root.exists(): return ()
if root.is_symlink() or not root.is_dir():
raise ProbeError('Android services root must be a real directory: '+str(root))
resolved_root=root.resolve()
services=[]
for service in sorted(root.iterdir(),key=lambda path:path.name):
if service.is_symlink():
raise ProbeError('Android service must not be a symlink: '+str(service))
if not service.is_dir(): continue
if not _ANDROID_SERVICE_NAME.fullmatch(service.name):
raise ProbeError('Unsafe Android service name: '+repr(service.name))
resolved_service=service.resolve()
if not resolved_service.is_relative_to(resolved_root):
raise ProbeError('Android service escapes its root: '+str(service))
for path in service.rglob('*'):
if path.is_symlink():
raise ProbeError('Android service contains a symlink: '+str(path))
services.append(service)
return tuple(services)
except ProbeError:
raise
except (OSError, ValueError, RuntimeError) as error:
raise ProbeError(f'Cannot inspect Android services: {error}') from error
def materialize_application_project(project: Path, source_resolution: dict) -> dict: def materialize_application_project(project: Path, source_resolution: dict) -> dict:
"""Create one fresh Castle project from the permanent manifest and RP01 paths.""" """Create one fresh Castle project from the permanent manifest and RP01 paths."""
if project.exists():raise ProbeError('Target directory must be fresh: '+str(project)) if project.exists():raise ProbeError('Target directory must be fresh: '+str(project))
@@ -78,6 +106,19 @@ def materialize_application_project(project: Path, source_resolution: dict) -> d
for path in paths:ET.SubElement(slot,'path',value=_manifest_path(path,project,kind)) for path in paths:ET.SubElement(slot,'path',value=_manifest_path(path,project,kind))
ET.indent(tree) ET.indent(tree)
tree.write(project/'CastleEngineManifest.xml',encoding='utf-8',xml_declaration=True) tree.write(project/'CastleEngineManifest.xml',encoding='utf-8',xml_declaration=True)
source_services=ROOT/'app/android_services'
services=android_service_roots(source_services)
if services:
destination_root=project/'android_services'
destination_root.mkdir()
for service in services:
destination=destination_root/service.name
if destination.exists() or destination.is_symlink():
raise ProbeError('Android service destination already exists: '+str(destination))
try:
shutil.copytree(service,destination)
except OSError as error:
raise ProbeError(f'Cannot copy Android service {service.name}: {error}') from error
version=_one_child(root,'version') version=_one_child(root,'version')
return {'project_dir':project,'data_root':data_root,'name':root.get('name'),'caption':root.get('caption'), return {'project_dir':project,'data_root':data_root,'name':root.get('name'),'caption':root.get('caption'),
'package':root.get('qualified_name'),'version':version.get('value'),'version_code':int(version.get('code')), 'package':root.get('qualified_name'),'version':version.get('value'),'version_code':int(version.get('code')),
+2 -2
View File
@@ -27,8 +27,8 @@ def _load_runtime_resource_manifest() -> tuple[dict[str, str], ...]:
raise ProbeError('Invalid runtime-resource manifest fields') raise ProbeError('Invalid runtime-resource manifest fields')
if value['schema'] != _SCHEMA: if value['schema'] != _SCHEMA:
raise ProbeError('Invalid runtime-resource manifest schema') raise ProbeError('Invalid runtime-resource manifest schema')
if not isinstance(value['entries'], list) or not value['entries']: if not isinstance(value['entries'], list):
raise ProbeError('Runtime-resource entries must be a non-empty array') raise ProbeError('Runtime-resource entries must be an array')
seen_paths: set[str] = set() seen_paths: set[str] = set()
entries = [] entries = []
+27 -2
View File
@@ -9,7 +9,7 @@ from common import (ROOT, COMMIT, CPUS, ProbeError, stamp, sha256, write_json, r
from source_fetch import fetch, verify from source_fetch import fetch, verify
from source_audit import inventory from source_audit import inventory
from source_resolution import load_source_contract, validate_source_contract, build_source_resolution from source_resolution import load_source_contract, validate_source_contract, build_source_resolution
from generate import materialize_application_project from generate import android_service_roots, materialize_application_project
from artifacts import inspect_elf, inspect_apk from artifacts import inspect_elf, inspect_apk
from resource_staging import stage_runtime_resources from resource_staging import stage_runtime_resources
@@ -108,6 +108,30 @@ def _prepare(build_id:str, diagnostics:bool=True)->tuple[Path,Path]:
if diagnostics:tool_versions(logs) if diagnostics:tool_versions(logs)
return work,logs return work,logs
def stage_android_services(project:Path, logs:Path)->None:
services=android_service_roots(project/'android_services')
if not services:return
if os.environ.get('KAM_CONTAINER_BUILD')!='1':
raise ProbeError('Project-owned Android services require the normal disposable-container build: ./build-android.sh')
castle_path=os.environ.get('CASTLE_ENGINE_PATH')
if not castle_path:
raise ProbeError('Cannot stage project-owned Android services: CASTLE_ENGINE_PATH is not set')
service_root=Path(castle_path)/'tools/build-tool/data/android/services'
if not service_root.is_dir():
raise ProbeError('Cannot stage project-owned Android services: Castle service root is missing: '+str(service_root))
try:
with (logs/'android-services.log').open('w',encoding='utf-8') as log:
for service in services:
destination=service_root/service.name
if destination.exists() or destination.is_symlink():
raise ProbeError('Project-owned Android service collides with Castle built-in service: '+service.name)
shutil.copytree(service,destination)
log.write(f'project service: {service.name}\nsource: {service}\ncastle destination: {destination}\nstatus: staged\n')
except ProbeError:
raise
except OSError as error:
raise ProbeError(f'Cannot stage project-owned Android services: {error}') from error
def build_local(a:argparse.Namespace,build_id:str)->int: def build_local(a:argparse.Namespace,build_id:str)->int:
work,logs=_prepare(build_id, not bool(os.environ.get('KAM_BUILD_ID'))) work,logs=_prepare(build_id, not bool(os.environ.get('KAM_BUILD_ID')))
app=None; resources=None; compile={'status':'NOT_RUN','exit_code':None}; package={'status':'NOT_RUN','exit_code':None} app=None; resources=None; compile={'status':'NOT_RUN','exit_code':None}; package={'status':'NOT_RUN','exit_code':None}
@@ -119,6 +143,7 @@ def build_local(a:argparse.Namespace,build_id:str)->int:
resolution=build_source_resolution(validated,vendor_root,ROOT/'code') resolution=build_source_resolution(validated,vendor_root,ROOT/'code')
descriptor=materialize_application_project(work/'application',resolution);app=_application(descriptor) descriptor=materialize_application_project(work/'application',resolution);app=_application(descriptor)
resources=stage_runtime_resources(descriptor['data_root']) resources=stage_runtime_resources(descriptor['data_root'])
stage_android_services(descriptor['project_dir'],logs)
except (ProbeError,OSError,ValueError,KeyError) as error: except (ProbeError,OSError,ValueError,KeyError) as error:
return _finish(_record(build_id,a.cpu,'BLOCKED',app,None,compile,package,error=str(error))) return _finish(_record(build_id,a.cpu,'BLOCKED',app,None,compile,package,error=str(error)))
if not shutil.which('castle-engine'): if not shutil.which('castle-engine'):
@@ -183,7 +208,7 @@ def docker_build(a:argparse.Namespace,build_id:str)->int:
if image is None:raise ProbeError('Image has no immutable RepoDigest') if image is None:raise ProbeError('Image has no immutable RepoDigest')
write_json(lock,{'image':image,'image_id':info['Id'],'resolved_from':IMAGE_TAG,'locked_at':stamp(),'note':'Subsequent runs use this digest, never silently advance the tag.'}) write_json(lock,{'image':image,'image_id':info['Id'],'resolved_from':IMAGE_TAG,'locked_at':stamp(),'note':'Subsequent runs use this digest, never silently advance the tag.'})
cmd=['docker','run','--rm','--platform=linux/amd64','--user','0:0','-e','HOME=/work/.cache/home', cmd=['docker','run','--rm','--platform=linux/amd64','--user','0:0','-e','HOME=/work/.cache/home',
'-e','KAM_BUILD_ID='+build_id,'-v',str(ROOT)+':/work','-w','/work'] '-e','KAM_BUILD_ID='+build_id,'-e','KAM_CONTAINER_BUILD=1','-v',str(ROOT)+':/work','-w','/work']
if os.environ.get('GITHUB_TOKEN'):cmd+=['-e','GITHUB_TOKEN'] if os.environ.get('GITHUB_TOKEN'):cmd+=['-e','GITHUB_TOKEN']
cmd+=['--entrypoint','python3',image,'tools/runner.py','--local','--cpu',a.cpu,'--timeout',str(a.timeout)] cmd+=['--entrypoint','python3',image,'tools/runner.py','--local','--cpu',a.cpu,'--timeout',str(a.timeout)]
if a.no_fetch:cmd+=['--no-fetch'] if a.no_fetch:cmd+=['--no-fetch']