#!/usr/bin/env ruby # frozen_string_literal: true # # Inject the Broadcast Upload Extension (ReplayKit screen sharing) into the Capacitor-generated Xcode project. # Mirrors add-share-extension.rb, with the extra step that this extension LINKS the LiveKit Swift package # (LKSampleHandler lives in the LiveKit product), so it adds a package product dependency to the new target. # # WHAT IT WIRES: # * a new app-extension target "BroadcastExtension" (bundle id .broadcast) whose source is our # SampleHandler.swift (subclass of LiveKit's LKSampleHandler) + Info.plist + entitlements, copied from # mobile/ios-broadcast/ # * the App Group entitlement (group.com.bizgaze.connect) on the extension (LiveKit's IPC socket lives there) # * a Swift Package product dependency on LiveKit (github.com/livekit/client-sdk-swift 2.15.3) so the # extension can subclass LKSampleHandler # * the extension embedded into the app ("Embed App Extensions") + set as a build dependency # # Idempotent: if the target already exists it is removed and rebuilt. require 'xcodeproj' require 'fileutils' ROOT = File.expand_path('..', __dir__) PROJECT = File.join(ROOT, 'ios', 'App', 'App.xcodeproj') SRC_DIR = File.join(ROOT, 'ios-broadcast') APP_DIR = File.join(ROOT, 'ios', 'App') EXT_NAME = 'BroadcastExtension' EXT_DIR = File.join(APP_DIR, EXT_NAME) APP_TARGET = 'App' APP_BUNDLE = ENV.fetch('BUNDLE_ID', 'com.bizgaze.connect') EXT_BUNDLE = "#{APP_BUNDLE}.broadcast" APP_GROUP = 'group.com.bizgaze.connect' LK_URL = 'https://github.com/livekit/client-sdk-swift.git' LK_VERSION = '2.15.3' abort "xcodeproj not found at #{PROJECT}" unless File.directory?(PROJECT) project = Xcodeproj::Project.open(PROJECT) app = project.targets.find { |t| t.name == APP_TARGET } abort "App target not found" unless app # ── Clean any previous injection so this is idempotent ────────────────────────────────────────────── project.targets.select { |t| t.name == EXT_NAME }.each do |t| t.build_configuration_list&.build_configurations&.each { |c| c.remove_from_project } t.remove_from_project end if (grp = project.main_group.children.find { |g| g.respond_to?(:display_name) && g.display_name == EXT_NAME }) grp.remove_from_project end # ── Copy our sources into the app project so paths inside the .xcodeproj are stable ────────────────── FileUtils.mkdir_p(EXT_DIR) %w[SampleHandler.swift Info.plist BroadcastExtension.entitlements].each do |f| FileUtils.cp(File.join(SRC_DIR, f), File.join(EXT_DIR, f)) end # ── Create the extension target ────────────────────────────────────────────────────────────────────── deployment = app.deployment_target || project.build_configurations.first&.build_settings&.[]('IPHONEOS_DEPLOYMENT_TARGET') || '15.0' ext = project.new_target(:app_extension, EXT_NAME, :ios, deployment, project.products_group, :swift) group = project.main_group.new_group(EXT_NAME, EXT_NAME.to_s) swift_ref = group.new_reference(File.join(EXT_DIR, 'SampleHandler.swift')) ext.add_file_references([swift_ref]) ext.build_configurations.each do |cfg| s = cfg.build_settings s['PRODUCT_BUNDLE_IDENTIFIER'] = EXT_BUNDLE s['PRODUCT_NAME'] = '$(TARGET_NAME)' s['INFOPLIST_FILE'] = "#{EXT_NAME}/Info.plist" s['CODE_SIGN_ENTITLEMENTS'] = "#{EXT_NAME}/BroadcastExtension.entitlements" s['IPHONEOS_DEPLOYMENT_TARGET'] = deployment s['SWIFT_VERSION'] = '5.0' s['TARGETED_DEVICE_FAMILY'] = '1,2' s['GENERATE_INFOPLIST_FILE'] = 'NO' s['SKIP_INSTALL'] = 'YES' s['CODE_SIGN_STYLE'] = 'Manual' s['MARKETING_VERSION'] = '1.0' s['CURRENT_PROJECT_VERSION'] = ENV.fetch('BUILD_NUMBER', '1') s['LD_RUNPATH_SEARCH_PATHS'] = '$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks' end # ── Link LiveKit (Swift Package product) so the extension can subclass LKSampleHandler ──────────────── # The app already resolves client-sdk-swift 2.15.3 (via the native-call plugin's Package.swift). Add a # project-level remote package reference to the SAME repo+version (SPM dedupes it) and attach the "LiveKit" # product to this extension target. root = project.root_object pkg = root.package_references.find { |r| r.respond_to?(:repositoryURL) && r.repositoryURL == LK_URL } unless pkg pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference) pkg.repositoryURL = LK_URL pkg.requirement = { 'kind' => 'exactVersion', 'version' => LK_VERSION } root.package_references << pkg end prod = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency) prod.package = pkg prod.product_name = 'LiveKit' ext.package_product_dependencies << prod bf = project.new(Xcodeproj::Project::Object::PBXBuildFile) bf.product_ref = prod ext.frameworks_build_phase.files << bf # ── App Group entitlement on the MAIN app target too (merge — keep aps-environment etc.) ────────────── app_ent_path = File.join(APP_DIR, 'App', 'App.entitlements') app_ent = File.exist?(app_ent_path) ? (Xcodeproj::Plist.read_from_path(app_ent_path) || {}) : {} groups = app_ent['com.apple.security.application-groups'] || [] groups << APP_GROUP unless groups.include?(APP_GROUP) app_ent['com.apple.security.application-groups'] = groups Xcodeproj::Plist.write_to_path(app_ent, app_ent_path) app.build_configurations.each { |cfg| cfg.build_settings['CODE_SIGN_ENTITLEMENTS'] = 'App/App.entitlements' } # ── Embed the extension into the app + depend on it ────────────────────────────────────────────────── app.add_dependency(ext) embed = app.build_phases.find { |p| p.respond_to?(:symbol_dst_subfolder_spec) && p.symbol_dst_subfolder_spec == :plug_ins } embed ||= begin phase = app.new_copy_files_build_phase('Embed App Extensions') phase.symbol_dst_subfolder_spec = :plug_ins phase end build_file = embed.add_file_reference(ext.product_reference) build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] } project.save puts "Broadcast Extension injected: #{EXT_NAME} (#{EXT_BUNDLE}) linked to LiveKit #{LK_VERSION}, embedded in #{APP_TARGET}"