From 6b2c6c467ddb25d53aed4fde7187ae2a834bd974 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Mon, 21 Sep 2026 15:04:17 -0700 Subject: [PATCH 1/3] Add merge symbol logic for MachO symbols --- scripts/merge_libraries.py | 49 ++++++++-- scripts/merge_libraries_test.py | 161 ++++++++++++++++++++++++++++++++ scripts/merge_libraries_test.sh | 6 +- 3 files changed, 207 insertions(+), 9 deletions(-) diff --git a/scripts/merge_libraries.py b/scripts/merge_libraries.py index efa335e69f..a68495e522 100644 --- a/scripts/merge_libraries.py +++ b/scripts/merge_libraries.py @@ -366,10 +366,14 @@ def demangle_symbol(symbol): _cache["demangle"][FLAGS.platform][symbol] = demangled_symbol break if FLAGS.platform == "darwin" or FLAGS.platform == "ios": - # On Darwin, try demangling again without the leading _. - demangled_symbol = demangler.demangle(symbol[1:]) - if demangled_symbol and (symbol[1:] != demangled_symbol): - _cache["demangle"][FLAGS.platform][symbol] = demangled_symbol + # On Darwin, try demangling again without the leading _ or l_. + for stripped in ([symbol[1:], symbol[2:]] + if symbol.startswith("l__Z") else [symbol[1:]]): + demangled_symbol = demangler.demangle(stripped) + if demangled_symbol and (stripped != demangled_symbol): + _cache["demangle"][FLAGS.platform][symbol] = demangled_symbol + break + if _cache["demangle"][FLAGS.platform][symbol] != symbol: break return _cache["demangle"][FLAGS.platform][symbol] @@ -398,6 +402,37 @@ def is_cpp_symbol(symbol): return "@" in symbol or "?" in symbol +def is_ignored_symbol(symbol): + """Returns True if the given symbol should be ignored and never renamed. + + Args: + symbol: Symbol to check. + Returns: + True if the symbol should be ignored, False otherwise. + """ + if FLAGS.platform in ("darwin", "ios"): + # Ignore any Objective-C or Objective-C++ methods and runtime metadata. + if ('[' in symbol or ']' in symbol or + symbol.startswith(("_OBJC_", "_objc_"))): + return True + # Ignore Mach-O assembler temporary labels and compiler metadata labels. + # Every real C or C++ symbol on Mach-O is emitted with a leading underscore + # (or "l__Z" for linker-private C++ symbols), so any non-C++ symbol lacking + # a leading underscore (ltmp*, lCPI*, lJTI*, l_.str, GCC_except_table*, etc.) + # is a compiler- or assembler-generated label that the linker discards or + # handles specially. Renaming an 'l'/'L' label turns it into a real symbol + # ("f_b_l...") at the address of the atom it labelled; when that atom is an + # Objective-C class reference, ld aborts in changeClassRefUseToGotUse with + # "unsupported reference (alias of) to class-ref". + if not is_cpp_symbol(symbol) and not symbol.startswith("_"): + return True + elif FLAGS.platform == "windows": + # Don't rename $LN*, those are local symbols. + if symbol.startswith("$LN"): + return True + return False + + def get_top_level_namespaces(demangled_symbols): """Gets all top-level C++ namespaces from a set of demangled symbols. @@ -525,8 +560,7 @@ def read_symbols_from_archive(archive_file): m = RE_NM_SYMBOLS_PLATFORM[FLAGS.platform].match(line) if m: symbol = m.group("symbol") - # Ignore any Objective-C or Objective-C++ methods. - if FLAGS.platform == "darwin" and ('[' in symbol or ']' in symbol): + if is_ignored_symbol(symbol): continue all_symbols.add(symbol) @@ -654,8 +688,7 @@ def rename_symbol(symbol): new_symbol = re.sub(r"(?<=[^a-z_])%s@@" % ns, r"%s@@" % new_ns, new_symbol) new_renames[symbol] = new_symbol else: - if FLAGS.platform == "windows" and symbol.startswith("$LN"): - # Don't rename $LN*, those are local symbols. + if is_ignored_symbol(symbol): return new_renames # C symbol. Just split, rename, and re-join. (prefix, remainder) = split_symbol(symbol) diff --git a/scripts/merge_libraries_test.py b/scripts/merge_libraries_test.py index ad44ff500f..7a7d92509c 100644 --- a/scripts/merge_libraries_test.py +++ b/scripts/merge_libraries_test.py @@ -276,6 +276,167 @@ def test_renaming_cpp_symbols(self): if os.path.exists(tempdir): shutil.rmtree(tempdir) + def test_macho_ignored_symbols(self): + """Verify Mach-O temporary labels and Objective-C metadata are ignored.""" + prev_platform = merge_libraries.FLAGS.platform + prev_namespaces = merge_libraries.FLAGS.hide_cpp_namespaces + prev_rename = merge_libraries.FLAGS.rename_string + try: + for platform in ("darwin", "ios"): + merge_libraries.FLAGS.platform = platform + merge_libraries.FLAGS.hide_cpp_namespaces = ["test_namespace"] + merge_libraries.FLAGS.rename_string = "f_b_" + merge_libraries.init_cache() + merge_libraries.init_demanglers() + + ignored_symbols = [ + # Assembler temporary labels + "ltmp0", + "ltmp1", + "lCPI0_0", + "lJTI0_0", + "l_.str", + "l_.str.1", + "l___PRETTY_FUNCTION__.foo", + "l__unnamed_cfstring_", + "L_OBJC_METH_VAR_NAME_0", + # Compiler metadata/exception labels without leading underscore + "GCC_except_table0", + "GCC_except_table14", + "EH_Frame1", + "func.eh", + # Objective-C methods and runtime metadata + "-[FIRApp configure]", + "+[FIRApp configure]", + "_OBJC_CLASSLIST_REFERENCES_$_", + "_OBJC_CLASSLIST_REFERENCES_$_.1", + "_OBJC_CLASS_$_NSError", + "_OBJC_METACLASS_$_FIRApp", + "_OBJC_IVAR_$_SomeClass._ivar", + "_OBJC_SELECTOR_REFERENCES_", + "_objc_msgSend$currentLocale", + ] + for sym in ignored_symbols: + self.assertTrue( + merge_libraries.is_ignored_symbol(sym), + f"Expected {sym} to be ignored on {platform}") + self.assertEqual( + merge_libraries.rename_symbol(sym), {}, + f"Expected {sym} not to be renamed on {platform}") + + # Real C symbols should not be ignored and should be renamed. + self.assertFalse(merge_libraries.is_ignored_symbol("_global_c_symbol")) + self.assertEqual( + merge_libraries.rename_symbol("_global_c_symbol"), + {"_global_c_symbol": "_f_b_global_c_symbol"}) + + # Linker-private C++ symbols (l__Z*) should be recognized as C++ symbols, + # not ignored, and renamed while preserving the leading 'l__Z' prefix. + private_cpp_sym = "l__ZN14test_namespace9TestClass10TestMethodEv" + self.assertTrue(merge_libraries.is_cpp_symbol(private_cpp_sym)) + self.assertFalse(merge_libraries.is_ignored_symbol(private_cpp_sym)) + self.assertEqual( + merge_libraries.rename_symbol(private_cpp_sym), + { + private_cpp_sym: + "l__ZN18f_b_test_namespace9TestClass10TestMethodEv" + }) + + merge_libraries.shutdown_demanglers() + merge_libraries.shutdown_cache() + finally: + merge_libraries.FLAGS.platform = prev_platform + merge_libraries.FLAGS.hide_cpp_namespaces = prev_namespaces + merge_libraries.FLAGS.rename_string = prev_rename + + def test_macho_objc_classref_linking(self): + """Verify Objective-C++ object files filter temporary/ObjC symbols cleanly.""" + if merge_libraries.FLAGS.platform != "darwin": + return + + tempdir = tempfile.mkdtemp() + cwd = os.getcwd() + os.chdir(tempdir) + prev_rename = merge_libraries.FLAGS.rename_string + try: + merge_libraries.FLAGS.rename_string = "f_b_" + objc_src = os.path.join(tempdir, "test_objc.mm") + objc_obj = os.path.join(tempdir, "test_objc.o") + objc_archive = os.path.join(tempdir, "libtest_objc.a") + main_src = os.path.join(tempdir, "main.cc") + exe_out = os.path.join(tempdir, "test_exe") + + with open(objc_src, "w") as f: + f.write(""" +#import +extern "C" void test_objc_c_func() { + NSLocale* loc = [NSLocale currentLocale]; + NSError* err = [NSError errorWithDomain:[loc localeIdentifier] code:1 userInfo:nil]; + (void)err; +} +""") + with open(main_src, "w") as f: + f.write(""" +extern "C" void f_b_test_objc_c_func(); +int main() { + f_b_test_objc_c_func(); + return 0; +} +""") + + subprocess.run(["clang++", "-c", objc_src, "-o", objc_obj], check=True) + subprocess.run( + [merge_libraries.FLAGS.binutils_ar_cmd, "rcs", objc_archive, objc_obj], + check=True) + + (defined_symbols, all_symbols) = merge_libraries.read_symbols_from_archive( + objc_archive) + + # Verify no ltmp*, l_.str*, or _OBJC_* symbols were read. + for sym in all_symbols: + self.assertFalse( + sym.startswith(("ltmp", "l_.str", "lCPI", "lJTI", "GCC_except_table")), + f"Unexpected compiler/assembler temporary symbol: {sym}") + self.assertFalse( + sym.startswith(("_OBJC_", "_objc_")), + f"Unexpected Objective-C runtime symbol: {sym}") + + rename_symbols = {} + for sym in defined_symbols: + if not merge_libraries.is_cpp_symbol(sym): + rename_symbols.update(merge_libraries.rename_symbol(sym)) + + self.assertEqual(rename_symbols, + {"_test_objc_c_func": "_f_b_test_objc_c_func"}) + + redefinition_file = merge_libraries.create_symbol_redefinition_file( + rename_symbols) + self.assertIsNotNone(redefinition_file) + + renamed_obj = os.path.join(tempdir, "test_objc_renamed.o") + merge_libraries.move_object_file(objc_obj, renamed_obj, + redefinition_file.name) + + after_defined = merge_libraries.read_symbols_from_archive(renamed_obj)[0] + self.assertIn("_f_b_test_objc_c_func", after_defined) + self.assertNotIn("_test_objc_c_func", after_defined) + + # When using llvm-objcopy (which preserves arm64 Mach-O compact unwind + # sections), also verify end-to-end linking with Apple's linker. + if "llvm-objcopy" in merge_libraries.FLAGS.binutils_objcopy_cmd: + subprocess.run( + [ + "clang++", main_src, renamed_obj, "-framework", "Foundation", + "-o", exe_out + ], + check=True) + self.assertTrue(os.path.isfile(exe_out)) + finally: + merge_libraries.FLAGS.rename_string = prev_rename + os.chdir(cwd) + if os.path.exists(tempdir): + shutil.rmtree(tempdir) + if __name__ == "__main__": absltest.main() diff --git a/scripts/merge_libraries_test.sh b/scripts/merge_libraries_test.sh index fedb10bf67..e75c1306e8 100755 --- a/scripts/merge_libraries_test.sh +++ b/scripts/merge_libraries_test.sh @@ -43,7 +43,11 @@ while getopts "t:P:Lh" opt; do esac done -readonly demangle_cmds=${tools_path}/c++filt,${tools_path}/demumble +if [[ -x ${tools_path}/demumble ]]; then + readonly demangle_cmds=${tools_path}/c++filt,${tools_path}/demumble +else + readonly demangle_cmds=${tools_path}/c++filt +fi if [[ ${use_llvm_binutils} -eq 1 ]]; then readonly binutils_objcopy=${tools_path}/llvm-objcopy readonly binutils_nm=${tools_path}/llvm-nm From 62fe90fc48646349aa9f03b7d79523bf37352ea8 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Mon, 5 Oct 2026 15:15:28 -0700 Subject: [PATCH 2/3] Update readme.md --- release_build_files/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/release_build_files/readme.md b/release_build_files/readme.md index 5c606747a5..1e59b8e3c8 100644 --- a/release_build_files/readme.md +++ b/release_build_files/readme.md @@ -616,6 +616,7 @@ code. ### Upcoming - Changes - General (Android): **Breaking Change** Updated minSdkVersion to 24. + - General (MacOS Desktop): Fix for linker issue on XCode 27. (#1920) - Auth: **Breaking Change:** Fixed spelling in `AuthError` enum values (`kAuthErrorWebStorateUnsupported` -> `kAuthErrorWebStorageUnsupported` and `kAuthErrorFederatedProviderAreadyInUse` -> `kAuthErrorFederatedProviderAlreadyInUse`). ### 13.13.0 From fff7a2111ff9c1ea79acfee1f10cadde126360a8 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Tue, 6 Oct 2026 13:29:39 -0700 Subject: [PATCH 3/3] Update merge_libraries.py --- scripts/merge_libraries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/merge_libraries.py b/scripts/merge_libraries.py index a68495e522..c7f3e1c11b 100644 --- a/scripts/merge_libraries.py +++ b/scripts/merge_libraries.py @@ -424,7 +424,7 @@ def is_ignored_symbol(symbol): # ("f_b_l...") at the address of the atom it labelled; when that atom is an # Objective-C class reference, ld aborts in changeClassRefUseToGotUse with # "unsupported reference (alias of) to class-ref". - if not is_cpp_symbol(symbol) and not symbol.startswith("_"): + if not symbol.startswith("_") and not is_cpp_symbol(symbol): return True elif FLAGS.platform == "windows": # Don't rename $LN*, those are local symbols.