Files
seeddsandAnimax Deng 6da1882871 NSP script: --prepare-xcode makes a per-game Xcode project to sign and run with your own developer account (GitHub PR #59)
Each game gets its own project in <NSP folder>/<title ID>_build/xcode, registered in a shared NxNative.xcworkspace; a target phase copies the RomFS, shader libraries and add-on content into the app before Xcode signs it, and preparing again keeps the bundle ID and team chosen in Xcode. The unsigned IPA build is unchanged.
On top of the PR: the resource phase stops when the profile's shader chain is no longer the prepared shaders' (the host loads only its own chain), prepared projects ask for the Increased Memory Limit capability (DM_ENTITLEMENTS overrides), the source links are made again on every preparation, and scheme names keep non-ASCII letters. The PR's TOML launcher is left out.

Co-authored-by: Animax Deng <Animax.deng@gmail.com>
2026-10-23 00:37:52 +08:00

62 lines
2.9 KiB
Python

"""Per-game Xcode signing settings and a workspace containing prepared games."""
import json
from pathlib import Path
import subprocess
import xml.etree.ElementTree as ET
def signing_settings(project):
"""Read only the app's Release signing settings, including changes made in Xcode."""
pbx = Path(project) / 'project.pbxproj'
if not pbx.is_file(): return {}
result = subprocess.run(['/usr/bin/plutil', '-convert', 'json', '-o', '-', str(pbx)], capture_output=True, text=True)
if result.returncode: raise ValueError(f'cannot read Xcode project: {pbx}')
objects = json.loads(result.stdout)['objects']
target = next((v for v in objects.values() if v.get('isa') == 'PBXNativeTarget' and v.get('name') == 'DmIOS'), None)
if not target: return {}
configs = objects[target['buildConfigurationList']]['buildConfigurations']
settings = next((objects[c]['buildSettings'] for c in configs if objects[c]['name'] == 'Release'), {})
return {k: settings[k] for k in ('PRODUCT_BUNDLE_IDENTIFIER', 'DEVELOPMENT_TEAM') if isinstance(settings.get(k), str)}
def workspace_tree(workspace):
path = Path(workspace) / 'contents.xcworkspacedata'
try:
return ET.parse(path) if path.exists() else ET.ElementTree(ET.Element('Workspace', version='1.0'))
except ET.ParseError as e:
raise ValueError(f'cannot read workspace {path}: {e}') from e
def workspace_projects(workspace):
for ref in workspace_tree(workspace).iter('FileRef'):
kind, _, path = ref.get('location', '').partition(':')
if kind == 'absolute': yield Path(path).resolve()
def check_bundle_id(workspace, project, bundle_id):
for other in workspace_projects(workspace):
if other == Path(project).resolve() or not other.exists(): continue
used = signing_settings(other).get('PRODUCT_BUNDLE_IDENTIFIER', '')
if used.lower() == bundle_id.lower():
raise ValueError(f'bundle ID {bundle_id} is already used by {other}; choose a different --bundle-id for this game')
def register_project(workspace, project, name, derived):
"""Add/update one project without replacing the other games or workspace settings."""
from gen_resources import workspace_settings
workspace, project = Path(workspace), Path(project).resolve()
tree = workspace_tree(workspace)
location = f'absolute:{project}'
group = next((g for g in tree.getroot().findall('Group') if any(r.get('location') == location for r in g.findall('FileRef'))), None)
if group is None:
group = ET.SubElement(tree.getroot(), 'Group', location='container:')
ET.SubElement(group, 'FileRef', location=location)
group.set('name', name)
workspace.mkdir(parents=True, exist_ok=True)
ET.indent(tree, space=' ')
path = workspace / 'contents.xcworkspacedata'
temp = path.with_suffix('.tmp')
tree.write(temp, encoding='utf-8', xml_declaration=True)
temp.replace(path)
workspace_settings(workspace, derived)