This commit is contained in:
1 parent
15a968a2f7
commit
b52921e171
38 files changed
+564
-2956
No files matched your search
+73
-119
@@ -1,130 +1,84 @@
|
||||
"""Generate isolated Android CGE targets. No KaM stubs or rewritten game loaders."""
|
||||
from __future__ import annotations
|
||||
import json, os, shutil, urllib.parse, xml.etree.ElementTree as ET
|
||||
import hashlib, os, xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from common import ROOT, COMMIT, VERSION, CPUS, ProbeError, safe_relative, sha256, write_json
|
||||
from source_resolution import build_source_resolution
|
||||
from common import ROOT, ProbeError
|
||||
|
||||
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']
|
||||
|
||||
def imports_source(profile:dict)->tuple[str,list[str]]:
|
||||
units=list(profile['units'])
|
||||
points=profile['id'] in ('core_types','core_streams','integration')
|
||||
streams=profile['id'] in ('core_streams','integration')
|
||||
if points and 'KM_Points' not in units:units.append('KM_Points')
|
||||
head='unit ProbeImports;\n{$mode delphi}{$H+}\ninterface\nuses ProbeReport;\nprocedure RunUpstreamChecks(R:TProbeReport);\nimplementation\n'
|
||||
head+='uses SysUtils'+(' ,'+', '.join(units) if units else '')+';\n'
|
||||
head+='procedure RunUpstreamChecks(R:TProbeReport);\n'
|
||||
if points:
|
||||
head+='var P,Q:TKMPoint;\n'
|
||||
if streams:head+=' S:TKMemoryStreamBinary; I:Integer; U:UnicodeString;\n'
|
||||
head+='begin\n'
|
||||
if not units:
|
||||
head+=" R.Add('upstream.imports','NOT_RUN','Platform-only APK; no KaM modules imported');\n"
|
||||
else:
|
||||
head+=" R.Add('upstream.imports','PASS','Real imported units initialized: "+', '.join(units)+"; this does not execute a game world');\n"
|
||||
if points:
|
||||
head+=''' try
|
||||
P:=KMPoint(17,-3); Q:=KMPointRight(P);
|
||||
R.Check('core.points',(P.X=17) and (P.Y=-3) and (Q.X=18) and (Q.Y=-3),
|
||||
'Calls original KMPoint / KMPointRight, not a local replacement');
|
||||
R.Check('core.point_layout',(SizeOf(TKMPoint)=8) and (SizeOf(TKMPointF)=8),
|
||||
Format('TKMPoint=%d TKMPointF=%d',[SizeOf(TKMPoint),SizeOf(TKMPointF)]));
|
||||
except on E:Exception do R.Add('core.points','FAIL',E.ClassName+': '+E.Message);end;
|
||||
'''
|
||||
if streams:
|
||||
head+=''' try
|
||||
S:=TKMemoryStreamBinary.Create;
|
||||
try
|
||||
S.Write(Integer(123456)); S.Write(P); S.WriteW(UnicodeString('KaM Android'));
|
||||
S.Position:=0; S.Read(I); S.Read(Q); S.ReadW(U);
|
||||
R.Check('core.stream_roundtrip',(I=123456) and (Q.X=P.X) and (Q.Y=P.Y)
|
||||
and (U=UnicodeString('KaM Android')) and (S.Position=S.Size),
|
||||
'Calls original TKMemoryStreamBinary Read/Write/ReadW/WriteW');
|
||||
finally S.Free;end;
|
||||
except on E:Exception do R.Add('core.stream_roundtrip','FAIL',E.ClassName+': '+E.Message);end;
|
||||
'''
|
||||
head+='end;\nend.\n'
|
||||
return head,units
|
||||
_PROJECT_ATTRIBUTES={
|
||||
'name':'kam_mobile', 'caption':'KaM Remake Mobile', 'author':'KaM Mobile',
|
||||
'qualified_name':'org.kammobile.kamremake', 'game_units':'GameInitialize',
|
||||
'compiler':'fpc', 'screen_orientation':'landscape', 'fullscreen_immersive':'true',
|
||||
}
|
||||
|
||||
def asset_inventory(root:Path|None, includes:list[str]|None=None)->list[dict]:
|
||||
if root is None:return []
|
||||
root=root.resolve()
|
||||
if not root.is_dir():raise ProbeError('Asset directory does not exist: '+str(root))
|
||||
if includes:
|
||||
result=[]
|
||||
seen=set()
|
||||
for raw in includes:
|
||||
rel=safe_relative(raw.replace('\\','/'))
|
||||
if rel in seen:continue
|
||||
seen.add(rel);p=root/rel
|
||||
if p.is_symlink():raise ProbeError('Asset symlinks are not allowed: '+str(p))
|
||||
if not p.is_file():raise ProbeError('Requested asset path not found under asset root: '+rel)
|
||||
result.append({'path':rel,'size':p.stat().st_size,'sha256':sha256(p),
|
||||
'url':'castle-data:/game/'+urllib.parse.quote(rel,safe='/')})
|
||||
return result
|
||||
result=[]
|
||||
for p in sorted(root.rglob('*')):
|
||||
if p.is_symlink():raise ProbeError('Asset symlinks are not allowed: '+str(p))
|
||||
if p.is_dir():continue
|
||||
if not p.is_file():raise ProbeError('Not a regular asset: '+str(p))
|
||||
rel=safe_relative(p.relative_to(root).as_posix())
|
||||
if rel=='README.md' or '.git' in p.parts:continue
|
||||
result.append({'path':rel,'size':p.stat().st_size,'sha256':sha256(p),
|
||||
'url':'castle-data:/game/'+urllib.parse.quote(rel,safe='/')})
|
||||
return result
|
||||
def _one_child(parent: ET.Element, tag: str) -> ET.Element:
|
||||
children=parent.findall(tag)
|
||||
if len(children)!=1:raise ProbeError(f'Manifest requires exactly one <{tag}>')
|
||||
return children[0]
|
||||
|
||||
def make_target(project:Path,profile:dict,cpu:str,build_id:str,source:Path|None,
|
||||
validated:dict|None,assets_root:Path|None=None,assets:list[dict]|None=None)->dict:
|
||||
def _manifest_path(path: object, project: Path, kind: str) -> str:
|
||||
try:
|
||||
resolved=Path(path).resolve()
|
||||
if not resolved.is_dir():raise ProbeError(f'{kind} path missing: {resolved}')
|
||||
relative=os.path.relpath(resolved,project).replace(os.sep,'/')
|
||||
if not any(char.isspace() for char in relative):return relative
|
||||
alias=project/'.source-paths'/hashlib.sha256(str(resolved).encode()).hexdigest()
|
||||
alias.parent.mkdir(exist_ok=True)
|
||||
if alias.exists() or alias.is_symlink():
|
||||
if not alias.is_symlink() or alias.resolve()!=resolved:raise ProbeError(f'{kind} path alias collision: {alias}')
|
||||
else:alias.symlink_to(resolved,target_is_directory=True)
|
||||
return os.path.relpath(alias,project).replace(os.sep,'/')
|
||||
except ProbeError:
|
||||
raise
|
||||
except (OSError, TypeError, ValueError, RuntimeError) as error:
|
||||
raise ProbeError(f'Cannot represent {kind} path {path!r}: {error}') from error
|
||||
|
||||
def _permanent_manifest() -> tuple[ET.ElementTree, ET.Element]:
|
||||
path=ROOT/'app/CastleEngineManifest.xml'
|
||||
try:
|
||||
tree=ET.parse(path)
|
||||
except (OSError, ET.ParseError) as error:
|
||||
raise ProbeError(f'Cannot read permanent manifest: {error}') from error
|
||||
root=tree.getroot()
|
||||
if root.tag!='project' or root.attrib!=_PROJECT_ATTRIBUTES:
|
||||
raise ProbeError('Invalid permanent application identity')
|
||||
version=_one_child(root,'version')
|
||||
if version.get('value')!='0.4.0' or version.get('code')!='30':
|
||||
raise ProbeError('Invalid permanent application version')
|
||||
options=_one_child(root,'compiler_options')
|
||||
if [child.tag for child in options] != ['search_paths','defines','custom_options','library_paths']:
|
||||
raise ProbeError('Invalid permanent compiler policy order')
|
||||
search_paths,defines,custom_options,library_paths=(
|
||||
_one_child(options,tag) for tag in ('search_paths','defines','custom_options','library_paths'))
|
||||
if search_paths.findall('path') or library_paths.findall('path'):
|
||||
raise ProbeError('Permanent manifest path slots must be empty')
|
||||
if [item.text for item in defines.findall('define')]!=UPSTREAM_DEFINES:
|
||||
raise ProbeError('Invalid permanent compiler defines')
|
||||
if [item.text for item in custom_options.findall('option')]!=COMPILER_OPTIONS:
|
||||
raise ProbeError('Invalid permanent compiler options')
|
||||
return tree,root
|
||||
|
||||
def materialize_application_project(project: Path, source_resolution: dict) -> dict:
|
||||
"""Create one fresh Castle project from the permanent manifest and RP01 paths."""
|
||||
if project.exists():raise ProbeError('Target directory must be fresh: '+str(project))
|
||||
try:
|
||||
compiler_paths=tuple(source_resolution['compiler_source_paths'])
|
||||
library_paths=tuple(source_resolution['library_paths'])
|
||||
except (KeyError, TypeError) as error:
|
||||
raise ProbeError(f'Invalid source resolution: {error}') from error
|
||||
tree,root=_permanent_manifest()
|
||||
project.mkdir(parents=True)
|
||||
shutil.copytree(ROOT/'code',project/'code')
|
||||
text,units=imports_source(profile)
|
||||
(project/'code/ProbeImports.pas').write_text(text,encoding='utf-8')
|
||||
(project/'code/ProbeBuild.pas').write_text(
|
||||
'unit ProbeBuild;\n{$mode objfpc}{$H+}\ninterface\nconst\n'+
|
||||
f" ProbeBuildId='{build_id}';\n ProbeProfile='{profile['id']}';\n ProbeCpu='{cpu}';\n"+
|
||||
f" ProbeUpstreamCommit='{COMMIT}';\n ProbePointerBytes={CPUS[cpu][3]};\nimplementation\nend.\n")
|
||||
data=project/'data';data.mkdir()
|
||||
entries=assets or []
|
||||
if entries and assets_root is None:raise ProbeError('Missing asset root')
|
||||
for e in entries:
|
||||
dest=data/'game'/e['path'];dest.parent.mkdir(parents=True,exist_ok=True)
|
||||
shutil.copyfile(assets_root/e['path'],dest)
|
||||
if sha256(dest)!=e['sha256']:raise ProbeError('Asset changed while staging: '+e['path'])
|
||||
write_json(data/'assets-manifest.json',{'schema':'kam-probe-assets/1','files':entries})
|
||||
name='kam_probe_'+profile['id']
|
||||
root=ET.Element('project',name=name,caption='KaM Android: '+profile['id'],
|
||||
author='KaM Mobile',qualified_name='org.kammobile.archprobe.'+profile['id'].replace('_',''),
|
||||
game_units=profile.get('game_unit','GameInitialize'),compiler='fpc',screen_orientation='landscape',fullscreen_immersive='true')
|
||||
ET.SubElement(root,'version',value=VERSION,code='30')
|
||||
options=ET.SubElement(root,'compiler_options');paths=ET.SubElement(options,'search_paths')
|
||||
if units:
|
||||
if source is None or validated is None:raise ProbeError('KaM source resolution is required for '+profile['id'])
|
||||
resolution=build_source_resolution(validated,source,project/'code')
|
||||
compiler_source_paths=resolution['compiler_source_paths']
|
||||
library_paths=resolution['library_paths']
|
||||
else:
|
||||
compiler_source_paths=(project/'code',)
|
||||
library_paths=()
|
||||
for p in compiler_source_paths:
|
||||
if not p.is_dir():raise ProbeError('Source path missing: '+str(p))
|
||||
ET.SubElement(paths,'path',value=os.path.relpath(p,project).replace(os.sep,'/'))
|
||||
defines=ET.SubElement(options,'defines')
|
||||
for define in (UPSTREAM_DEFINES if units else []):ET.SubElement(defines,'define').text=define
|
||||
custom=ET.SubElement(options,'custom_options')
|
||||
for flag in COMPILER_OPTIONS:ET.SubElement(custom,'option').text=flag
|
||||
libraries=ET.SubElement(options,'library_paths')
|
||||
for p in library_paths:
|
||||
if not p.is_dir():raise ProbeError('Native library path missing: '+str(p))
|
||||
ET.SubElement(libraries,'path',value=os.path.relpath(p,project).replace(os.sep,'/'))
|
||||
# Android SDK versions deliberately inherited from the locked CGE image.
|
||||
ET.indent(root)
|
||||
ET.ElementTree(root).write(project/'CastleEngineManifest.xml',encoding='utf-8',xml_declaration=True)
|
||||
description={'profile':profile['id'],'name':name,'package':'org.kammobile.archprobe.'+profile['id'].replace('_',''),
|
||||
'cpu':cpu,'units':units,'game_unit':profile.get('game_unit','GameInitialize'),'source_paths':[str(p) for p in compiler_source_paths],
|
||||
'compiler_options':COMPILER_OPTIONS,'upstream_defines':UPSTREAM_DEFINES if units else [],
|
||||
'assets':entries,'shims':False}
|
||||
write_json(project/'target.json',description)
|
||||
return description
|
||||
data_root=project/'data';data_root.mkdir()
|
||||
options=_one_child(root,'compiler_options')
|
||||
for tag,paths,kind in (('search_paths',compiler_paths,'Source'),('library_paths',library_paths,'Native library')):
|
||||
slot=_one_child(options,tag)
|
||||
for child in list(slot):slot.remove(child)
|
||||
for path in paths:ET.SubElement(slot,'path',value=_manifest_path(path,project,kind))
|
||||
ET.indent(tree)
|
||||
tree.write(project/'CastleEngineManifest.xml',encoding='utf-8',xml_declaration=True)
|
||||
version=_one_child(root,'version')
|
||||
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')),
|
||||
'game_unit':root.get('game_units'),'compiler_source_paths':compiler_paths,'library_paths':library_paths}
|
||||
Reference in new issue
Block a user