Files
kam-mobile/tools/generate.py
T

84 lines
4.7 KiB
Python
Raw Normal View History

2026-09-11 18:18:40 +05:00
"""Generate isolated Android CGE targets. No KaM stubs or rewritten game loaders."""
from __future__ import annotations
2026-09-14 20:47:20 +05:00
import hashlib, os, xml.etree.ElementTree as ET
2026-09-11 18:18:40 +05:00
from pathlib import Path
2026-09-14 20:47:20 +05:00
from common import ROOT, ProbeError
2026-09-11 18:18:40 +05:00
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']
2026-09-14 20:47:20 +05:00
_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',
}
2026-09-11 18:18:40 +05:00
2026-09-14 20:47:20 +05:00
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]
2026-09-11 18:18:40 +05:00
2026-09-14 20:47:20 +05:00
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."""
2026-09-11 18:18:40 +05:00
if project.exists():raise ProbeError('Target directory must be fresh: '+str(project))
2026-09-14 20:47:20 +05:00
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()
2026-09-11 18:18:40 +05:00
project.mkdir(parents=True)
2026-09-14 20:47:20 +05:00
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}