diff --git a/lib/motion/project.rb b/lib/motion/project.rb index 062c7ec9..daecdf76 100644 --- a/lib/motion/project.rb +++ b/lib/motion/project.rb @@ -26,11 +26,15 @@ require 'motion/project/app' require 'motion/project/config' require 'motion/project/builder' require 'motion/project/vendor' +require 'motion/project/template' require 'motion/project/plist' -App = Motion::Project::App +if Motion::Project::App.template == nil + warn "require 'lib/motion/project' is deprecated, please require 'lib/motion/project/template/ios' instead" + require 'motion/project/template/ios' +end -# Check for software updates. +# Check for updates. system('/usr/bin/motion update --check') if $?.exitstatus == 2 puts '=' * 80 @@ -39,115 +43,6 @@ if $?.exitstatus == 2 puts '' end -desc "Build the project, then run the simulator" -task :default => :simulator - -desc "Build everything" -task :build => ['build:simulator', 'build:device'] - -namespace :build do - desc "Build the simulator version" - task :simulator do - App.build('iPhoneSimulator') - end - - desc "Build the device version" - task :device do - App.build('iPhoneOS') - App.codesign('iPhoneOS') - end -end - -desc "Run the simulator" -task :simulator => ['build:simulator'] do - app = App.config.app_bundle('iPhoneSimulator') - target = ENV['target'] || App.config.sdk_version - - # Cleanup the simulator application sandbox, to avoid having old resource files there. - if ENV['clean'] - sim_apps = File.expand_path("~/Library/Application Support/iPhone Simulator/#{target}/Applications") - Dir.glob("#{sim_apps}/**/*.app").each do |app_bundle| - if File.basename(app_bundle) == File.basename(app) - rm_rf File.dirname(app_bundle) - break - end - end - end - - # Prepare the device family. - family_int = - if family = ENV['device_family'] - App.config.device_family_int(family.downcase.intern) - else - App.config.device_family_ints[0] - end - retina = ENV['retina'] - - # Configure the SimulateDevice variable (the only way to specify if we want to run in retina mode or not). - simulate_device = App.config.device_family_string(family_int, target, retina) - default_simulator = `/usr/bin/defaults read com.apple.iphonesimulator "SimulateDevice"`.strip - if default_simulator != simulate_device && default_simulator != "'#{simulate_device}'" - system("/usr/bin/killall \"iPhone Simulator\" >& /dev/null") - system("/usr/bin/defaults write com.apple.iphonesimulator \"SimulateDevice\" \"'#{simulate_device}'\"") - end - - # Launch the simulator. - xcode = App.config.xcode_dir - env = "DYLD_FRAMEWORK_PATH=\"#{xcode}/../Frameworks\":\"#{xcode}/../OtherFrameworks\"" - env << ' SIM_SPEC_MODE=1' if App.config.spec_mode - sim = File.join(App.config.bindir, 'sim') - debug = (ENV['debug'] ? 1 : (App.config.spec_mode ? '0' : '2')) - App.info 'Simulate', app - at_exit { system("stty echo") } if $stdout.tty? # Just in case the simulator launcher crashes and leaves the terminal without echo. - sh "#{env} #{sim} #{debug} #{family_int} #{target} \"#{xcode}\" \"#{app}\"" -end - -desc "Create an .ipa archive" -task :archive => ['build:device'] do - App.archive -end - -namespace :archive do - desc "Create an .ipa archive for distribution (AppStore)" - task :distribution do - App.config_without_setup.build_mode = :release - App.config.distribution_mode = true - Rake::Task["build:device"].invoke - App.archive - end -end - -desc "Same as 'spec:simulator'" -task :spec => ['spec:simulator'] - -namespace :spec do - desc "Run the test/spec suite on the simulator" - task :simulator do - App.config.spec_mode = true - Rake::Task["simulator"].invoke - end - - desc "Run the test/spec suite on the device" - task :device do - App.config.spec_mode = true - ENV['debug'] ||= '1' - Rake::Task["device"].invoke - end -end - -desc "Deploy on the device" -task :device => :archive do - App.info 'Deploy', App.config.archive - device_id = (ENV['id'] or App.config.device_id) - unless App.config.provisioned_devices.include?(device_id) - App.fail "Device ID `#{device_id}' not provisioned in profile `#{App.config.provisioning_profile}'" - end - env = "XCODE_DIR=\"#{App.config.xcode_dir}\"" - deploy = File.join(App.config.bindir, 'deploy') - flags = Rake.application.options.trace ? '-d' : '' - sh "#{env} #{deploy} #{flags} \"#{device_id}\" \"#{App.config.archive}\"" -end - desc "Clear build objects" task :clean do App.info 'Delete', App.config.build_dir @@ -166,44 +61,3 @@ task :config do puts key.ljust(22) + " : #{map[key].inspect}" end end - -desc "Generate ctags" -task :ctags do - tags_file = 'tags' - config = App.config - if !File.exist?(tags_file) or File.mtime(config.project_file) > File.mtime(tags_file) - bs_files = config.bridgesupport_files + config.vendor_projects.map { |p| Dir.glob(File.join(p.path, '*.bridgesupport')) }.flatten - ctags = File.join(config.bindir, 'ctags') - config = File.join(config.motiondir, 'data', 'bridgesupport-ctags.cfg') - sh "#{ctags} --options=\"#{config}\" #{bs_files.map { |x| '"' + x + '"' }.join(' ')}" - end -end - -desc "Create a .a static library" -task :static do - libs = %w{iPhoneSimulator iPhoneOS}.map do |platform| - '"' + App.build(platform, :static => true) + '"' - end - fat_lib = File.join(App.config.build_dir, App.config.name + '-universal.a') - App.info 'Create', fat_lib - sh "/usr/bin/lipo -create #{libs.join(' ')} -output \"#{fat_lib}\"" -end - -=begin -# Automatically load project extensions. A project extension is a gem whose -# name starts with `motion-' and which exposes a `lib/motion/project' libdir. -require 'rubygems' -Gem.path.each do |gemdir| - Dir.glob(File.join(gemdir, 'gems', '*')).each do |gempath| - base = File.basename(gempath) - if md = base.match(/^(motion-.*)-((\d+\.)*\d+)/) and File.exist?(File.join(gempath, 'lib', 'motion', 'project')) - ext_name = md[1] - begin - require ext_name - rescue LoadError => e - $stderr.puts "Can't autoload extension `#{ext_name}': #{e.message}" - end - end - end -end -=end diff --git a/lib/motion/project/app.rb b/lib/motion/project/app.rb index f512cbfa..7bac15f9 100644 --- a/lib/motion/project/app.rb +++ b/lib/motion/project/app.rb @@ -21,7 +21,7 @@ # (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 'erb' +require 'motion/project/template' module Motion; module Project class App @@ -38,6 +38,8 @@ module Motion; module Project end class << self + attr_accessor :template + def config_mode @config_mode ||= begin if mode = ENV['mode'] @@ -55,7 +57,7 @@ module Motion; module Project def config_without_setup @configs ||= {} - @configs[config_mode] ||= Motion::Project::Config.new('.', config_mode) + @configs[config_mode] ||= Motion::Project::Config.make(@template, '.', config_mode) end def config @@ -82,8 +84,8 @@ module Motion; module Project builder.codesign(config, platform) end - def create(app_name, template_name="ios") - Template.new(app_name, template_name).generate + def create(app_name, template_name=:ios) + Motion::Project::Template.new(app_name, template_name).generate end def log(what, msg) @@ -109,75 +111,5 @@ module Motion; module Project log what, msg unless VERBOSE end end - - class Template - # for ERB - attr_reader :name - - def initialize(app_name, template_name) - @name = @app_name = app_name - @template_name = template_name - @template_directory = File.expand_path(File.join(__FILE__, "../template/#{@template_name}")) - - unless app_name.match(/^[\w\s-]+$/) - fail "Invalid app name" - end - - if File.exist?(app_name) - fail "Directory `#{app_name}' already exists" - end - - unless File.exist?(@template_directory) - fail "Invalid template name" - end - end - - def generate - App.log 'Create', @app_name - FileUtils.mkdir(@app_name) - - Dir.chdir(@app_name) do - create_directories() - create_files() - end - end - - private - - def template_directory - @template_directory - end - - def create_directories - Dir.glob(File.join(template_directory, "**/")).each do |dir| - dir.sub!("#{template_directory}/", '') - FileUtils.mkdir_p(dir) if dir.length > 0 - end - end - - def create_files - Dir.glob(File.join(template_directory, "**/*"), File::FNM_DOTMATCH).each do |src| - dest = src.sub("#{template_directory}/", '') - next if File.directory?(src) - next if dest.include?(".DS_Store") - - dest = replace_file_name(dest) - if dest =~ /(.+)\.erb$/ - App.log 'Create', "#{@app_name}/#{$1}" - File.open($1, "w") { |io| - io.print ERB.new(File.read(src)).result(binding) - } - else - App.log 'Create', "#{@app_name}/#{dest}" - FileUtils.cp(src, dest) - end - end - end - - def replace_file_name(file_name) - file_name = file_name.sub("{name}", "#{@name}") - file_name - end - end end end; end diff --git a/lib/motion/project/builder.rb b/lib/motion/project/builder.rb index 5a3ecb15..340427d3 100644 --- a/lib/motion/project/builder.rb +++ b/lib/motion/project/builder.rb @@ -88,7 +88,7 @@ module Motion; module Project; # LLVM bitcode. bc = File.join(objs_build_dir, "#{path}.#{arch}.bc") bs_flags = bs_files.map { |x| "--uses-bs \"" + x + "\" " }.join(' ') - sh "/usr/bin/env VM_KERNEL_PATH=\"#{kernel}\" VM_OPT_LEVEL=\"#{config.opt_level}\" #{ruby} #{bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"" + sh "/usr/bin/env VM_KERNEL_PATH=\"#{kernel}\" VM_OPT_LEVEL=\"#{config.opt_level}\" /usr/bin/arch -arch #{arch} #{ruby} #{bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"" # Assembly. asm = File.join(objs_build_dir, "#{path}.#{arch}.s") @@ -180,8 +180,6 @@ module Motion; module Project; # Generate init file. init_txt = < - extern "C" { void ruby_sysinit(int *, char ***); void ruby_init(void); @@ -213,17 +211,21 @@ RubyMotionInit(int argc, char **argv) const char *progname = argv[0]; ruby_script(progname); } +#if !__LP64__ try { +#endif void *self = rb_vm_top_self(); EOS app_objs.each do |_, init_func| init_txt << "#{init_func}(self, 0);\n" end init_txt << < - -extern "C" { - void rb_define_global_const(const char *, void *); - void rb_rb2oc_exc_handler(void); - void rb_exit(int); - void RubyMotionInit(int argc, char **argv); -EOS - if config.spec_mode - spec_objs.each do |_, init_func| - main_txt << "void #{init_func}(void *, void *);\n" - end - end - main_txt << < - -@implementation SpecLauncher - -+ (id)launcher -{ - [UIApplication sharedApplication]; - - // Enable simulator accessibility. - // Thanks http://www.stewgleadow.com/blog/2011/10/14/enabling-accessibility-for-ios-applications/ - NSString *simulatorRoot = [[[NSProcessInfo processInfo] environment] objectForKey:@"IPHONE_SIMULATOR_ROOT"]; - if (simulatorRoot != nil) { - void *appSupportLibrary = dlopen([[simulatorRoot stringByAppendingPathComponent:@"/System/Library/PrivateFrameworks/AppSupport.framework/AppSupport"] fileSystemRepresentation], RTLD_LAZY); - CFStringRef (*copySharedResourcesPreferencesDomainForDomain)(CFStringRef domain) = (CFStringRef (*)(CFStringRef)) dlsym(appSupportLibrary, "CPCopySharedResourcesPreferencesDomainForDomain"); - - if (copySharedResourcesPreferencesDomainForDomain != NULL) { - CFStringRef accessibilityDomain = copySharedResourcesPreferencesDomainForDomain(CFSTR("com.apple.Accessibility")); - - if (accessibilityDomain != NULL) { - CFPreferencesSetValue(CFSTR("ApplicationAccessibilityEnabled"), kCFBooleanTrue, accessibilityDomain, kCFPreferencesAnyUser, kCFPreferencesAnyHost); - CFRelease(accessibilityDomain); - } - } - } - - // Load the UIAutomation framework. - dlopen("/Developer/Library/PrivateFrameworks/UIAutomation.framework/UIAutomation", RTLD_LOCAL); - - SpecLauncher *launcher = [[self alloc] init]; - [[NSNotificationCenter defaultCenter] addObserver:launcher selector:@selector(appLaunched:) name:UIApplicationDidBecomeActiveNotification object:nil]; - return launcher; -} - -- (void)appLaunched:(id)notification -{ - // Give a bit of time for the simulator to attach... - [self performSelector:@selector(runSpecs) withObject:nil afterDelay:0.3]; -} - -- (void)runSpecs -{ -EOS - spec_objs.each do |_, init_func| - main_txt << "#{init_func}(self, 0);\n" - end - main_txt << "[NSClassFromString(@\"Bacon\") performSelector:@selector(run)];\n" - main_txt << < File.mtime(main_exec) \ @@ -439,6 +340,7 @@ EOS end # Copy resources, handle subdirectories. + app_resources_dir = config.app_resources_dir(platform) reserved_app_bundle_files = [ '_CodeSignature/CodeResources', 'CodeResources', 'embedded.mobileprovision', 'Info.plist', 'PkgInfo', 'ResourceRules.plist', @@ -458,7 +360,7 @@ EOS if reserved_app_bundle_files.include?(res) App.fail "Cannot use `#{res_path}' as a resource file because it's a reserved application bundle file" end - dest_path = File.join(bundle_path, res) + dest_path = File.join(app_resources_dir, res) if !File.exist?(dest_path) or File.mtime(res_path) > File.mtime(dest_path) FileUtils.mkdir_p(File.dirname(dest_path)) App.info 'Copy', res_path @@ -468,7 +370,7 @@ EOS # Delete old resource files. resources_files = resources_paths.map { |x| path_on_resources_dirs(config.resources_dirs, x) } - Dir.chdir(bundle_path) do + Dir.chdir(app_resources_dir) do Dir.glob('*').each do |bundle_res| bundle_res = convert_filesystem_encoding(bundle_res) next if File.directory?(bundle_res) diff --git a/lib/motion/project/config.rb b/lib/motion/project/config.rb index eb24ab22..dbde5a63 100644 --- a/lib/motion/project/config.rb +++ b/lib/motion/project/config.rb @@ -47,44 +47,39 @@ module Motion; module Project end end - variable :files, :xcode_dir, :sdk_version, :deployment_target, :frameworks, - :weak_frameworks, :framework_search_paths, :libs, :delegate_class, :name, :build_dir, - :resources_dirs, :specs_dir, :identifier, :codesign_certificate, - :provisioning_profile, :device_family, :interface_orientations, :version, - :short_version, :icons, :prerendered_icon, :background_modes, :seed_id, - :entitlements, :fonts, :status_bar_style, :motiondir, :detect_dependencies + variable :name, :files, :build_dir, :specs_dir, :version, :motiondir # Internal only. - attr_accessor :build_mode, :spec_mode, :distribution_mode, :dependencies + attr_accessor :build_mode, :spec_mode, :distribution_mode, :dependencies, + :template, :detect_dependencies + + ConfigTemplates = {} + + def self.register(template) + ConfigTemplates[template] = self + end + + def self.make(template, project_dir, build_mode) + klass = ConfigTemplates[template] + unless klass + $stderr.puts "Config template `#{template}' not registered" + exit 1 + end + config = klass.new(project_dir, build_mode) + config.template = template + config + end def initialize(project_dir, build_mode) @project_dir = project_dir @files = Dir.glob(File.join(project_dir, 'app/**/*.rb')) - @info_plist = {} - @dependencies = {} - @detect_dependencies = true - @frameworks = ['UIKit', 'Foundation', 'CoreGraphics'] - @weak_frameworks = [] - @framework_search_paths = [] - @libs = [] - @delegate_class = 'AppDelegate' + @build_mode = build_mode @name = 'Untitled' @resources_dirs = [File.join(project_dir, 'resources')] @build_dir = File.join(project_dir, 'build') @specs_dir = File.join(project_dir, 'spec') - @device_family = :iphone - @bundle_signature = '????' - @interface_orientations = [:portrait, :landscape_left, :landscape_right] @version = '1.0' - @short_version = '1' - @status_bar_style = :default - @background_modes = [] - @icons = [] - @prerendered_icon = false - @vendor_projects = [] - @entitlements = {} - @spec_mode = false - @build_mode = build_mode + @detect_dependencies = true end OSX_VERSION = `/usr/bin/sw_vers -productVersion`.strip.sub(/\.\d+$/, '').to_f @@ -115,40 +110,6 @@ module Motion; module Project self end - def xcode_dir - @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(< 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 - - # icons - if !(icons.is_a?(Array) and icons.all? { |x| x.is_a?(String) }) - App.fail "app.icons should be an array of strings." - end + # Do nothing, for now. end def supported_versions - @supported_versions ||= Dir.glob(File.join(motiondir, 'data/*')).select{|path| File.directory?(path)}.map do |path| + @supported_versions ||= Dir.glob(File.join(motiondir, 'data', template.to_s, '*')).select{|path| File.directory?(path)}.map do |path| File.basename path end end @@ -316,58 +245,6 @@ EOS end 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('iPhoneSimulator'), 'System', 'Library', 'Frameworks') - deps.each do |framework| - framework_path = File.join(slf, framework + '.framework', framework) - 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\/(.+)$/) and md[1] == md[2] - deps << md[1] - deps.uniq! - end - end - end - 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 ver < deployment_target || sdk_version < 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 spec_core_files @spec_core_files ||= begin # Core library + core helpers. @@ -400,345 +277,7 @@ EOS end def datadir(target=deployment_target) - File.join(motiondir, 'data', target) - 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, 'iPhoneOS.platform/Developer/SDKs/iPhoneOS*.sdk')).map do |path| - File.basename(path).scan(/iPhoneOS(.*)\.sdk/)[0][0] - end - if versions.size == 0 - App.fail "Can't find an iOS SDK in `#{platforms_dir}'" - end - supported_vers = versions.reverse.find { |vers| File.exist?(datadir(vers)) } - unless supported_vers - App.fail "RubyMotion doesn't support any of these SDK versions: #{versions.join(', ')}" - end - supported_vers - 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 locate_compiler(platform, *execs) - paths = [File.join(platform_dir(platform), 'Developer/usr/bin')] - paths.unshift File.join(xcode_dir, 'Toolchains/XcodeDefault.xctoolchain/usr/bin') - - execs.each do |exec| - paths.each do |path| - cc = File.join(path, exec) - return escape_path(cc) if File.exist?(cc) - end - end - App.fail "Can't locate compilers for platform `#{platform}'" - end - - def archs - @archs ||= begin - h = {} - %w{iPhoneSimulator iPhoneOS}.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 - 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))}\" -miphoneos-version-min=#{deployment_target} -F#{sdk(platform)}/System/Library/Frameworks" - end - - def cflags(platform, cplusplus) - "#{common_flags(platform)} -fexceptions -fblocks -fobjc-legacy-dispatch -fobjc-abi-version=2" + (cplusplus ? '' : ' -std=c99') - end - - def ldflags(platform) - ldflags = common_flags(platform) - ldflags << " -fobjc-arc" if deployment_target < '5.0' - ldflags - end - - def bundle_name - @name + (spec_mode ? '_spec' : '') - end - - def app_bundle(platform) - File.join(versionized_build_dir(platform), bundle_name + '.app') - end - - def app_bundle_dsym(platform) - File.join(versionized_build_dir(platform), bundle_name + '.dSYM') - end - - def app_bundle_executable(platform) - File.join(app_bundle(platform), name) - end - - def archive - File.join(versionized_build_dir('iPhoneOS'), bundle_name + '.ipa') - end - - def identifier - @identifier ||= "com.yourcompany.#{@name.gsub(/\s/, '')}" - spec_mode ? @identifier + '_spec' : @identifier - end - - def device_family_int(family) - case family - when :iphone then 1 - when :ipad then 2 - else - App.fail "Unknown device_family value: `#{family}'" - end - end - - def device_family_string(family, target, retina) - device = case family - when :iphone, 1 - "iPhone" - when :ipad, 2 - "iPad" - end - case retina - when 'true' - device + ((family == 1 and target >= '6.0') ? ' (Retina 4-inch)' : ' (Retina)') - when '3.5' - device + ' (Retina 3.5-inch)' - when '4' - device + ' (Retina 4-inch)' - else - device - end - end - - def device_family_ints - ary = @device_family.is_a?(Array) ? @device_family : [@device_family] - ary.map { |family| device_family_int(family) } - end - - def interface_orientations_consts - @interface_orientations.map do |ori| - case ori - when :portrait then 'UIInterfaceOrientationPortrait' - when :landscape_left then 'UIInterfaceOrientationLandscapeLeft' - when :landscape_right then 'UIInterfaceOrientationLandscapeRight' - when :portrait_upside_down then 'UIInterfaceOrientationPortraitUpsideDown' - else - App.fail "Unknown interface_orientation value: `#{ori}'" - end - end - end - - def background_modes_consts - @background_modes.map do |mode| - case mode - when :audio then 'audio' - when :location then 'location' - when :voip then 'voip' - when :newsstand_content then 'newsstand-content' - when :external_accessory then 'external-accessory' - when :bluetooth_central then 'bluetooth-central' - else - App.fail "Unknown background_modes value: `#{mode}'" - end - end - end - - def status_bar_style_const - case @status_bar_style - when :default then 'UIStatusBarStyleDefault' - when :black_translucent then 'UIStatusBarStyleBlackTranslucent' - when :black_opaque then 'UIStatusBarStyleBlackOpaque' - else - App.fail "Unknown status_bar_style value: `#{@status_bar_style}'" - end - end - - def info_plist - @info_plist - end - - def dt_info_plist -{ -} - end - - def info_plist_data - ios_version_to_build = lambda do |vers| - # XXX we should retrieve these values programmatically. - case vers - when '4.3'; '8F191m' - when '5.0'; '9A334' - when '5.1'; '9B176' - else; '10A403' # 6.0 or later - end - end - Motion::PropertyList.to_s({ - 'BuildMachineOSBuild' => `sw_vers -buildVersion`.strip, - 'MinimumOSVersion' => deployment_target, - 'CFBundleDevelopmentRegion' => 'en', - 'CFBundleName' => @name, - 'CFBundleDisplayName' => @name, - 'CFBundleExecutable' => @name, - 'CFBundleIdentifier' => identifier, - 'CFBundleInfoDictionaryVersion' => '6.0', - 'CFBundlePackageType' => 'APPL', - 'CFBundleResourceSpecification' => 'ResourceRules.plist', - 'CFBundleShortVersionString' => @short_version, - 'CFBundleSignature' => @bundle_signature, - 'CFBundleSupportedPlatforms' => ['iPhoneOS'], - 'CFBundleVersion' => @version, - 'CFBundleIconFiles' => icons, - 'CFBundleIcons' => { - 'CFBundlePrimaryIcon' => { - 'CFBundleIconFiles' => icons, - 'UIPrerenderedIcon' => prerendered_icon, - } - }, - 'UIAppFonts' => fonts, - 'UIDeviceFamily' => device_family_ints.map { |x| x.to_s }, - 'UISupportedInterfaceOrientations' => interface_orientations_consts, - 'UIStatusBarStyle' => status_bar_style_const, - 'UIBackgroundModes' => background_modes_consts, - 'DTXcode' => begin - vers = xcode_version[0].gsub(/\./, '') - if vers.length == 2 - '0' + vers + '0' - else - '0' + vers - end - end, - 'DTXcodeBuild' => xcode_version[1], - 'DTSDKName' => "iphoneos#{sdk_version}", - 'DTSDKBuild' => ios_version_to_build.call(sdk_version), - 'DTPlatformName' => 'iphoneos', - 'DTCompiler' => 'com.apple.compilers.llvm.clang.1_0', - 'DTPlatformVersion' => sdk_version, - 'DTPlatformBuild' => ios_version_to_build.call(sdk_version) - }.merge(dt_info_plist).merge(info_plist)) - end - - def pkginfo_data - "AAPL#{@bundle_signature}" - end - - def codesign_certificate - @codesign_certificate ||= begin - cert_type = (distribution_mode ? 'Distribution' : 'Developer') - certs = `/usr/bin/security -q find-certificate -a`.scan(/"iPhone #{cert_type}: [^"]+"/).uniq - if certs.size == 0 - App.fail "Can't find an iPhone Developer certificate in the keychain" - elsif certs.size > 1 - App.warn "Found #{certs.size} iPhone Developer certificates in the keychain. Set the `codesign_certificate' project setting. Will use the first certificate: `#{certs[0]}'" - end - certs[0][1..-2] # trim trailing `"` characters - end - end - - def device_id - @device_id ||= begin - deploy = File.join(App.config.bindir, 'deploy') - device_id = `#{deploy} -D`.strip - if device_id.empty? - App.fail "Can't find an iOS device connected on USB" - end - device_id - end - end - - def provisioning_profile(name = /iOS Team Provisioning Profile/) - @provisioning_profile ||= begin - paths = Dir.glob(File.expand_path("~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision")).select do |path| - text = File.read(path) - text.force_encoding('binary') if RUBY_VERSION >= '1.9.0' - text.scan(/\s*Name\s*<\/key>\s*\s*([^<]+)\s*<\/string>/)[0][0].match(name) - end - if paths.size == 0 - App.fail "Can't find a provisioning profile named `#{name}'" - elsif paths.size > 1 - App.warn "Found #{paths.size} provisioning profiles named `#{name}'. Set the `provisioning_profile' project setting. Will use the first one: `#{paths[0]}'" - end - paths[0] - end - end - - def read_provisioned_profile_array(key) - text = File.read(provisioning_profile) - text.force_encoding('binary') if RUBY_VERSION >= '1.9.0' - text.scan(/\s*#{key}\s*<\/key>\s*(.*?)\s*<\/array>/m)[0][0].scan(/(.*?)<\/string>/).map { |str| str[0].strip } - end - private :read_provisioned_profile_array - - def provisioned_devices - @provisioned_devices ||= read_provisioned_profile_array('ProvisionedDevices') - end - - def seed_id - @seed_id ||= begin - seed_ids = read_provisioned_profile_array('ApplicationIdentifierPrefix') - if seed_ids.size == 0 - App.fail "Can't find an application seed ID in the provisioning profile `#{provisioning_profile}'" - elsif seed_ids.size > 1 - App.warn "Found #{seed_ids.size} seed IDs in the provisioning profile. Set the `seed_id' project setting. Will use the last one: `#{seed_ids.last}'" - end - seed_ids.last - end - end - - def entitlements_data - dict = entitlements - if distribution_mode - dict['application-identifier'] ||= seed_id + '.' + identifier - else - # Required for gdb. - dict['get-task-allow'] = true if dict['get-task-allow'].nil? - end - Motion::PropertyList.to_s(dict) - end - - def fonts - @fonts ||= begin - resources_dirs.flatten.inject([]) do |fonts, dir| - if File.exist?(dir) - Dir.chdir(dir) do - fonts.concat(Dir.glob('*.{otf,ttf}')) - end - else - fonts - end - end - end - end - - def gen_bridge_metadata(headers, bs_file) - sdk_path = self.sdk('iPhoneSimulator') - includes = headers.map { |header| "-I'#{File.dirname(header)}'" }.uniq - a = sdk_version.scan(/(\d+)\.(\d+)/)[0] - sdk_version_headers = ((a[0].to_i * 10000) + (a[1].to_i * 100)).to_s - extra_flags = OSX_VERSION >= 10.7 ? '--no-64-bit' : '' - sh "RUBYOPT='' /usr/bin/gen_bridge_metadata --format complete #{extra_flags} --cflags \"-isysroot #{sdk_path} -miphoneos-version-min=#{sdk_version} -D__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__=#{sdk_version_headers} -I. #{includes.join(' ')}\" #{headers.map { |x| "\"#{x}\"" }.join(' ')} -o \"#{bs_file}\"" + File.join(motiondir, 'data', template.to_s, target) end end end; end diff --git a/lib/motion/project/template.rb b/lib/motion/project/template.rb new file mode 100644 index 00000000..22b02a6e --- /dev/null +++ b/lib/motion/project/template.rb @@ -0,0 +1,98 @@ +# 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 'erb' + +module Motion; module Project + class Template + # for ERB + attr_reader :name + + def initialize(app_name, template_name) + @name = @app_name = app_name + @template_name = template_name.to_s + @template_directory = File.expand_path(File.join(__FILE__, "../template/#{@template_name}")) + + unless app_name.match(/^[\w\s-]+$/) + fail "Invalid app name" + end + + if File.exist?(app_name) + fail "Directory `#{app_name}' already exists" + end + + unless File.exist?(@template_directory) + fail "Invalid template name" + end + end + + def generate + App.log 'Create', @app_name + FileUtils.mkdir(@app_name) + + Dir.chdir(@app_name) do + create_directories() + create_files() + end + end + + private + + def template_directory + @template_directory + end + + def create_directories + template_files = File.join(template_directory, 'files') + Dir.glob(File.join(template_files, "**/")).each do |dir| + dir.sub!("#{template_files}/", '') + FileUtils.mkdir_p(dir) if dir.length > 0 + end + end + + def create_files + template_files = File.join(template_directory, 'files') + Dir.glob(File.join(template_files, "**/*"), File::FNM_DOTMATCH).each do |src| + dest = src.sub("#{template_files}/", '') + next if File.directory?(src) + next if dest.include?(".DS_Store") + + dest = replace_file_name(dest) + if dest =~ /(.+)\.erb$/ + App.log 'Create', "#{@app_name}/#{$1}" + File.open($1, "w") { |io| + io.print ERB.new(File.read(src)).result(binding) + } + else + App.log 'Create', "#{@app_name}/#{dest}" + FileUtils.cp(src, dest) + end + end + end + + def replace_file_name(file_name) + file_name = file_name.sub("{name}", "#{@name}") + file_name + end + end +end; end diff --git a/lib/motion/project/template/gem/README.md.erb b/lib/motion/project/template/gem/files/README.md.erb similarity index 100% rename from lib/motion/project/template/gem/README.md.erb rename to lib/motion/project/template/gem/files/README.md.erb diff --git a/lib/motion/project/template/gem/Rakefile.erb b/lib/motion/project/template/gem/files/Rakefile.erb similarity index 100% rename from lib/motion/project/template/gem/Rakefile.erb rename to lib/motion/project/template/gem/files/Rakefile.erb diff --git a/lib/motion/project/template/gem/app/app_delegate.rb b/lib/motion/project/template/gem/files/app/app_delegate.rb similarity index 100% rename from lib/motion/project/template/gem/app/app_delegate.rb rename to lib/motion/project/template/gem/files/app/app_delegate.rb diff --git a/lib/motion/project/template/gem/lib/project/{name}.rb.erb b/lib/motion/project/template/gem/files/lib/project/{name}.rb.erb similarity index 100% rename from lib/motion/project/template/gem/lib/project/{name}.rb.erb rename to lib/motion/project/template/gem/files/lib/project/{name}.rb.erb diff --git a/lib/motion/project/template/gem/lib/{name}.rb b/lib/motion/project/template/gem/files/lib/{name}.rb similarity index 100% rename from lib/motion/project/template/gem/lib/{name}.rb rename to lib/motion/project/template/gem/files/lib/{name}.rb diff --git a/lib/motion/project/template/gem/resources/Default-568h@2x.png b/lib/motion/project/template/gem/files/resources/Default-568h@2x.png similarity index 100% rename from lib/motion/project/template/gem/resources/Default-568h@2x.png rename to lib/motion/project/template/gem/files/resources/Default-568h@2x.png diff --git a/lib/motion/project/template/gem/spec/main_spec.rb.erb b/lib/motion/project/template/gem/files/spec/main_spec.rb.erb similarity index 100% rename from lib/motion/project/template/gem/spec/main_spec.rb.erb rename to lib/motion/project/template/gem/files/spec/main_spec.rb.erb diff --git a/lib/motion/project/template/gem/{name}.gemspec.erb b/lib/motion/project/template/gem/files/{name}.gemspec.erb similarity index 100% rename from lib/motion/project/template/gem/{name}.gemspec.erb rename to lib/motion/project/template/gem/files/{name}.gemspec.erb diff --git a/lib/motion/project/template/ios.rb b/lib/motion/project/template/ios.rb new file mode 100644 index 00000000..087c4d97 --- /dev/null +++ b/lib/motion/project/template/ios.rb @@ -0,0 +1,161 @@ +# 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/app' + +App = Motion::Project::App +App.template = :ios + +require 'motion/project' +require 'motion/project/template/ios/config' + +desc "Build the project, then run the simulator" +task :default => :simulator + +desc "Build everything" +task :build => ['build:simulator', 'build:device'] + +namespace :build do + desc "Build the simulator version" + task :simulator do + App.build('iPhoneSimulator') + end + + desc "Build the device version" + task :device do + App.build('iPhoneOS') + App.codesign('iPhoneOS') + end +end + +desc "Run the simulator" +task :simulator => ['build:simulator'] do + app = App.config.app_bundle('iPhoneSimulator') + target = ENV['target'] || App.config.sdk_version + + # Cleanup the simulator application sandbox, to avoid having old resource files there. + if ENV['clean'] + sim_apps = File.expand_path("~/Library/Application Support/iPhone Simulator/#{target}/Applications") + Dir.glob("#{sim_apps}/**/*.app").each do |app_bundle| + if File.basename(app_bundle) == File.basename(app) + rm_rf File.dirname(app_bundle) + break + end + end + end + + # Prepare the device family. + family_int = + if family = ENV['device_family'] + App.config.device_family_int(family.downcase.intern) + else + App.config.device_family_ints[0] + end + retina = ENV['retina'] + + # Configure the SimulateDevice variable (the only way to specify if we want to run in retina mode or not). + simulate_device = App.config.device_family_string(family_int, target, retina) + default_simulator = `/usr/bin/defaults read com.apple.iphonesimulator "SimulateDevice"`.strip + if default_simulator != simulate_device && default_simulator != "'#{simulate_device}'" + system("/usr/bin/killall \"iPhone Simulator\" >& /dev/null") + system("/usr/bin/defaults write com.apple.iphonesimulator \"SimulateDevice\" \"'#{simulate_device}'\"") + end + + # Launch the simulator. + xcode = App.config.xcode_dir + env = "DYLD_FRAMEWORK_PATH=\"#{xcode}/../Frameworks\":\"#{xcode}/../OtherFrameworks\"" + env << ' SIM_SPEC_MODE=1' if App.config.spec_mode + sim = File.join(App.config.bindir, 'sim') + debug = (ENV['debug'] ? 1 : (App.config.spec_mode ? '0' : '2')) + App.info 'Simulate', app + at_exit { system("stty echo") } if $stdout.tty? # Just in case the simulator launcher crashes and leaves the terminal without echo. + sh "#{env} #{sim} #{debug} #{family_int} #{target} \"#{xcode}\" \"#{app}\"" +end + +desc "Create an .ipa archive" +task :archive => ['build:device'] do + App.archive +end + +namespace :archive do + desc "Create an .ipa archive for distribution (AppStore)" + task :distribution do + App.config_without_setup.build_mode = :release + App.config.distribution_mode = true + Rake::Task["build:device"].invoke + App.archive + end +end + +desc "Same as 'spec:simulator'" +task :spec => ['spec:simulator'] + +namespace :spec do + desc "Run the test/spec suite on the simulator" + task :simulator do + App.config.spec_mode = true + Rake::Task["simulator"].invoke + end + + desc "Run the test/spec suite on the device" + task :device do + App.config.spec_mode = true + ENV['debug'] ||= '1' + Rake::Task["device"].invoke + end +end + +desc "Deploy on the device" +task :device => :archive do + App.info 'Deploy', App.config.archive + device_id = (ENV['id'] or App.config.device_id) + unless App.config.provisioned_devices.include?(device_id) + App.fail "Device ID `#{device_id}' not provisioned in profile `#{App.config.provisioning_profile}'" + end + env = "XCODE_DIR=\"#{App.config.xcode_dir}\"" + deploy = File.join(App.config.bindir, 'deploy') + flags = Rake.application.options.trace ? '-d' : '' + sh "#{env} #{deploy} #{flags} \"#{device_id}\" \"#{App.config.archive}\"" +end + +desc "Generate ctags" +task :ctags do + tags_file = 'tags' + config = App.config + if !File.exist?(tags_file) or File.mtime(config.project_file) > File.mtime(tags_file) + bs_files = config.bridgesupport_files + config.vendor_projects.map { |p| Dir.glob(File.join(p.path, '*.bridgesupport')) }.flatten + ctags = File.join(config.bindir, 'ctags') + config = File.join(config.motiondir, 'data', 'bridgesupport-ctags.cfg') + sh "#{ctags} --options=\"#{config}\" #{bs_files.map { |x| '"' + x + '"' }.join(' ')}" + end +end + +desc "Create a .a static library" +task :static do + libs = %w{iPhoneSimulator iPhoneOS}.map do |platform| + '"' + App.build(platform, :static => true) + '"' + end + fat_lib = File.join(App.config.build_dir, App.config.name + '-universal.a') + App.info 'Create', fat_lib + sh "/usr/bin/lipo -create #{libs.join(' ')} -output \"#{fat_lib}\"" +end diff --git a/lib/motion/project/template/ios/config.rb b/lib/motion/project/template/ios/config.rb new file mode 100644 index 00000000..04b4d187 --- /dev/null +++ b/lib/motion/project/template/ios/config.rb @@ -0,0 +1,303 @@ +# 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/xcode_config' + +module Motion; module Project; + class IOSConfig < XcodeConfig + register :ios + + variable :device_family, :interface_orientations, :background_modes, + :status_bar_style + + def initialize(project_dir, build_mode) + super + @frameworks = ['UIKit', 'Foundation', 'CoreGraphics'] + @device_family = :iphone + @interface_orientations = [:portrait, :landscape_left, :landscape_right] + @background_modes = [] + @status_bar_style = :default + end + + def platforms; ['iPhoneSimulator', 'iPhoneOS']; end + def local_platform; 'iPhoneSimulator'; end + def deploy_platform; 'iPhoneOS'; end + + def locate_compiler(platform, *execs) + paths = [File.join(platform_dir(platform), 'Developer/usr/bin')] + paths.unshift File.join(xcode_dir, 'Toolchains/XcodeDefault.xctoolchain/usr/bin') + + execs.each do |exec| + paths.each do |path| + cc = File.join(path, exec) + return escape_path(cc) if File.exist?(cc) + end + end + App.fail "Can't locate compilers for platform `#{platform}'" + end + + def common_flags(platform) + super + " -miphoneos-version-min=#{deployment_target}" + end + + def device_family_int(family) + case family + when :iphone then 1 + when :ipad then 2 + else + App.fail "Unknown device_family value: `#{family}'" + end + end + + def device_family_string(family, target, retina) + device = case family + when :iphone, 1 + "iPhone" + when :ipad, 2 + "iPad" + end + case retina + when 'true' + device + ((family == 1 and target >= '6.0') ? ' (Retina 4-inch)' : ' (Retina)') + when '3.5' + device + ' (Retina 3.5-inch)' + when '4' + device + ' (Retina 4-inch)' + else + device + end + end + + def device_family_ints + ary = @device_family.is_a?(Array) ? @device_family : [@device_family] + ary.map { |family| device_family_int(family) } + end + + def interface_orientations_consts + @interface_orientations.map do |ori| + case ori + when :portrait then 'UIInterfaceOrientationPortrait' + when :landscape_left then 'UIInterfaceOrientationLandscapeLeft' + when :landscape_right then 'UIInterfaceOrientationLandscapeRight' + when :portrait_upside_down then 'UIInterfaceOrientationPortraitUpsideDown' + else + App.fail "Unknown interface_orientation value: `#{ori}'" + end + end + end + + def background_modes_consts + @background_modes.map do |mode| + case mode + when :audio then 'audio' + when :location then 'location' + when :voip then 'voip' + when :newsstand_content then 'newsstand-content' + when :external_accessory then 'external-accessory' + when :bluetooth_central then 'bluetooth-central' + else + App.fail "Unknown background_modes value: `#{mode}'" + end + end + end + + def status_bar_style_const + case @status_bar_style + when :default then 'UIStatusBarStyleDefault' + when :black_translucent then 'UIStatusBarStyleBlackTranslucent' + when :black_opaque then 'UIStatusBarStyleBlackOpaque' + else + App.fail "Unknown status_bar_style value: `#{@status_bar_style}'" + end + end + + def device_id + @device_id ||= begin + deploy = File.join(App.config.bindir, 'deploy') + device_id = `#{deploy} -D`.strip + if device_id.empty? + App.fail "Can't find an iOS device connected on USB" + end + device_id + end + end + + def app_bundle(platform) + File.join(versionized_build_dir(platform), bundle_name + '.app') + end + + def app_bundle_executable(platform) + File.join(app_bundle(platform), name) + end + + def app_resources_dir(platform) + app_bundle(platform) + end + + def info_plist_data + ios_version_to_build = lambda do |vers| + # XXX we should retrieve these values programmatically. + case vers + when '4.3'; '8F191m' + when '5.0'; '9A334' + when '5.1'; '9B176' + else; '10A403' # 6.0 or later + end + end + Motion::PropertyList.to_s({ + 'MinimumOSVersion' => deployment_target, + 'CFBundleResourceSpecification' => 'ResourceRules.plist', + 'CFBundleSupportedPlatforms' => [deploy_platform], + 'CFBundleIconFiles' => icons, + 'CFBundleIcons' => { + 'CFBundlePrimaryIcon' => { + 'CFBundleIconFiles' => icons, + 'UIPrerenderedIcon' => prerendered_icon, + } + }, + 'UIAppFonts' => fonts, + 'UIDeviceFamily' => device_family_ints.map { |x| x.to_s }, + 'UISupportedInterfaceOrientations' => interface_orientations_consts, + 'UIStatusBarStyle' => status_bar_style_const, + 'UIBackgroundModes' => background_modes_consts, + 'DTXcode' => begin + vers = xcode_version[0].gsub(/\./, '') + if vers.length == 2 + '0' + vers + '0' + else + '0' + vers + end + end, + 'DTXcodeBuild' => xcode_version[1], + 'DTSDKName' => "iphoneos#{sdk_version}", + 'DTSDKBuild' => ios_version_to_build.call(sdk_version), + 'DTPlatformName' => 'iphoneos', + 'DTCompiler' => 'com.apple.compilers.llvm.clang.1_0', + 'DTPlatformVersion' => sdk_version, + 'DTPlatformBuild' => ios_version_to_build.call(sdk_version) + }.merge(generic_info_plist).merge(dt_info_plist).merge(info_plist)) + end + + def main_cpp_file_txt(spec_objs) + main_txt = < + +extern "C" { + void rb_define_global_const(const char *, void *); + void rb_rb2oc_exc_handler(void); + void rb_exit(int); + void RubyMotionInit(int argc, char **argv); +EOS + if spec_mode + spec_objs.each do |_, init_func| + main_txt << "void #{init_func}(void *, void *);\n" + end + end + main_txt << < + +@implementation SpecLauncher + ++ (id)launcher +{ + [UIApplication sharedApplication]; + + // Enable simulator accessibility. + // Thanks http://www.stewgleadow.com/blog/2011/10/14/enabling-accessibility-for-ios-applications/ + NSString *simulatorRoot = [[[NSProcessInfo processInfo] environment] objectForKey:@"IPHONE_SIMULATOR_ROOT"]; + if (simulatorRoot != nil) { + void *appSupportLibrary = dlopen([[simulatorRoot stringByAppendingPathComponent:@"/System/Library/PrivateFrameworks/AppSupport.framework/AppSupport"] fileSystemRepresentation], RTLD_LAZY); + CFStringRef (*copySharedResourcesPreferencesDomainForDomain)(CFStringRef domain) = (CFStringRef (*)(CFStringRef)) dlsym(appSupportLibrary, "CPCopySharedResourcesPreferencesDomainForDomain"); + + if (copySharedResourcesPreferencesDomainForDomain != NULL) { + CFStringRef accessibilityDomain = copySharedResourcesPreferencesDomainForDomain(CFSTR("com.apple.Accessibility")); + + if (accessibilityDomain != NULL) { + CFPreferencesSetValue(CFSTR("ApplicationAccessibilityEnabled"), kCFBooleanTrue, accessibilityDomain, kCFPreferencesAnyUser, kCFPreferencesAnyHost); + CFRelease(accessibilityDomain); + } + } + } + + // Load the UIAutomation framework. + dlopen("/Developer/Library/PrivateFrameworks/UIAutomation.framework/UIAutomation", RTLD_LOCAL); + + SpecLauncher *launcher = [[self alloc] init]; + [[NSNotificationCenter defaultCenter] addObserver:launcher selector:@selector(appLaunched:) name:UIApplicationDidBecomeActiveNotification object:nil]; + return launcher; +} + +- (void)appLaunched:(id)notification +{ + // Give a bit of time for the simulator to attach... + [self performSelector:@selector(runSpecs) withObject:nil afterDelay:0.3]; +} + +- (void)runSpecs +{ +EOS + spec_objs.each do |_, init_func| + main_txt << "#{init_func}(self, 0);\n" + end + main_txt << "[NSClassFromString(@\"Bacon\") performSelector:@selector(run)];\n" + main_txt << < :run + +desc "Build the project" +task :build do + App.build('MacOSX') +end + +desc "Run the project" +task :run => 'build' do + exec = App.config.app_bundle_executable('MacOSX') + App.info 'Run', exec + sh exec +end diff --git a/lib/motion/project/template/osx/config.rb b/lib/motion/project/template/osx/config.rb new file mode 100644 index 00000000..a8e18f64 --- /dev/null +++ b/lib/motion/project/template/osx/config.rb @@ -0,0 +1,111 @@ +# 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/xcode_config' + +module Motion; module Project; + class OSXConfig < XcodeConfig + register :osx + + variable :copyright + + def initialize(project_dir, build_mode) + super + @copyright = "Copyright © #{Time.now.year} #{`whoami`.strip}. All rights reserved." + @frameworks = ['AppKit', 'Foundation', 'CoreGraphics'] + end + + def platforms; ['MacOSX']; end + def local_platform; 'MacOSX'; end + def deploy_platform; 'MacOSX'; end + + def locate_compiler(platform, *execs) + execs.each do |exec| + cc = File.join('/usr/bin', exec) + return escape_path(cc) if File.exist?(cc) + end + App.fail "Can't locate compilers for platform `#{platform}'" + end + + def archs; { 'MacOSX' => ['x86_64'] }; end + + def common_flags(platform) + super + " -mmacosx-version-min=#{deployment_target}" + end + + def app_bundle(platform) + File.join(versionized_build_dir(platform), bundle_name + '.app', 'Contents') + end + + def app_bundle_executable(platform) + File.join(app_bundle(platform), 'MacOS', name) + end + + def app_resources_dir(platform) + File.join(app_bundle(platform), 'Resources') + end + + def info_plist_data + Motion::PropertyList.to_s({ + 'NSHumanReadableCopyright' => copyright, + 'NSPrincipalClass' => 'NSApplication' + }.merge(generic_info_plist).merge(dt_info_plist).merge(info_plist)) + end + + def main_cpp_file_txt(spec_objs) + main_txt = < + +extern "C" { + void rb_define_global_const(const char *, void *); + void rb_rb2oc_exc_handler(void); + void rb_exit(int); + void RubyMotionInit(int argc, char **argv); +} + +int +main(int argc, char **argv) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; +#if !__LP64__ + try { +#endif + RubyMotionInit(argc, argv); + #{define_global_env_txt} + NSApplication *app = [NSApplication sharedApplication]; + [app setDelegate:[NSClassFromString(@"#{delegate_class}") new]]; + [app run]; + rb_exit(0); +#if !__LP64__ + } + catch (...) { + rb_rb2oc_exc_handler(); + } +#endif + [pool release]; + return 0; +} +EOS + end + end +end; end diff --git a/lib/motion/project/template/osx/files/.gitignore b/lib/motion/project/template/osx/files/.gitignore new file mode 100644 index 00000000..c3576622 --- /dev/null +++ b/lib/motion/project/template/osx/files/.gitignore @@ -0,0 +1,16 @@ +.repl_history +build +tags +app/pixate_code.rb +resources/*.nib +resources/*.momd +resources/*.storyboardc +.DS_Store +nbproject +.redcar +#*# +*~ +*.sw[po] +.eprj +.sass-cache +.idea diff --git a/lib/motion/project/template/osx/files/Rakefile.erb b/lib/motion/project/template/osx/files/Rakefile.erb new file mode 100644 index 00000000..84b39a5c --- /dev/null +++ b/lib/motion/project/template/osx/files/Rakefile.erb @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +$:.unshift("/Library/RubyMotion/lib") +require 'motion/project/template/osx' + +Motion::Project::App.setup do |app| + # Use `rake config' to see complete project settings. + app.name = '<%= name %>' +end diff --git a/lib/motion/project/template/osx/files/app/app_delegate.rb b/lib/motion/project/template/osx/files/app/app_delegate.rb new file mode 100644 index 00000000..c2344255 --- /dev/null +++ b/lib/motion/project/template/osx/files/app/app_delegate.rb @@ -0,0 +1,4 @@ +class AppDelegate + def applicationDidFinishLaunching(notification) + end +end diff --git a/lib/motion/project/template/osx/files/resources/Default-568h@2x.png b/lib/motion/project/template/osx/files/resources/Default-568h@2x.png new file mode 100644 index 00000000..0f9e292b Binary files /dev/null and b/lib/motion/project/template/osx/files/resources/Default-568h@2x.png differ diff --git a/lib/motion/project/template/osx/files/spec/main_spec.rb.erb b/lib/motion/project/template/osx/files/spec/main_spec.rb.erb new file mode 100644 index 00000000..a638776e --- /dev/null +++ b/lib/motion/project/template/osx/files/spec/main_spec.rb.erb @@ -0,0 +1,9 @@ +describe "Application '<%= name %>'" do + before do + @app = UIApplication.sharedApplication + end + + it "has one window" do + @app.windows.size.should == 1 + end +end diff --git a/lib/motion/project/xcode_config.rb b/lib/motion/project/xcode_config.rb new file mode 100644 index 00000000..3d7d5c30 --- /dev/null +++ b/lib/motion/project/xcode_config.rb @@ -0,0 +1,383 @@ +# 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. + +module Motion; module Project; + class XcodeConfig < Config + variable :xcode_dir, :sdk_version, :deployment_target, :frameworks, + :weak_frameworks, :framework_search_paths, :libs, :resources_dirs, + :identifier, :codesign_certificate, :provisioning_profile, + :short_version, :icons, :prerendered_icon, :seed_id, :entitlements, + :fonts, :delegate_class + + def initialize(project_dir, build_mode) + super + @info_plist = {} + @dependencies = {} + @frameworks = [] + @weak_frameworks = [] + @framework_search_paths = [] + @libs = [] + @bundle_signature = '????' + @short_version = '1' + @icons = [] + @prerendered_icon = false + @vendor_projects = [] + @entitlements = {} + @delegate_class = 'AppDelegate' + @spec_mode = false + end + + def xcode_dir + @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(< 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 + + # icons + if !(icons.is_a?(Array) and icons.all? { |x| x.is_a?(String) }) + App.fail "app.icons should be an array of strings." + 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}*.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_vers = versions.reverse.find { |vers| File.exist?(datadir(vers)) } + unless supported_vers + App.fail "RubyMotion doesn't support any of these SDK versions: #{versions.join(', ')}" + end + supported_vers + 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') + deps.each do |framework| + framework_path = File.join(slf, framework + '.framework', framework) + 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\/(.+)$/) and md[1] == md[2] + deps << md[1] + deps.uniq! + end + end + end + 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 ver < deployment_target || sdk_version < 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 archs + @archs ||= begin + 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 + 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) + "#{common_flags(platform)} -fexceptions -fblocks -fobjc-legacy-dispatch -fobjc-abi-version=2" + (cplusplus ? '' : ' -std=c99') + end + + def ldflags(platform) + ldflags = common_flags(platform) + ldflags << " -fobjc-arc" if deployment_target < '5.0' + ldflags + end + + def bundle_name + @name + (spec_mode ? '_spec' : '') + end + + def app_bundle_dsym(platform) + File.join(versionized_build_dir(platform), bundle_name + '.dSYM') + end + + def archive + File.join(versionized_build_dir(deploy_platform), bundle_name + '.ipa') + 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, + 'CFBundleSignature' => @bundle_signature, + 'CFBundleVersion' => @version + } + end + + def pkginfo_data + "AAPL#{@bundle_signature}" + end + + def codesign_certificate + @codesign_certificate ||= begin + cert_type = (distribution_mode ? 'Distribution' : 'Developer') + certs = `/usr/bin/security -q find-certificate -a`.scan(/"iPhone #{cert_type}: [^"]+"/).uniq + if certs.size == 0 + App.fail "Can't find an iPhone Developer certificate in the keychain" + elsif certs.size > 1 + App.warn "Found #{certs.size} iPhone Developer certificates in the keychain. Set the `codesign_certificate' project setting. Will use the first certificate: `#{certs[0]}'" + end + certs[0][1..-2] # trim trailing `"` characters + end + end + + def provisioning_profile(name = /iOS Team Provisioning Profile/) + @provisioning_profile ||= begin + paths = Dir.glob(File.expand_path("~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision")).select do |path| + text = File.read(path) + text.force_encoding('binary') if RUBY_VERSION >= '1.9.0' + text.scan(/\s*Name\s*<\/key>\s*\s*([^<]+)\s*<\/string>/)[0][0].match(name) + end + if paths.size == 0 + App.fail "Can't find a provisioning profile named `#{name}'" + elsif paths.size > 1 + App.warn "Found #{paths.size} provisioning profiles named `#{name}'. Set the `provisioning_profile' project setting. Will use the first one: `#{paths[0]}'" + end + paths[0] + end + end + + def read_provisioned_profile_array(key) + text = File.read(provisioning_profile) + text.force_encoding('binary') if RUBY_VERSION >= '1.9.0' + text.scan(/\s*#{key}\s*<\/key>\s*(.*?)\s*<\/array>/m)[0][0].scan(/(.*?)<\/string>/).map { |str| str[0].strip } + end + private :read_provisioned_profile_array + + def provisioned_devices + @provisioned_devices ||= read_provisioned_profile_array('ProvisionedDevices') + end + + def seed_id + @seed_id ||= begin + seed_ids = read_provisioned_profile_array('ApplicationIdentifierPrefix') + if seed_ids.size == 0 + App.fail "Can't find an application seed ID in the provisioning profile `#{provisioning_profile}'" + elsif seed_ids.size > 1 + App.warn "Found #{seed_ids.size} seed IDs in the provisioning profile. Set the `seed_id' project setting. Will use the last one: `#{seed_ids.last}'" + end + seed_ids.last + end + end + + def entitlements_data + dict = entitlements + if distribution_mode + dict['application-identifier'] ||= seed_id + '.' + identifier + else + # Required for gdb. + dict['get-task-allow'] = true if dict['get-task-allow'].nil? + end + Motion::PropertyList.to_s(dict) + end + + def fonts + @fonts ||= begin + resources_dirs.flatten.inject([]) do |fonts, dir| + if File.exist?(dir) + Dir.chdir(dir) do + fonts.concat(Dir.glob('*.{otf,ttf}')) + end + else + fonts + end + end + end + end + + def gen_bridge_metadata(headers, bs_file) + sdk_path = self.sdk(local_platform) + includes = headers.map { |header| "-I'#{File.dirname(header)}'" }.uniq + a = sdk_version.scan(/(\d+)\.(\d+)/)[0] + sdk_version_headers = ((a[0].to_i * 10000) + (a[1].to_i * 100)).to_s + extra_flags = OSX_VERSION >= 10.7 ? '--no-64-bit' : '' + sh "RUBYOPT='' /usr/bin/gen_bridge_metadata --format complete #{extra_flags} --cflags \"-isysroot #{sdk_path} -miphoneos-version-min=#{sdk_version} -D__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__=#{sdk_version_headers} -I. #{includes.join(' ')}\" #{headers.map { |x| "\"#{x}\"" }.join(' ')} -o \"#{bs_file}\"" + end + + def define_global_env_txt + rubymotion_env = + if spec_mode + 'test' + else + development? ? 'development' : 'release' + end + "rb_define_global_const(\"RUBYMOTION_ENV\", @\"#{rubymotion_env}\");\nrb_define_global_const(\"RUBYMOTION_VERSION\", @\"#{Motion::Version}\");\n" + end + end +end; end