darling/tools/generate-xcode-stubs.py
Ariel Abreu 8974de2700 Add a script to generate minimal stubs for some frameworks needed by Xcode
This is mainly used for the modular build, so that we have
dependency-free stubs for certain GUI frameworks that Xcode loads but
doesn't actually use in the CLI.
2023-05-12 10:01:53 -07:00

703 lines
24 KiB
Python
Executable File

#!/usr/bin/env python3
import os
from typing import List, Dict
import subprocess
import re
import pathlib
from enum import Enum
import shutil
import datetime
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
#
# USING THIS SCRIPT:
# * edit `DYLDINFO`, `OTOOL`, and `NM` to refer to the corresponding programs to analyze x86_64 Mach-Os.
# * edit `XCODE_PATH` to point to the Xcode application.
# * edit `SYSTEM_ROOT` to point to the root of a macOS or stock Darling installation.
# * if desired, edit `OUT_DIR` to point to where the stub frameworks will be generated; the default is the best in most cases.
#
###
# EDIT THESE TO USE SCRIPT
###
DYLDINFO = 'x86_64-apple-darwin11-dyldinfo'
OTOOL = 'x86_64-apple-darwin11-otool'
NM = 'x86_64-apple-darwin11-nm'
XCODE_PATH = '/data/darling/Applications/Xcode.app'
SYSTEM_ROOT = '/usr/local/libexec/darling'
OUT_DIR = os.path.join(SCRIPT_DIR, '..', 'src', 'frameworks', 'dev-stubs')
###
# DON'T EDIT ANYTHING ELSE
###
#
# most of this was just trial-and-errored until i got something that worked.
# in particular, the RPATH computations are iffy, but they get the job done.
#
MACH_BINARY_MIME_TYPE = 'application/x-mach-binary'
XCODE_MAIN_BINARY = os.path.join(XCODE_PATH, 'Contents', 'MacOS', 'Xcode')
# extra RPATHs added to the RPATHs of plugins (not the main Xcode binary) and their dependencies.
EXTRA_PLUGIN_RPATHS = [
os.path.join(XCODE_PATH, 'Contents', 'SharedFrameworks'),
os.path.join(XCODE_PATH, 'Contents', 'OtherFrameworks'),
os.path.join(XCODE_PATH, 'Contents', 'Frameworks'),
os.path.join(XCODE_PATH, 'Contents', 'PlugIns'),
os.path.join(XCODE_PATH, 'Contents', 'Developer', 'Platforms', 'MacOSX.platform', 'Developer', 'Library', 'Frameworks'),
os.path.join(XCODE_PATH, 'Contents', 'Developer', 'Platforms', 'MacOSX.platform', 'Developer', 'usr', 'lib'),
os.path.join(XCODE_PATH, 'Contents', 'Developer', 'Library', 'Frameworks'),
os.path.join(XCODE_PATH, 'Contents', 'Developer', 'Platforms', 'MacOSX.platform', 'Developer', 'Library', 'PrivateFrameworks'),
os.path.join(XCODE_PATH, 'Contents', 'PlugIns', 'Xcode3Core.ideplugin', 'Contents', 'Frameworks'),
os.path.join(XCODE_PATH, 'Contents', 'SharedFrameworks', 'XCBuild.framework', 'Versions', 'A', 'Frameworks'),
]
# RPATH templates to try to add for each binary, if they exist.
# `{}` is substituted with the full path to the binary.
TRY_PATH_TEMPLATES = [
'{}/../Frameworks',
'{}/Frameworks',
'{}/../SharedFrameworks',
'{}/SharedFrameworks',
'{}/../OtherFrameworks',
'{}/OtherFrameworks',
'{}/../SystemFrameworks',
'{}/SystemFrameworks',
'{}/../PlugIns',
'{}/PlugIns',
]
# these are binaries that are repeated often throughout the source and we don't really care about them.
# it's mainly just lots of copies of the Swift system libraries.
EXCLUDED_BINARIES = [
'libswiftAccelerate.dylib',
'libswiftAppKit.dylib',
'libswiftAVFoundation.dylib',
'libswiftCloudKit.dylib',
'libswiftCompression.dylib',
'libswiftContacts.dylib',
'libswiftCoreAudio.dylib',
'libswiftCoreData.dylib',
'libswiftCore.dylib',
'libswiftCoreFoundation.dylib',
'libswiftCoreGraphics.dylib',
'libswiftCoreImage.dylib',
'libswiftCoreLocation.dylib',
'libswiftCoreMedia.dylib',
'libswiftCoreMIDI.dylib',
'libswiftCryptoTokenKit.dylib',
'libswiftDarwin.dylib',
'libswift_Differentiation.dylib',
'libswiftDispatch.dylib',
'libswiftFoundation.dylib',
'libswiftGameplayKit.dylib',
'libswiftGLKit.dylib',
'libswiftIntents.dylib',
'libswiftIOKit.dylib',
'libswiftMapKit.dylib',
'libswiftMetal.dylib',
'libswiftMetalKit.dylib',
'libswiftModelIO.dylib',
'libswiftNaturalLanguage.dylib',
'libswiftNetwork.dylib',
'libswiftObjectiveC.dylib',
'libswiftOpenCL.dylib',
'libswiftos.dylib',
'libswiftPhotos.dylib',
'libswiftQuartzCore.dylib',
'libswiftRemoteMirror.dylib',
'libswiftSafariServices.dylib',
'libswiftSceneKit.dylib',
'libswiftsimd.dylib',
'libswiftSpriteKit.dylib',
'libswiftSwiftLang.dylib',
'libswiftSwiftOnoneSupport.dylib',
'libswiftVision.dylib',
'libswiftXCTest.dylib',
'libswiftXPC.dylib',
]
# these are the short names for the binaries we want to generate stubs for
IMPORTANT_BINARIES = [
'AppKit',
'AudioToolbox',
'Cocoa',
'CoreData',
'CoreGraphics',
'CoreText',
'ImageIO',
'OpenGL',
'QuartzCore',
]
COPYRIGHT_HEADER = """
/*
* This file is part of Darling.
*
* Copyright (C) {} Darling Developers
*
* Darling is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Darling is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Darling. If not, see <http://www.gnu.org/licenses/>.
*/
"""
STUB_HEADER = """
#include <stdlib.h>
#include <stdio.h>
static int verbose = 0;
__attribute__((constructor))
static void initme(void) {
verbose = getenv("STUB_VERBOSE") != NULL;
}
void __simple_kprintf(const char* format, ...) __attribute__((format(printf, 1, 2)));
#define LOG_FUNC __simple_kprintf
"""
TEMPLATE_STUB = """
void* {0}(void) {{
if (verbose) LOG_FUNC("STUB: {0} called\\n");
return NULL;
}};
"""
TEMPLATE_INTERFACE = """
@interface {} : NSObject
@end
"""
TEMPLATE_CATEGORY = """
@interface {}
@end
"""
TEMPLATE_IMPL = """
@implementation {}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{{
return [NSMethodSignature signatureWithObjCTypes: "v@:"];
}}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{{
NSLog(@"Stub called: %@ in %@", NSStringFromSelector([anInvocation selector]), [self class]);
}}
"""
TEMPLATE_METHOD = """
{0} (id){1}
{{
NSLog(@"Stub called: %@ in %@", NSStringFromSelector(_cmd), [self class]);
return nil;
}}
"""
CMAKE_TEMPLATE = """
project({0}_stub)
set(DYLIB_COMPAT_VERSION "1.0.0")
set(DYLIB_CURRENT_VERSION "1.0.0")
set(FRAMEWORK_VERSION "{1}")
add_framework({0}
FAT
CURRENT_VERSION
VERSION ${{FRAMEWORK_VERSION}}
TARGET_NAME {0}${{STUB_SUFFIX}}
${{NO_INSTALL_ARG}}
SOURCES
src/classes.m
src/main.m
DEPENDENCIES
system
Foundation
)
#target_include_directories({0}${{STUB_SUFFIX}} BEFORE PRIVATE include)
"""
# most variables are just string keys, so that's the default.
# these overrides for the definitions of certain variables prevent them from being defined as strings and instead define them with the appropriate values.
VAR_OVERRIDES = {
'AppKit': {
'NSUnderlineByWordMask': 'NSUInteger NSUnderlineByWordMask = 0x8000;',
'NSApp': 'NSObject *NSApp = nil;',
'NSFontWeightBold': 'const CGFloat NSFontWeightBold = 0x3fd99999a0000000;',
'NSFontWeightMedium': 'const CGFloat NSFontWeightMedium = 0x3fcd70a3e0000000;',
'NSFontWeightRegular': 'const CGFloat NSFontWeightRegular = 0x0000000000000000;',
'NSFontWeightSemibold': 'const CGFloat NSFontWeightSemibold = 0x3fd3333340000000;',
'NSFontWeightThin': 'const CGFloat NSFontWeightThin = 0xbfe3333340000000;',
'NSFontWeightLight': 'const CGFloat NSFontWeightLight = 0xbfd99999a0000000;',
'NSFontWeightUltraLight': 'const CGFloat NSFontWeightUltraLight = 0xbfe99999a0000000;',
'NSFontWeightBlack': 'const CGFloat NSFontWeightBlack = 0x3fe3d70a40000000;',
'NSFontWeightHeavy': 'const CGFloat NSFontWeightHeavy = 0x3fe1eb8520000000;',
'NSViewNoIntrinsicMetric': 'const CGFloat NSViewNoIntrinsicMetric = 0xbff0000000000000;',
'NSAppKitVersionNumber': 'const double NSAppKitVersionNumber = 1504;',
},
'CoreGraphics': {
'CGRectZero': 'const CGRect CGRectZero = {{0, 0}, {0, 0}};',
'CGRectNull': 'const CGRect CGRectNull = {{INFINITY, INFINITY}, {0, 0}};',
'CGPointZero': 'const CGPoint CGPointZero = {0, 0};',
'CGSizeZero': 'const CGSize CGSizeZero = {0, 0};',
'CGRectInfinite': 'const CGRect CGRectInfinite = {{0, 0}, {INFINITY, INFINITY}};',
'kCGDisplayPixelHeight': 'unsigned int kCGDisplayPixelHeight = 1;',
'kCGDisplayPixelWidth': 'unsigned int kCGDisplayPixelWidth = 1;',
},
'QuartzCore': {
'CATransform3DIdentity': 'const CATransform3D CATransform3DIdentity = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};'
},
}
# sometimes, functions are incorrectly detected as variables. this can be used to override certain symbols as functions in those cases.
# note that this should rarely be necessary, since we double-check with `nm`.
FUNC_OVERRIDES = {
#'CoreGraphics': [
# 'CGRectGetMinX',
# 'CGRectGetWidth',
#],
}
# this is necessary to ensure certain functions that we need but Xcode doesn't need still get stubbed.
ENSURE_FUNCS = {
'CoreGraphics': [
'CGSSetDenyWindowServerConnections',
],
}
# this is necessary to ensure certain methods get stubbed as individual methods.
# for objc, we can stub using an invocation forwarding method. this is fine for the vast majority of cases.
# however, Xcode swizzles some methods, so they have to exist as individual methods.
ENSURE_METHODS = {
'AppKit': {
'NSColorWell': [
'-activate:',
'-deactivate',
],
'NSObject (BindingSupport)': [
'-bind:toObject:withKeyPath:options:',
'-infoForBinding:',
'-unbind:',
],
'NSObject (NSKeyValueBindingCreation)': [
'-exposedBindings',
],
},
}
# this is added to the top of the `main.m` source file for the corresponding framework.
PROLOGUES = {
'AppKit': """
#import <CoreGraphics/CGGeometry.h>
""",
'CoreGraphics': """
#include <math.h>
#ifdef __LP64__
typedef double CGFloat;
#else
typedef float CGFloat;
#endif
struct CGPoint {
CGFloat x;
CGFloat y;
};
typedef struct CGPoint CGPoint;
struct CGSize {
CGFloat width;
CGFloat height;
};
typedef struct CGSize CGSize;
struct CGVector {
CGFloat dx;
CGFloat dy;
};
typedef struct CGVector CGVector;
struct CGRect {
CGPoint origin;
CGSize size;
};
typedef struct CGRect CGRect;
""",
'QuartzCore': """
#ifdef __LP64__
typedef double CGFloat;
#else
typedef float CGFloat;
#endif
typedef struct {
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
} CATransform3D;
""",
}
for bin in IMPORTANT_BINARIES:
if not bin in VAR_OVERRIDES:
VAR_OVERRIDES[bin] = {}
if not bin in FUNC_OVERRIDES:
FUNC_OVERRIDES[bin] = []
RPATH_REGEX = r"\s*cmd LC_RPATH\n\s*cmdsize [0-9]+\n\s*path ([^(]+?)\s*\(offset"
DEPS_REGEX = r"^\s*([^(\n]+?)\s*\("
VAR_REGEX = r"^\s*(?:__DATA|__DATA_CONST)\s+[A-Za-z0-9_]+\s+0x[0-9a-fA-F]+\s+[A-Za-z_]+\s+[0-9]+\s+([A-Za-z0-9_]+)\s+((?!_OBJC_)[A-Za-z0-9_$]+)"
FUNC_REGEX = r"^\s*(?!__DATA|__DATA_CONST)[A-Za-z0-9_]+\s+[A-Za-z0-9_]+\s+0x[0-9a-fA-F]+\s+[A-Za-z_]+\s+[0-9]+\s+([A-Za-z0-9_]+)\s+((?!_OBJC_)[A-Za-z0-9_$]+)"
CLASS_REGEX = r"^\s*[A-Za-z0-9_]+\s+[A-Za-z0-9_]+\s+0x[0-9a-fA-F]+\s+[A-Za-z_]+\s+[0-9]+\s+([A-Za-z0-9_]+)\s+(_OBJC_[A-Za-z0-9_$]+)"
NM_REGEX = r"^[0-9A-Fa-f]+\s+T\s+([A-Za-z_0-9$]+)"
LAZY_REGEX = r"^\s*[A-Za-z0-9_]+\s+[A-Za-z0-9_]+\s+0x[0-9a-fA-F]+\s+0x[0-9a-fA-F]+\s+([A-Za-z0-9_]+)\s+((?!_OBJC_)[A-Za-z0-9_$]+)"
RPATHS: Dict[str, List[str]] = {}
DEPS: Dict[str, List[str]] = {}
NM_OVERRIDES: Dict[str, List[str]] = {}
class ResolutionError(Exception):
def __init__(self, message: str) -> None:
self.message = message
super().__init__(message)
class SymbolType(Enum):
VARIABLE = 1
FUNCTION = 2
OBJC_CLASS = 3
class Symbol:
def __init__(self, binary: str, name: str, type: SymbolType) -> None:
self.binary = binary
self.name = name
self.type = type
def __eq__(self, other: object) -> bool:
return isinstance(other, Symbol) and other.name == self.name and other.binary == self.binary
def __hash__(self) -> int:
return hash((self.binary, self.name))
def __str__(self) -> str:
return f'({self.binary}, {self.name}, {self.type})'
def __repr__(self) -> str:
return str(self)
def basename_no_ext(string: str) -> str:
result = string
while True:
result, ext = os.path.splitext(result)
if len(ext) == 0:
break
return result
def get_rpaths(binary: str, executable: str | None = None, extra_rpaths: List[str] | None = None, ignore_cache: bool = False) -> List[str]:
if (not ignore_cache) and (binary in RPATHS):
return RPATHS[binary]
output = subprocess.check_output([OTOOL, '-l', binary]).decode()
matches = re.finditer(RPATH_REGEX, output, re.MULTILINE)
rpaths = [match.group(1) for match in matches]
if extra_rpaths != None:
rpaths += extra_rpaths
final_rpaths: List[str] = []
for rpath in rpaths:
parts = pathlib.PurePosixPath(rpath).parts
if parts[0] == '@executable_path' or parts[0] == '@executable':
if executable == None:
raise ResolutionError('Cannot use @executable_path when no executable is specified')
else:
final_rpaths.append(os.path.normpath(os.path.join(os.path.dirname(executable), *parts[1:])))
elif parts[0] == '@loader_path':
final_rpaths.append(os.path.normpath(os.path.join(os.path.dirname(binary), *parts[1:])))
elif parts[0].startswith('@'):
raise ResolutionError('Unrecognized special path root (' + parts[0] + ') in RPATH: ' + rpath)
else:
if rpath.startswith(XCODE_PATH + '/') or rpath.startswith(SYSTEM_ROOT + '/'):
final_rpaths.append(rpath)
else:
final_rpaths.append(os.path.normpath(os.path.join(SYSTEM_ROOT, os.path.relpath(rpath, '/'))))
for template in TRY_PATH_TEMPLATES:
path = template.format(os.path.dirname(binary))
if os.path.exists(path):
final_rpaths.append(os.path.normpath(path))
final_rpaths = sorted(set(final_rpaths))
RPATHS[binary] = final_rpaths
return final_rpaths
def generate_binary_list(xcode_path: str):
# this is used to collect the root binaries for the search;
# the Xcode binary in `MacOS` covers most things, but plug-ins are dynamically loaded by
# Xcode at runtime, so we won't automatically pull them in with our search. instead, we
# scan them as roots as well.
bins: List[str] = [XCODE_MAIN_BINARY]
for plugin in os.listdir(os.path.join(xcode_path, 'Contents', 'PlugIns')):
full_path = os.path.join(xcode_path, 'Contents', 'PlugIns', plugin)
basename = os.path.splitext(plugin)[0]
fw_path = os.path.join(full_path, 'Versions', 'A', basename)
bundle_path = os.path.join(full_path, 'Contents', 'MacOS', basename)
if os.path.exists(fw_path):
bins.append(fw_path)
elif os.path.exists(bundle_path):
bins.append(bundle_path)
return bins
def get_deps(binary: str, recursive: bool = False, executable: str | None = None, processed: List[str] = []) -> List[str]:
if binary in processed:
return []
processed.append(binary)
output = subprocess.check_output([OTOOL, '-L', binary]).decode()
matches = re.finditer(DEPS_REGEX, output, re.MULTILINE)
deps = [match.group(1) for match in matches]
final_deps: List[str] = []
rpaths = get_rpaths(binary)
for dep in deps:
parts = pathlib.PurePosixPath(dep).parts
if parts[0] == '@executable_path' or parts[0] == '@executable':
if executable == None:
raise ResolutionError('Cannot use @executable_path when no executable is specified')
else:
final_deps.append(os.path.normpath(os.path.join(os.path.dirname(executable), *parts[1:])))
elif parts[0] == '@loader_path':
final_deps.append(os.path.normpath(os.path.join(os.path.dirname(binary), *parts[1:])))
elif parts[0] == '@rpath':
found = False
for rpath in rpaths:
if os.path.exists(os.path.join(rpath, *parts[1:])):
final_deps.append(os.path.normpath(os.path.join(rpath, *parts[1:])))
found = True
break
if not found:
raise ResolutionError('Could not find dependency in RPATH: ' + dep + '\nSearched: ' + str(rpaths))
elif parts[0].startswith('@'):
raise ResolutionError('Unrecognized special path root (' + parts[0] + ') in dependency: ' + dep)
else:
if dep.startswith('/Applications/Xcode.app/'):
final_deps.append(os.path.join(XCODE_PATH, *parts[3:]))
else:
final_deps.append(os.path.normpath(dep))
final_deps = sorted(set(final_deps))
# some binaries reference themselves as dependencies (for some reason). fix that.
if binary in final_deps:
final_deps.remove(binary)
# get rid of some repetitive duplicated dependencies
final_deps = [x for x in final_deps if not os.path.basename(x) in EXCLUDED_BINARIES or x.startswith(SYSTEM_ROOT) or x.startswith('/usr')]
DEPS[binary] = final_deps
if recursive:
recursive_deps = final_deps.copy()
for dep in final_deps:
if dep.startswith(XCODE_PATH + '/'):
# overwrite the RPATH cache for the dependency since we're loading it
get_rpaths(dep, executable, extra_rpaths = EXTRA_PLUGIN_RPATHS + rpaths, ignore_cache = True)
try:
recursive_deps += get_deps(dep, True, executable, processed)
except ResolutionError as e:
raise ResolutionError('Encountered error while processing dependency "' + dep + '" of "' + binary + '":\n' + e.message)
recursive_deps = sorted(set(recursive_deps))
return recursive_deps
else:
return final_deps
def get_bind_symbols(binary: str, binary_map: Dict[str, str]) -> List[Symbol]:
get_bin_name = lambda name: binary_map[name] if name in binary_map else f'@{name}@'
output = subprocess.check_output([DYLDINFO, '-bind', binary]).decode()
var_matches = re.finditer(VAR_REGEX, output, re.MULTILINE)
vars = list(set(Symbol(get_bin_name(match.group(1)), match.group(2), SymbolType.VARIABLE) for match in var_matches))
func_matches = re.finditer(FUNC_REGEX, output, re.MULTILINE)
funcs = list(set(Symbol(get_bin_name(match.group(1)), match.group(2), SymbolType.FUNCTION) for match in func_matches))
class_matches = re.finditer(CLASS_REGEX, output, re.MULTILINE)
classes = list(set(Symbol(get_bin_name(match.group(1)), match.group(2)[len('_OBJC_CLASS_$_'):], SymbolType.OBJC_CLASS) for match in class_matches if match.group(2).startswith('_OBJC_CLASS_$_')))
# most lazy symbols are functions, so let's assume that and override manually as necessary
output = subprocess.check_output([DYLDINFO, '-lazy_bind', binary]).decode()
matches = re.finditer(LAZY_REGEX, output, re.MULTILINE)
lazy = list(set(Symbol(get_bin_name(match.group(1)), match.group(2), SymbolType.FUNCTION) for match in matches))
symbols = vars + funcs + classes + lazy
for symbol in symbols:
if symbol.binary.startswith('@') and symbol.binary.endswith('@'):
continue
if not symbol.binary in NM_OVERRIDES:
if os.path.exists(SYSTEM_ROOT + symbol.binary) or os.path.exists(symbol.binary):
output = subprocess.check_output([NM, '-Ug', '-arch', 'x86_64', (SYSTEM_ROOT + symbol.binary) if os.path.exists(SYSTEM_ROOT + symbol.binary) else symbol.binary]).decode()
matches = re.finditer(NM_REGEX, output, re.MULTILINE)
NM_OVERRIDES[symbol.binary] = [x.group(1) for x in matches]
else:
NM_OVERRIDES[symbol.binary] = []
if symbol.name in NM_OVERRIDES[symbol.binary]:
symbol.type = SymbolType.FUNCTION
return symbols
def generate_method(method: str) -> str:
type = method[0]
signature = ''
split = method[1:].split(':')
if len(split) == 1:
signature = split[0]
else:
for previous, current in zip(split, split[1:]):
if len(signature) > 0:
signature += ' '
signature += f'{previous}: (id){previous}'
return '\n\n' + TEMPLATE_METHOD.format(type, signature).strip()
def main():
bins = generate_binary_list(XCODE_PATH)
try:
# cache the RPATHs for the root binaries
for bin in bins:
get_rpaths(bin, executable = XCODE_MAIN_BINARY, extra_rpaths = None if bin == XCODE_MAIN_BINARY else EXTRA_PLUGIN_RPATHS)
all_bins: List[str] = []
system_bins: List[str] = []
xc_bins: List[str] = []
# collect all the binaries that are loaded
for bin in bins:
all_bins.append(bin)
all_bins += get_deps(bin, True, XCODE_MAIN_BINARY)
all_bins = sorted(set(all_bins))
system_bins_tmp = [x for x in all_bins if not x.startswith(XCODE_PATH + '/')]
xc_bins = sorted(set(all_bins).difference(set(system_bins_tmp)))
system_bins = sorted(x[len(SYSTEM_ROOT):] if x.startswith(SYSTEM_ROOT + '/') else x for x in system_bins_tmp)
for bin in system_bins:
print(bin)
binary_map = {basename_no_ext(os.path.basename(x)): x for x in all_bins}
important_bins = [x for x in all_bins if basename_no_ext(os.path.basename(x)) in IMPORTANT_BINARIES]
# now let's get a list of all the symbols required at load time (and their associated binaries)
all_symbols: List[Symbol] = []
for bin in xc_bins:
all_symbols += get_bind_symbols(bin, binary_map)
for bin in system_bins:
short_bin_name = basename_no_ext(os.path.basename(bin))
if not short_bin_name in IMPORTANT_BINARIES:
if os.path.exists(SYSTEM_ROOT + bin):
all_symbols += get_bind_symbols(SYSTEM_ROOT + bin, binary_map)
all_symbols = list(set(all_symbols))
system_symbols = [x for x in all_symbols if x.binary in system_bins]
important_symbols = [x for x in all_symbols if x.binary in important_bins]
for symbol in important_symbols:
bin_name = basename_no_ext(os.path.basename(symbol.binary))
if symbol.name in FUNC_OVERRIDES[bin_name] or symbol.name[1:] in FUNC_OVERRIDES[bin_name]:
symbol.type = SymbolType.FUNCTION
important_symbols_separated: Dict[str, List[Symbol]] = {}
for important_bin in IMPORTANT_BINARIES:
important_symbols_separated[important_bin] = [x for x in important_symbols if basename_no_ext(os.path.basename(x.binary)) == important_bin]
for important_bin, symbols in important_symbols_separated.items():
THIS_DIR = os.path.join(OUT_DIR, important_bin)
shutil.rmtree(THIS_DIR, ignore_errors=True)
os.makedirs(THIS_DIR, exist_ok=True)
classes = sorted(x.name for x in symbols if x.type == SymbolType.OBJC_CLASS)
vars = sorted(x.name[1:] if x.name[0] == '_' else x.name for x in symbols if x.type == SymbolType.VARIABLE)
funcs = sorted(x.name[1:] if x.name[0] == '_' else x.name for x in symbols if x.type == SymbolType.FUNCTION)
os.makedirs(os.path.join(THIS_DIR, 'src'), exist_ok=True)
#os.makedirs(os.path.join(THIS_DIR, 'include'), exist_ok=True)
#os.makedirs(os.path.join(THIS_DIR, 'include', important_bin), exist_ok=True)
with open(os.path.join(THIS_DIR, 'src', 'classes.m'), 'w') as classes_source:
classes_source.write(COPYRIGHT_HEADER.format(datetime.datetime.now(datetime.timezone.utc).year).strip() + '\n\n')
classes_source.write('#import <Foundation/Foundation.h>\n\n')
for klass in classes:
classes_source.write((TEMPLATE_CATEGORY if klass.find('(') >= 0 else TEMPLATE_INTERFACE).format(klass).strip() + '\n\n' + TEMPLATE_IMPL.format(klass).strip())
if important_bin in ENSURE_METHODS and klass in ENSURE_METHODS[important_bin]:
for method in ENSURE_METHODS[important_bin][klass]:
classes_source.write(generate_method(method))
classes_source.write('\n\n@end\n\n')
if important_bin in ENSURE_METHODS:
for klass in ENSURE_METHODS[important_bin]:
if not klass in classes:
classes_source.write((TEMPLATE_CATEGORY if klass.find('(') >= 0 else TEMPLATE_INTERFACE).format(klass).strip() + f'\n\n@implementation {klass}')
for method in ENSURE_METHODS[important_bin][klass]:
classes_source.write(generate_method(method))
classes_source.write('\n\n@end\n\n')
with open(os.path.join(THIS_DIR, 'CMakeLists.txt'), 'w') as cmake_txt:
cmake_txt.write(CMAKE_TEMPLATE.format(important_bin, 'C' if important_bin == 'AppKit' else 'A').strip() + '\n')
with open(os.path.join(THIS_DIR, 'src', 'main.m'), 'w') as file:
file.write(COPYRIGHT_HEADER.format(datetime.datetime.now(datetime.timezone.utc).year).strip() + '\n\n')
file.write('#import <objc/NSObject.h>\n\n')
file.write('@interface NSString : NSObject\n@end\n\n')
if important_bin in PROLOGUES:
file.write(PROLOGUES[important_bin].strip() + '\n\n')
file.write(STUB_HEADER.strip() + '\n\n')
# most variables are just string keys, so default to that and then manually fix as necessary
#
# note that we don't really care what the value is in the string case (these are stubs, after all)
for var in vars:
if var in VAR_OVERRIDES[important_bin]:
file.write(VAR_OVERRIDES[important_bin][var] + '\n')
else:
file.write(f'NSString *const {var} = @"{var}";\n')
file.write('\n')
for func in funcs:
file.write(TEMPLATE_STUB.format(func).strip() + '\n\n')
file.write('\n')
if important_bin in ENSURE_FUNCS:
for func in ENSURE_FUNCS[important_bin]:
file.write(TEMPLATE_STUB.format(func).strip() + '\n\n')
except ResolutionError as e:
print(e.message)
exit(1)
if __name__ == '__main__':
main()