129 lines
7.3 KiB
Python
129 lines
7.3 KiB
Python
"""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
|
||
|
|
from pathlib import Path
|
||
|
|
from common import ROOT, COMMIT, VERSION, CPUS, ProbeError, safe_relative, sha256, write_json
|
||
|
|
from source_audit import search_paths
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
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 make_target(project:Path,profile:dict,cpu:str,build_id:str,source:Path|None,
|
||
|
|
extras:dict,assets_root:Path|None=None,assets:list[dict]|None=None)->dict:
|
||
|
|
if project.exists():raise ProbeError('Target directory must be fresh: '+str(project))
|
||
|
|
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')
|
||
|
|
ET.SubElement(paths,'path',value='code/')
|
||
|
|
include=[]
|
||
|
|
if units:
|
||
|
|
if source is None:raise ProbeError('KaM source is required for '+profile['id'])
|
||
|
|
include=search_paths(source)
|
||
|
|
include.extend(Path(p).resolve() for p in extras.get('source_paths',[]))
|
||
|
|
for p in include:
|
||
|
|
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 extras.get('library_paths',[]):
|
||
|
|
p=Path(p).resolve()
|
||
|
|
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 include],
|
||
|
|
'compiler_options':COMPILER_OPTIONS,'upstream_defines':UPSTREAM_DEFINES if units else [],
|
||
|
|
'assets':entries,'shims':False}
|
||
|
|
write_json(project/'target.json',description)
|
||
|
|
return description
|