From 3597fd4b3e07d1bdaebe5d5d09bb390e592fa348 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Sun, 1 Nov 2026 03:11:18 +0800 Subject: [PATCH] [fix] include OpenSSL resource symbols in iOS archives --- .github/workflows/build-ios.yml | 9 +-- .../CrossDeskMobile.xcodeproj/project.pbxproj | 23 +++++++- apps/ios/README.md | 10 ++++ apps/ios/scripts/generate_openssl_symbols.py | 59 +++++++++++++++++++ apps/ios/scripts/verify_app_bundle.py | 15 ++++- 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 apps/ios/scripts/generate_openssl_symbols.py diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 2081698b..634acfc7 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -276,7 +276,8 @@ jobs: if [[ "${CROSSDESK_RELEASE_BUILD:-NO}" == "YES" ]]; then VERIFY_FLAGS+=(--require-release) fi - python3 apps/ios/scripts/verify_app_bundle.py "${APP_PATH}" "${VERIFY_FLAGS[@]}" + python3 apps/ios/scripts/verify_app_bundle.py "${APP_PATH}" \ + --dsym-dir "${DERIVED_DATA}/Build/Products/Release-iphoneos" "${VERIFY_FLAGS[@]}" xcrun lipo -info "${EXECUTABLE_PATH}" echo "Minimum iOS version: ${MINIMUM_OS}" @@ -293,10 +294,10 @@ jobs: cp apps/ios/licenses/OPEN_SOURCE_NOTICE.txt "${PACKAGE_DIR}/OPEN_SOURCE_NOTICE.txt" cp "${APP_PATH}/SourceMetadata.json" "${PACKAGE_DIR}/SourceMetadata.json" - DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/CrossDeskMobile.app.dSYM" - if [[ -d "${DSYM_PATH}" ]]; then + for DSYM_NAME in CrossDeskMobile.app.dSYM OpenSSL.framework.dSYM; do + DSYM_PATH="${DERIVED_DATA}/Build/Products/Release-iphoneos/${DSYM_NAME}" cp -R "${DSYM_PATH}" "${PACKAGE_DIR}/" - fi + done ditto -c -k --sequesterRsrc --keepParent "${PACKAGE_DIR}" "${PACKAGE_FILE}" echo "PACKAGE_FILE=${PACKAGE_FILE}" >> "${GITHUB_ENV}" diff --git a/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj b/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj index 8a5e0fbe..18f1057f 100644 --- a/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj +++ b/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj @@ -222,6 +222,7 @@ 300000000000000000000001 /* Frameworks */, 600000000000000000000002 /* Resources */, 300000000000000000000002 /* Embed Frameworks */, + 700000000000000000000002 /* Generate OpenSSL symbols */, ); buildRules = ( ); @@ -318,6 +319,24 @@ shellPath = /bin/zsh; shellScript = "set -e\npython3 \"${PROJECT_DIR}/scripts/prepare_source_metadata.py\" --output \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SourceMetadata.json\"\n\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\npython3 \"${PROJECT_DIR}/scripts/verify_native_build.py\" --inputs \"${PROJECT_DIR}/Vendor/${PLATFORM_NAME}/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --xmake-version \"${PROJECT_DIR}/Vendor/${PLATFORM_NAME}/${CONFIGURATION}/xmake-version.txt\"\npython3 \"${PROJECT_DIR}/scripts/package_openssl_ios.py\" --inputs \"${PROJECT_DIR}/Vendor/${PLATFORM_NAME}/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --output \"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\" --platform \"${PLATFORM_NAME}\"\n"; }; + 700000000000000000000002 /* Generate OpenSSL symbols */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(TARGET_BUILD_DIR)/$(FRAMEWORKS_FOLDER_PATH)/OpenSSL.framework/OpenSSL", + "$(PROJECT_DIR)/scripts/generate_openssl_symbols.py", + ); + name = "Generate OpenSSL symbols"; + outputPaths = ( + "$(DWARF_DSYM_FOLDER_PATH)/OpenSSL.framework.dSYM/Contents/Resources/DWARF/OpenSSL", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/zsh; + shellScript = "set -e\nif [[ \"${DEBUG_INFORMATION_FORMAT}\" == \"dwarf-with-dsym\" ]]; then\n python3 -B \"${PROJECT_DIR}/scripts/generate_openssl_symbols.py\" --binary \"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework/OpenSSL\" --output \"${DWARF_DSYM_FOLDER_PATH}/OpenSSL.framework.dSYM\"\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -496,7 +515,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -548,7 +567,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; ENABLE_BITCODE = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; FRAMEWORK_SEARCH_PATHS = ( diff --git a/apps/ios/README.md b/apps/ios/README.md index 18f9c60a..e09e4bb9 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -586,6 +586,16 @@ Xcode removes the static archive from the embedded framework (it may insert a small resource-framework stub) and preserves `PrivacyInfo.xcprivacy`, source metadata and license texts. The application does not load a dynamic OpenSSL copy. +For builds using `DWARF with dSYM File`, the **Generate OpenSSL symbols** phase +runs after embedding. It verifies that Xcode's placeholder has no executable +code, generates `OpenSSL.framework.dSYM` from that exact binary, and checks its +UUIDs. This companion describes the empty resource stub, not the statically +linked OpenSSL implementation. Xcode includes it alongside the app's dSYM in +the archive so Organizer can upload symbols for every embedded Mach-O binary. +`verify_app_bundle.py` checks both UUID matches when given an archived app; +for an ordinary Release build, pass `--dsym-dir /path/to/Release-iphoneos`. +The CI ZIP includes both dSYM bundles. + The iOS-specific OpenSSL recipe disables POSIX metadata calls, automatic `openssl.cnf` loading, and the unused file-URI store provider. It selects the upstream `getrandom` entropy backend, which uses Darwin's `getentropy` on the diff --git a/apps/ios/scripts/generate_openssl_symbols.py b/apps/ios/scripts/generate_openssl_symbols.py new file mode 100644 index 00000000..13d643d3 --- /dev/null +++ b/apps/ios/scripts/generate_openssl_symbols.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +"""Generate matching symbols for Xcode's embedded, codeless OpenSSL framework.""" + +import argparse +from pathlib import Path +import re +import subprocess +import sys + + +def uuids(path): + output = subprocess.check_output(["xcrun", "dwarfdump", "--uuid", str(path)], text=True) + result = dict((arch, uuid.upper()) for uuid, arch in + re.findall(r"UUID: ([0-9A-Fa-f-]{36}) \(([^)]+)\)", output)) + if not result: + raise ValueError(f"No Mach-O UUIDs found: {path}") + return result + + +def verify_symbols(binary, dsym): + dwarf = dsym / "Contents/Resources/DWARF" / binary.name + if not dwarf.is_file(): + raise ValueError(f"Missing dSYM DWARF file: {dwarf}") + if uuids(binary) != uuids(dwarf): + raise ValueError(f"dSYM UUIDs do not match {binary}: {dwarf}") + + +def generate(binary, output): + # This is only for Xcode's resource placeholder. Real OpenSSL code remains + # statically linked into the app and must not receive an empty dSYM here. + commands = subprocess.check_output(["xcrun", "otool", "-l", str(binary)], text=True) + text_sizes = re.findall( + r"sectname __text\s+segname __TEXT\s+addr \S+\s+size (0x[0-9a-fA-F]+)", commands) + if not text_sizes or any(int(size, 16) for size in text_sizes): + raise ValueError(f"Expected a codeless OpenSSL resource stub: {binary}") + output.parent.mkdir(parents=True, exist_ok=True) + # No function-level debug info exists in this empty stub. dsymutil still + # emits a valid UUID-matched DWARF companion; quiet suppresses that expected + # warning only after verifying that there is no executable code to lose. + subprocess.run(["xcrun", "dsymutil", "--quiet", str(binary), "-o", str(output)], check=True) + verify_symbols(binary, output) + subprocess.run(["xcrun", "dwarfdump", "--verify", str(output)], check=True) + print(f"Generated UUID-matched OpenSSL resource symbols: {output}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--binary", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + generate(args.binary, args.output) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, subprocess.CalledProcessError) as error: + print(f"OpenSSL symbols: {error}", file=sys.stderr) + sys.exit(1) diff --git a/apps/ios/scripts/verify_app_bundle.py b/apps/ios/scripts/verify_app_bundle.py index c6fa9cb3..657578d3 100644 --- a/apps/ios/scripts/verify_app_bundle.py +++ b/apps/ios/scripts/verify_app_bundle.py @@ -10,8 +10,10 @@ import re import subprocess import sys +from generate_openssl_symbols import verify_symbols -def verify(app, require_release=False): + +def verify(app, require_release=False, dsym_dir=None): info = plistlib.loads((app / "Info.plist").read_bytes()) binary = app / info["CFBundleExecutable"] subprocess.run(["xcrun", "lipo", str(binary), "-verify_arch", "arm64"], check=True) @@ -63,6 +65,14 @@ def verify(app, require_release=False): sdk_symbols = subprocess.check_output(["xcrun", "nm", "-gU", str(embedded_binary)], text=True) if re.search(r"\b_(?:SSL_new|OPENSSL_init_crypto)\b", sdk_symbols): raise ValueError("Unexpected dynamic OpenSSL implementation in resource framework") + # Archive validation includes the exact files that Organizer uploads. + if dsym_dir is None and app.parents[2].suffix == ".xcarchive": + dsym_dir = app.parents[2] / "dSYMs" + if dsym_dir is not None: + verify_symbols(binary, dsym_dir / f"{app.name}.dSYM") + if embedded_binary.exists(): + verify_symbols(embedded_binary, dsym_dir / "OpenSSL.framework.dSYM") + print("Verified app and embedded OpenSSL dSYM UUIDs") linked = subprocess.check_output(["xcrun", "otool", "-L", str(binary)], text=True) if "OpenSSL.framework" in linked: raise ValueError("OpenSSL must be statically linked into the application") @@ -79,8 +89,9 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("app", type=Path) parser.add_argument("--require-release", action="store_true") + parser.add_argument("--dsym-dir", type=Path) args = parser.parse_args() - verify(args.app, args.require_release) + verify(args.app.resolve(), args.require_release, args.dsym_dir) if __name__ == "__main__":