Initial import
This commit is contained in:
commit
dfcc43c72d
34 files changed
+3057
No files matched your search
Executable
+129
@@ -0,0 +1,129 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user