"""Generate a dependency-free Xcode project. Run with Python 3 from any directory.""" import hashlib import json import pathlib import plistlib ROOT = pathlib.Path(__file__).resolve().parents[1] objects = {} def uid(name): return hashlib.sha256(name.encode()).hexdigest()[:24].upper() def add(key_name, isa, **fields): key = uid(key_name) objects[key] = dict(isa=isa, **fields) return key def configurations(name, settings): configs = [] for mode in ("Debug", "Release"): values = dict(settings) values.update(SWIFT_OPTIMIZATION_LEVEL="-Onone" if mode == "Debug" else "-O") if mode == "Debug": values.update(ENABLE_TESTABILITY="YES", SWIFT_ACTIVE_COMPILATION_CONDITIONS="DEBUG") if name == "MusicBridgeWidgets": values["SWIFT_ACTIVE_COMPILATION_CONDITIONS"] = values.get("SWIFT_ACTIVE_COMPILATION_CONDITIONS", "") + " MUSICBRIDGE_WIDGET" configs.append(add(name + mode, "XCBuildConfiguration", name=mode, buildSettings=values)) return add(name + "configs", "XCConfigurationList", buildConfigurations=configs, defaultConfigurationIsVisible=0, defaultConfigurationName="Release") def source_group(folder): refs, builds = [], [] for path in sorted((ROOT / folder).glob("*.swift")): ref = add(path.relative_to(ROOT).as_posix(), "PBXFileReference", lastKnownFileType="sourcecode.swift", path=path.name, sourceTree="") refs.append(ref) builds.append(add("build/" + path.relative_to(ROOT).as_posix(), "PBXBuildFile", fileRef=ref)) return add(folder, "PBXGroup", path=folder, sourceTree="", children=refs), builds app_group, app_sources = source_group("MusicBridge") remote_shared_group, remote_shared_sources = source_group("RemoteShared") remote_group, remote_sources = source_group("MusicBridgeRemote") remote_sources += [add("remote-shared/" + str(index), "PBXBuildFile", fileRef=objects[build]["fileRef"]) for index, build in enumerate(remote_shared_sources)] for filename in ("Models.swift", "BridgeClient.swift"): remote_sources.append(add("remote-core/" + filename, "PBXBuildFile", fileRef=uid("MusicBridge/" + filename))) app_sources += remote_shared_sources shared_group, shared_sources = source_group("Shared") widget_group, widget_sources = source_group("MusicBridgeWidgets") app_sources += shared_sources widget_sources += [add("widget-shared/" + str(index), "PBXBuildFile", fileRef=objects[build]["fileRef"]) for index, build in enumerate(shared_sources)] assets = add("assets", "PBXFileReference", lastKnownFileType="folder.assetcatalog", path="Assets.xcassets", sourceTree="") objects[app_group]["children"].append(assets) assets_build = add("assets-build", "PBXBuildFile", fileRef=assets) test_group, test_sources = source_group("MusicBridgeTests") fixture = add("fixture", "PBXFileReference", lastKnownFileType="text.json", path="state-v1.json", sourceTree="") objects[test_group]["children"].append(fixture) fixture_build = add("fixture-build", "PBXBuildFile", fileRef=fixture) tls_fixture = add("tls-fixture", "PBXFileReference", lastKnownFileType="text.json", path="tls-fixture.json", sourceTree="") objects[test_group]["children"].append(tls_fixture) tls_fixture_build = add("tls-fixture-build", "PBXBuildFile", fileRef=tls_fixture) products = [] targets = [] for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True), ("MusicBridgeWidgets", widget_sources, False), ("MusicBridgeRemote", remote_sources, False)]: is_widget = name == "MusicBridgeWidgets" is_remote = name == "MusicBridgeRemote" is_extension = is_widget or is_remote product = add(name + "product", "PBXFileReference", explicitFileType="wrapper.extensionkit-extension" if is_remote else "wrapper.app-extension" if is_extension else "wrapper.cfbundle" if is_test else "wrapper.application", includeInIndex=0, path=name + (".appex" if is_extension else ".xctest" if is_test else ".app"), sourceTree="BUILT_PRODUCTS_DIR") products.append(product) phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0), add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0), add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[] if is_extension else [fixture_build, tls_fixture_build] if is_test else [assets_build], runOnlyForDeploymentPostprocessing=0)] settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".remote" if is_remote else ".widgets" if is_widget else ".tests" if is_test else ""), SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="27.0" if is_remote else "17.0", SDKROOT="iphoneos", SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic", GENERATE_INFOPLIST_FILE="YES" if is_test else "NO") deps = [] if is_test: settings.update(TEST_HOST="$(BUILT_PRODUCTS_DIR)/MusicBridge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MusicBridge", BUNDLE_LOADER="$(TEST_HOST)") proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge") deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)] elif is_extension: settings.update(INFOPLIST_FILE=name + "/Info.plist", CURRENT_PROJECT_VERSION="8", MARKETING_VERSION="0.7.0", APPLICATION_EXTENSION_API_ONLY="YES", SKIP_INSTALL="YES", LD_RUNPATH_SEARCH_PATHS="$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks") if is_remote: settings.update(CODE_SIGN_ENTITLEMENTS="MusicBridgeRemote/Remote.entitlements") else: settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="8", MARKETING_VERSION="0.7.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon", CODE_SIGN_ENTITLEMENTS="MusicBridge/App.entitlements") embed = add("embed-widget", "PBXBuildFile", fileRef=uid("MusicBridgeWidgetsproduct"), settings=dict(ATTRIBUTES=["RemoveHeadersOnCopy"])) embed_remote = add("embed-remote", "PBXBuildFile", fileRef=uid("MusicBridgeRemoteproduct"), settings=dict(ATTRIBUTES=["RemoveHeadersOnCopy"])) phases.append(add("embed-extensions", "PBXCopyFilesBuildPhase", buildActionMask=2147483647, dstPath="", dstSubfolderSpec=13, files=[embed], name="Embed App Extensions", runOnlyForDeploymentPostprocessing=0)) phases.append(add("embed-remote-extensions", "PBXCopyFilesBuildPhase", buildActionMask=2147483647, dstPath="$(EXTENSIONS_FOLDER_PATH)", dstSubfolderSpec=16, files=[embed_remote], name="Embed ExtensionKit Extensions", runOnlyForDeploymentPostprocessing=0)) proxy = add("widget-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgeWidgetstarget"), remoteInfo="MusicBridgeWidgets") deps = [add("widget-dependency", "PBXTargetDependency", target=uid("MusicBridgeWidgetstarget"), targetProxy=proxy)] remote_proxy = add("remote-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgeRemotetarget"), remoteInfo="MusicBridgeRemote") deps.append(add("remote-dependency", "PBXTargetDependency", target=uid("MusicBridgeRemotetarget"), targetProxy=remote_proxy)) targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product, productType="com.apple.product-type.extensionkit-extension" if is_remote else "com.apple.product-type.app-extension" if is_widget else "com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application", buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps)) product_group = add("products", "PBXGroup", name="Products", sourceTree="", children=products) main_group = add("main", "PBXGroup", sourceTree="", children=[app_group, shared_group, widget_group, remote_shared_group, remote_group, test_group, product_group]) project = add("project", "PBXProject", attributes=dict(LastUpgradeCheck="1600", BuildIndependentTargetsInParallel="YES"), buildConfigurationList=configurations("project", dict(CLANG_ENABLE_MODULES="YES", CLANG_ENABLE_OBJC_ARC="YES")), compatibilityVersion="Xcode 14.0", developmentRegion="ru", knownRegions=["ru", "en", "Base"], mainGroup=main_group, productRefGroup=product_group, projectDirPath="", projectRoot="", targets=targets) def encode(value, indent=0): if isinstance(value, dict): return "{\n" + "".join("\t" * (indent + 1) + json.dumps(k) + " = " + encode(v, indent + 1) + ";\n" for k, v in value.items()) + "\t" * indent + "}" if isinstance(value, list): return "(" + ", ".join(encode(v, indent) for v in value) + ("," if value else "") + ")" return json.dumps(value, ensure_ascii=True) project_dir = ROOT / "MusicBridge.xcodeproj" project_dir.mkdir(exist_ok=True) (project_dir / "project.pbxproj").write_bytes(("// !$*UTF8*$!\n" + encode(dict(archiveVersion=1, classes={}, objectVersion=56, objects=objects, rootObject=project)) + "\n").encode("utf-8")) scheme_dir = project_dir / "xcshareddata" / "xcschemes" scheme_dir.mkdir(parents=True, exist_ok=True) def reference(target, product): return f'' app_ref = reference("MusicBridge", "MusicBridge.app") test_ref = reference("MusicBridgeTests", "MusicBridgeTests.xctest") (scheme_dir / "MusicBridge.xcscheme").write_bytes(f''' {app_ref} {test_ref} {app_ref} {app_ref} '''.encode("utf-8")) info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", CFBundleExecutable="$(EXECUTABLE_NAME)", CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)", CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)", CFBundlePackageType="APPL", CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)", LSRequiresIPhoneOS=True, UILaunchScreen={}, NSSupportsLiveActivities=True, UISupportedInterfaceOrientations=["UIInterfaceOrientationPortrait"], NSLocalNetworkUsageDescription="MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.", NSCameraUsageDescription="Камера сканирует QR в MusicBridge на ПК для безопасного сопряжения.", NSBonjourServices=["_musicbridge._tcp"], CFBundleURLTypes=[dict(CFBundleURLName="ru.yukinoki.musicbridge.pair", CFBundleURLSchemes=["musicbridge"])], # iOS 17+ requires explicit IP exceptions for custom self-signed trust. # BridgeClient still requires HTTPS, an exact SHA-256 pin and valid dates. # Never disable ATS globally or include public IP ranges. NSAppTransportSecurity=dict(NSAllowsLocalNetworking=True, NSExceptionDomains={ network: dict(NSExceptionAllowsInsecureHTTPLoads=True) for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16") })) (ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False)) widget_info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", CFBundleExecutable="$(EXECUTABLE_NAME)", CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)", CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)", CFBundlePackageType="XPC!", CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)", NSExtension=dict(NSExtensionPointIdentifier="com.apple.widgetkit-extension")) (ROOT / "MusicBridgeWidgets" / "Info.plist").write_bytes(plistlib.dumps(widget_info, sort_keys=False)) shared_group_name = "$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control" info["MusicBridgeSharedKeychainGroup"] = "ru.yukinoki.musicbridge.remote-control" (ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False)) remote_info = {k: v for k, v in widget_info.items() if k != "NSExtension"} remote_info.update(EXAppExtensionAttributes=dict(EXExtensionPointIdentifier="com.apple.nowplaying.remote-media"), MusicBridgeSharedKeychainGroup=info["MusicBridgeSharedKeychainGroup"], NSLocalNetworkUsageDescription=info["NSLocalNetworkUsageDescription"], NSAppTransportSecurity=info["NSAppTransportSecurity"]) (ROOT / "MusicBridgeRemote" / "Info.plist").write_bytes(plistlib.dumps(remote_info, sort_keys=False)) for folder, filename in (("MusicBridge", "App.entitlements"), ("MusicBridgeRemote", "Remote.entitlements")): groups = ["$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)", shared_group_name] (ROOT / folder / filename).write_bytes(plistlib.dumps({"keychain-access-groups": groups})) print("Generated Xcode project, shared scheme and Info.plist.")