#!/usr/bin/env ruby # Adds Plezy's tvOS Top Shelf extension target and Runner-side bridge source. require 'xcodeproj' PROJECT_PATH = File.expand_path('../Runner.xcodeproj', __dir__) SCHEME_PATH = File.expand_path('../Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme', __dir__) project = Xcodeproj::Project.open(PROJECT_PATH) runner = project.targets.find { |t| t.name == 'Runner' } raise 'Runner target not found' unless runner main_group = project.main_group runner_group = main_group['Runner'] raise 'Runner group not found' unless runner_group products_group = main_group['Products'] || main_group.new_group('Products') frameworks_group = main_group['Frameworks'] || main_group.new_group('Frameworks') generated_config_ref = project.files.find { |file| file.path == 'Flutter/Generated.xcconfig' } raise 'Generated.xcconfig not found' unless generated_config_ref def ensure_file(group, path, name: nil, source_tree: '') existing = group.files.find { |f| f.path == path || f.display_name == (name || File.basename(path)) } return existing if existing ref = group.new_file(path) ref.name = name if name ref.source_tree = source_tree ref end def ensure_source(target, file_ref) phase = target.source_build_phase return if phase.files_references.include?(file_ref) phase.add_file_reference(file_ref, true) end def ensure_copy_file(project, phase, file_ref) existing = phase.files.find { |build_file| build_file.file_ref == file_ref } return existing if existing build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile) build_file.file_ref = file_ref build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] } phase.files << build_file build_file end def ensure_framework(target, file_ref) phase = target.frameworks_build_phase return if phase.files_references.include?(file_ref) phase.add_file_reference(file_ref, true) end def ensure_shell_script(target, name, script) phase = target.shell_script_build_phases.find { |p| p.name == name } unless phase phase = target.new_shell_script_build_phase(name) end phase.shell_path = '/bin/sh' phase.shell_script = script phase end system_shelf_ref = ensure_file(runner_group, 'SystemShelfPlugin.swift') ensure_source(runner, system_shelf_ref) ensure_file(runner_group, 'Runner.entitlements') tests_group = main_group['RunnerTests'] || main_group.new_group('RunnerTests', 'RunnerTests') test_target = project.targets.find { |target| target.name == 'RunnerTests' } unless test_target test_target = project.new_target(:unit_test_bundle, 'RunnerTests', :tvos, '14.0') end test_target.product_type = 'com.apple.product-type.bundle.unit-test' test_target.frameworks_build_phase.files.delete_if do |build_file| build_file.file_ref&.display_name == 'Foundation.framework' end project.files.select { |file| file.display_name == 'Foundation.framework' }.each do |file_ref| still_used = project.targets.any? do |target| target.frameworks_build_phase.files_references.include?(file_ref) end file_ref.remove_from_project unless still_used end RUNNER_TESTS_DIR = File.expand_path('../RunnerTests', __dir__) COMPILED_TEST_EXTENSIONS = %w[.swift .m .mm].freeze runner_test_files = Dir.children(RUNNER_TESTS_DIR).reject { |name| name.start_with?('.') }.sort runner_test_sources = runner_test_files.select { |name| COMPILED_TEST_EXTENSIONS.include?(File.extname(name)) } raise "No RunnerTests sources found in #{RUNNER_TESTS_DIR}" if runner_test_sources.empty? test_target.source_build_phase.files.delete_if do |build_file| file_ref = build_file.file_ref file_ref && !runner_test_sources.include?(file_ref.display_name) end tests_group.files.reject { |file_ref| runner_test_files.include?(file_ref.display_name) }.each do |file_ref| file_ref.remove_from_project end runner_test_sources.each do |filename| ensure_source(test_target, ensure_file(tests_group, filename)) end test_target.add_dependency(runner) unless test_target.dependencies.any? { |dependency| dependency.target == runner } test_target.build_configurations.each do |config| settings = config.build_settings settings['BUNDLE_LOADER'] = '$(TEST_HOST)' settings.delete('CODE_SIGNING_ALLOWED') settings['GENERATE_INFOPLIST_FILE'] = 'YES' settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.edde746.plezy.RunnerTests' settings['SDKROOT'] = 'appletvos' settings['SUPPORTED_PLATFORMS'] = 'appletvos appletvsimulator' settings['SWIFT_VERSION'] = '5.0' settings['TARGETED_DEVICE_FAMILY'] = '3' settings['TEST_HOST'] = '$(BUILT_PRODUCTS_DIR)/Runner.app/Runner' settings['TVOS_DEPLOYMENT_TARGET'] = '14.0' end event_delivery_ref = ensure_file(runner_group, 'TvosEventDeliveryCoordinator.swift') ensure_source(runner, event_delivery_ref) extension_group = main_group['TopShelfExtension'] || main_group.new_group('TopShelfExtension', 'TopShelfExtension') top_shelf_ref = ensure_file(extension_group, 'TopShelfProvider.swift') ensure_file(extension_group, 'Info.plist') ensure_file(extension_group, 'TopShelfExtension.entitlements') extension_target = project.targets.find { |t| t.name == 'TopShelfExtension' } unless extension_target extension_target = project.new_target(:app_extension, 'TopShelfExtension', :tvos, '14.0') end extension_target.product_type = 'com.apple.product-type.app-extension' ensure_source(extension_target, top_shelf_ref) removed_framework_refs = [] extension_target.frameworks_build_phase.files.delete_if do |build_file| next false unless build_file.file_ref&.display_name == 'Foundation.framework' removed_framework_refs << build_file.file_ref true end removed_framework_refs.compact.uniq.each do |file_ref| still_used = project.targets.any? do |target| target.frameworks_build_phase.files_references.include?(file_ref) end file_ref.remove_from_project unless still_used end tv_services_ref = ensure_file( frameworks_group, 'System/Library/Frameworks/TVServices.framework', name: 'TVServices.framework', source_tree: 'SDKROOT' ) ensure_framework(extension_target, tv_services_ref) extension_product = extension_target.product_reference extension_product.name = 'TopShelfExtension.appex' extension_product.path = 'TopShelfExtension.appex' extension_product.explicit_file_type = 'wrapper.app-extension' products_group.children << extension_product unless products_group.children.include?(extension_product) runner.add_dependency(extension_target) unless runner.dependencies.any? { |d| d.target == extension_target } embed_phase = runner.copy_files_build_phases.find { |phase| phase.name == 'Embed App Extensions' } unless embed_phase embed_phase = project.new(Xcodeproj::Project::Object::PBXCopyFilesBuildPhase) embed_phase.name = 'Embed App Extensions' embed_phase.dst_subfolder_spec = '13' embed_phase.dst_path = '' runner.build_phases.insert(-3, embed_phase) end ensure_copy_file(project, embed_phase, extension_product) runner.build_configurations.each do |config| config.build_settings['CODE_SIGN_ENTITLEMENTS'] = 'Runner/Runner.entitlements' end extension_target.build_configurations.each do |config| config.base_configuration_reference = generated_config_ref settings = config.build_settings settings['APPLICATION_EXTENSION_API_ONLY'] = 'YES' settings['CLANG_ENABLE_MODULES'] = 'YES' settings['CODE_SIGN_ENTITLEMENTS'] = 'TopShelfExtension/TopShelfExtension.entitlements' settings['CODE_SIGN_IDENTITY'] = 'Apple Development' settings['CODE_SIGN_STYLE'] = 'Automatic' settings['CURRENT_PROJECT_VERSION'] = '$(FLUTTER_BUILD_NUMBER)' settings['DEVELOPMENT_TEAM'] = 'G88U5B5783' settings['ENABLE_BITCODE'] = 'NO' settings['INFOPLIST_FILE'] = 'TopShelfExtension/Info.plist' settings['LD_RUNPATH_SEARCH_PATHS'] = [ '$(inherited)', '@executable_path/Frameworks', '@executable_path/../../Frameworks', ] settings['MARKETING_VERSION'] = '$(FLUTTER_BUILD_NAME)' settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.edde746.plezy.TopShelfExtension' settings['PRODUCT_NAME'] = '$(TARGET_NAME)' settings['SDKROOT'] = 'appletvos' settings['SKIP_INSTALL'] = 'YES' settings['SUPPORTED_PLATFORMS'] = 'appletvos appletvsimulator' settings['SWIFT_VERSION'] = '5.0' settings['TARGETED_DEVICE_FAMILY'] = '3' settings['TVOS_DEPLOYMENT_TARGET'] = '14.0' end ensure_shell_script( extension_target, 'Sync Version', '/bin/bash "$SOURCE_ROOT/scripts/xcode_appletv.sh" sync_version' + "\n" ) scheme = Xcodeproj::XCScheme.new(SCHEME_PATH) unless scheme.test_action.testables.any? do |testable| testable.buildable_references.any? { |reference| reference.target_name == test_target.name } end scheme.add_test_target(test_target) end scheme.save! project.save puts 'Saved Top Shelf wiring'