# encoding: utf-8 # Copyright (c) 2012, HipByte SPRL and contributors # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. require 'motion/project/config' require 'motion/util/code_sign' require 'motion/project/target' module Motion; module Project; class XcodeConfig < Config variable :xcode_dir, :sdk_version, :deployment_target, :frameworks, :weak_frameworks, :embedded_frameworks, :external_frameworks, :framework_search_paths, :libs, :identifier, :codesign_certificate, :short_version, :entitlements, :delegate_class, :version def initialize(project_dir, build_mode) super @info_plist = {} @frameworks = [] @weak_frameworks = [] @embedded_frameworks = [] @external_frameworks = [] @framework_search_paths = [] @libs = [] @targets = [] @bundle_signature = '????' @short_version = nil @entitlements = {} @delegate_class = 'AppDelegate' @spec_mode = false @vendor_projects = [] @version = '1.0' end def xcode_dir @xcode_version = nil @xcode_dir ||= begin xcode_dot_app_path = '/Applications/Xcode.app/Contents/Developer' # First, honor /usr/bin/xcode-select xcodeselect = '/usr/bin/xcode-select' if File.exist?(xcodeselect) path = `#{xcodeselect} -print-path`.strip if path.match(/^\/Developer\//) and File.exist?(xcode_dot_app_path) @xcode_error_printed ||= false $stderr.puts(< Util::Version.new(sdk_version) App.fail "Deployment target `#{deployment_target}' must be equal or lesser than SDK version `#{sdk_version}'" end unless File.exist?(datadir) App.fail "iOS deployment target #{deployment_target} is not supported by this version of RubyMotion" end # embedded_frameworks %w{ embedded_frameworks external_frameworks }.each do |attr| value = send(attr) if !(value.is_a?(Array) and value.all? { |x| File.exist?(x) and File.extname(x) == '.framework' }) App.fail "app.#{attr} should be an array of framework paths" end end super end def platforms_dir File.join(xcode_dir, 'Platforms') end def platform_dir(platform) File.join(platforms_dir, platform + '.platform') end def sdk_version @sdk_version ||= begin versions = Dir.glob(File.join(platforms_dir, "#{deploy_platform}.platform/Developer/SDKs/#{deploy_platform}[1-9]*.sdk")).map do |path| File.basename(path).scan(/#{deploy_platform}(.*)\.sdk/)[0][0] end if versions.size == 0 App.fail "Can't find an iOS SDK in `#{platforms_dir}'" end supported_version = supported_sdk_versions(versions) unless supported_version # We don't have BridgeSupport data for any of the available SDKs. So # use the latest available SDK of which the major version is the same # as the latest available BridgeSupport version. supported_sdks = supported_versions.map do |version| Util::Version.new(version) end.sort.reverse available_sdks = versions.map do |version| Util::Version.new(version) end.sort.reverse available_sdks.each do |available_sdk| major_version = available_sdk.segments.first compatible_sdk = supported_sdks.find do |supported_sdk| supported_sdk.segments.first == major_version end if compatible_sdk # Never override a user's setting! @deployment_target ||= compatible_sdk.to_s supported_version = available_sdk.to_s App.warn("The available SDK (#{available_sdk}) is newer than " \ "the latest available RubyMotion BridgeSupport " \ "metadata (#{compatible_sdk}). The `sdk_version` and " \ "`deployment_target` settings will be configured " \ "accordingly.") break end end end supported_version || App.fail("The requested deployment target SDK " \ "is not available or supported by " \ "RubyMotion at this time.") end end def deployment_target @deployment_target ||= sdk_version end def sdk(platform) path = File.join(platform_dir(platform), 'Developer/SDKs', platform + sdk_version + '.sdk') escape_path(path) end def frameworks_dependencies @frameworks_dependencies ||= begin # Compute the list of frameworks, including dependencies, that the project uses. deps = frameworks.dup.uniq slf = File.join(sdk(local_platform), 'System', 'Library', 'Frameworks') find_dependencies = lambda { |framework_path| if File.exist?(framework_path) `#{locate_binary('otool')} -L \"#{framework_path}\"`.scan(/\t([^\s]+)\s\(/).each do |dep| # Only care about public, non-umbrella frameworks (for now). if md = dep[0].match(/^\/System\/Library\/Frameworks\/(.+)\.framework\/(Versions\/.\/)?(.+)$/) and md[1] == md[3] if File.exist?(File.join(datadir, 'BridgeSupport', md[1] + '.bridgesupport')) deps << md[1] deps.uniq! end end end end } deps.each do |framework| framework_path = File.join(slf, framework + '.framework', framework) find_dependencies.call(framework_path) end embedded_frameworks.each do |framework| framework_path = File.expand_path(File.join(framework, File.basename(framework, ".framework"))) find_dependencies.call(framework_path) end if @framework_search_paths.empty? deps = deps.select { |dep| File.exist?(File.join(datadir, 'BridgeSupport', dep + '.bridgesupport')) } end deps end end def frameworks_stubs_objects(platform) stubs = [] (frameworks_dependencies + weak_frameworks).uniq.each do |framework| stubs_obj = File.join(datadir, platform, "#{framework}_stubs.o") stubs << stubs_obj if File.exist?(stubs_obj) end stubs end def bridgesupport_files @bridgesupport_files ||= begin bs_files = [] deps = ['RubyMotion'] + (frameworks_dependencies + weak_frameworks).uniq deps << 'UIAutomation' if spec_mode deps.each do |framework| supported_versions.each do |ver| next if Util::Version.new(ver) < Util::Version.new(deployment_target) || Util::Version.new(sdk_version) < Util::Version.new(ver) bs_path = File.join(datadir(ver), 'BridgeSupport', framework + '.bridgesupport') if File.exist?(bs_path) bs_files << bs_path end end end bs_files end end def default_archs h = {} platforms.each do |platform| h[platform] = Dir.glob(File.join(datadir, platform, '*.bc')).map do |path| path.scan(/kernel-(.+).bc$/)[0][0] end end h end def archs @archs ||= default_archs end def arch_flags(platform) archs[platform].map { |x| "-arch #{x}" }.join(' ') end def common_flags(platform) "#{arch_flags(platform)} -isysroot \"#{unescape_path(sdk(platform))}\" -F#{sdk(platform)}/System/Library/Frameworks" end def cflags(platform, cplusplus) optz_level = development? ? '-O0' : '-O3' "#{common_flags(platform)} #{optz_level} -fexceptions -fblocks" + (cplusplus ? '' : ' -std=c99') + (xcode_version[0] < '5.0' ? '' : ' -fmodules') end def ldflags(platform) common_flags(platform) + ' -Wl,-no_pie' end # @return [String] The application bundle name, excluding extname. # def bundle_name name + (spec_mode ? '_spec' : '') end # @return [String] The application bundle filename, including extname. # def bundle_filename bundle_name + '.app' end def versionized_build_dir(platform) File.join(build_dir, platform + '-' + deployment_target + '-' + build_mode_name) end def app_bundle_dsym(platform) File.join(versionized_build_dir(platform), bundle_filename + '.dSYM') end def archive_extension raise "not implemented" end def archive File.join(versionized_build_dir(deploy_platform), bundle_name + archive_extension) end def identifier @identifier ||= "com.yourcompany.#{name.gsub(/\s/, '')}" spec_mode ? @identifier + '_spec' : @identifier end def info_plist @info_plist end def dt_info_plist {} end def generic_info_plist { 'BuildMachineOSBuild' => `sw_vers -buildVersion`.strip, 'CFBundleDevelopmentRegion' => 'en', 'CFBundleName' => name, 'CFBundleDisplayName' => name, 'CFBundleIdentifier' => identifier, 'CFBundleExecutable' => name, 'CFBundleInfoDictionaryVersion' => '6.0', 'CFBundlePackageType' => 'APPL', 'CFBundleShortVersionString' => (@short_version || @version), 'CFBundleSignature' => @bundle_signature, 'CFBundleVersion' => @version } end # TODO # * Add env vars from user. # * Add optional Instruments template to use. def profiler_config_plist(platform, args, template, builtin_templates, set_build_env = true) working_dir = File.expand_path(versionized_build_dir(platform)) optional_data = {} if template template_path = nil if File.exist?(template) template_path = template elsif !builtin_templates.grep(/#{template}/i).empty? template = template.downcase template_path = profiler_known_templates.find do |path| File.basename(path, File.extname(path)).downcase == template end else App.fail("Invalid Instruments template path or name.") end if xcode_version[0] >= '6.0' && xcode_version[0] < '6.2' # workaround for RM-599 and RM-672 template_path = File.expand_path("#{xcode_dir}/../Applications/Instruments.app/Contents/Resources/templates/#{template_path}.tracetemplate") end optional_data['XrayTemplatePath'] = template_path end env = ENV.to_hash if set_build_env env.merge!({ 'DYLD_FRAMEWORK_PATH' => working_dir, 'DYLD_LIBRARY_PATH' => working_dir, '__XCODE_BUILT_PRODUCTS_DIR_PATHS' => working_dir, '__XPC_DYLD_FRAMEWORK_PATH' => working_dir, '__XPC_DYLD_LIBRARY_PATH' => working_dir, }) end { 'CFBundleIdentifier' => identifier, 'absolutePathOfLaunchable' => File.expand_path(app_bundle_executable(platform)), 'argumentEntries' => (args or ''), 'workingDirectory' => working_dir, 'workspacePath' => '', # Normally: /path/to/Project.xcodeproj/project.xcworkspace 'environmentEntries' => env, 'optionalData' => { 'launchOptions' => { 'architectureType' => 1, }, }.merge(optional_data), } end def profiler_known_templates # Get a list of just the templates (ignoring devices) list = `#{locate_binary('instruments')} -s 2>&1`.strip.split("\n") start = list.index('Known Templates:') + 1 list = list[start..-1] # Only interested in the template (file base) names list.map { |line| line.sub(/^\s*"/, '').sub(/",*$/, '') } end def profiler_config_device_identifier(device_name, target) if xcode_version[0] >= '6.0' re = /#{device_name} \(#{target} Simulator\) \[(.+)\]/ `#{locate_binary('instruments')} -s 2>&1`.strip.split("\n").each { |line| if m = re.match(line) return m[1] end } else App.config.sdk('iPhoneSimulator') end end def pkginfo_data "AAPL#{@bundle_signature}" end def codesign_certificate(platform) @codesign_certificate ||= begin type = (distribution_mode ? 'Distribution' : 'Developer') certs = Util::CodeSign.identity_names(release?) certs = certs.grep(/#{platform} #{type}/) if certs.size == 0 App.fail "Cannot find any #{platform} #{type} certificate in the " \ "keychain" elsif certs.size > 1 App.warn "Found #{certs.size} #{platform} #{type} certificates in " \ "the keychain. Set the `codesign_certificate' project " \ "setting to explicitely use one of (defaults to the " \ "first): #{certs.join(', ')}" end certs.first end end def gen_bridge_metadata(platform, headers, bs_file, c_flags, exceptions=[]) # Instead of potentially passing hundreds of arguments to the # `gen_bridge_metadata` command, which can lead to a 'too many arguments' # error, we list them in a temp file and pass that to the command. require 'tempfile' headers_file = Tempfile.new('gen_bridge_metadata-headers-list') headers.each { |header| headers_file.puts(header) } headers_file.close # flush # Prepare rest of options. sdk_path = self.sdk(local_platform) includes = ['-I.'] + headers.map { |header| "-I'#{File.dirname(header)}'" }.uniq exceptions = exceptions.map { |x| "\"#{x}\"" }.join(' ') c_flags = "#{c_flags} -isysroot '#{sdk_path}' #{bridgesupport_cflags} #{includes.join(' ')}" sh "RUBYOPT='' '#{File.join(bindir, 'gen_bridge_metadata')}' #{bridgesupport_flags} --cflags \"#{c_flags}\" --headers \"#{headers_file.path}\" -o '#{bs_file}' #{ "-e #{exceptions}" if exceptions.length != 0}" end def define_global_env_txt "rb_define_global_const(\"RUBYMOTION_ENV\", @\"#{rubymotion_env_value}\");\nrb_define_global_const(\"RUBYMOTION_VERSION\", @\"#{Motion::Version}\");\n" end def spritekit_texture_atlas_compiler path = File.join(xcode_dir, 'usr/bin/TextureAtlas') File.exist?(path) ? path : nil end def assets_bundles xcassets_bundles = [] resources_dirs.each do |dir| if File.exist?(dir) xcassets_bundles.concat(Dir.glob(File.join(dir, '*.xcassets'))) end end xcassets_bundles end def app_icons_asset_bundle app_icons_asset_bundles = assets_bundles.map { |b| Dir.glob(File.join(b, '*.appiconset')) }.flatten if app_icons_asset_bundles.size > 1 App.warn "Found #{app_icons_asset_bundles.size} app icon sets across all " \ "xcasset bundles. Only the first one (alphabetically) " \ "will be used." end app_icons_asset_bundles.sort.first end def app_icon_name_from_asset_bundle File.basename(app_icons_asset_bundle, '.appiconset') end attr_reader :vendor_projects def vendor_project(path, type, opts={}) opts[:force_load] = true unless opts[:force_load] == false @vendor_projects << Motion::Project::Vendor.new(path, type, self, opts) end def unvendor_project(path) @vendor_projects.delete_if { |x| x.path == path } end def clean_project super @vendor_projects.each { |vendor| vendor.clean(platforms) } @targets.each { |target| target.clean } end attr_accessor :targets # App Extensions are required to include a 64-bit slice for App Store # submission, so do not exclude `arm64` by default. # # From https://developer.apple.com/library/prerelease/iOS/documentation/General/Conceptual/ExtensibilityPG/ExtensionCreation.html: # # NOTE ABOUT 64-BIT ARCHITECTURE # # An app extension target must include the arm64 (iOS) or x86_64 # architecture (OS X) in its Architectures build settings or it will be # rejected by the App Store. Xcode includes the appropriate 64-bit # architecture with its “Standard architectures” setting when you create a # new app extension target. # # If your containing app target links to an embedded framework, the app # must also include 64-bit architecture or it will be rejected by the App # Store. # # From https://developer.apple.com/library/ios/documentation/General/Conceptual/ExtensibilityPG/ExtensionScenarios.html#//apple_ref/doc/uid/TP40014214-CH21-SW5 # # A containing app that links to an embedded framework must include the # arm64 (iOS) or x86_64 (OS X) architecture build setting or it will be # rejected by the App Store. # def target(path, type, opts={}) unless File.exist?(path) App.fail "Could not find target of type '#{type}' at '#{path}'" end unless archs['iPhoneOS'].include?('arm64') App.warn "Device builds of App Extensions and Frameworks are " \ "required to have a 64-bit slice for App Store submissions " \ "to be accepted." App.warn "Your application will now have 64-bit enabled by default, " \ "be sure to properly test it on a 64-bit device." archs['iPhoneOS'] << 'arm64' end case type when :framework opts[:load] = true unless opts[:load] == false @targets << Motion::Project::FrameworkTarget.new(path, type, self, opts) when :extension @targets << Motion::Project::ExtensionTarget.new(path, type, self, opts) else App.fail("Unsupported target type '#{type}'") end end end end; end