"""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_resolution import build_source_resolution 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, validated:dict|None,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') 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