mirror of
https://github.com/darlinghq/darling.git
synced 2024-11-23 04:09:43 +00:00
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.
This commit is contained in:
parent
98c8acf3da
commit
8974de2700
702
tools/generate-xcode-stubs.py
Executable file
702
tools/generate-xcode-stubs.py
Executable file
@ -0,0 +1,702 @@
|
||||
#!/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()
|
Loading…
Reference in New Issue
Block a user