Support empty resources and project Android services
Sync changed files to Google Drive / sync (push) Successful in 1m2s
Sync changed files to Google Drive / sync (push) Successful in 1m2s
This commit is contained in:
1 parent
c6b5725c35
commit
19ee837986
5 files changed
+77
-6
No files matched your search
@@ -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:
|
||||||
|
|||||||
@@ -1 +1,4 @@
|
|||||||
{}
|
{
|
||||||
|
"schema": "kam-runtime-resources/1",
|
||||||
|
"entries": []
|
||||||
|
}
|
||||||
+42
-1
@@ -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')),
|
||||||
|
|||||||
@@ -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
@@ -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']
|
||||||
|
|||||||
Reference in new issue
Block a user