commit c14c4326023f5bfec19439619cafff5db4d97b67 Author: Laurent Sansonetti Date: Fri Jun 1 19:26:29 2012 +0200 initial revision diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..09efb655 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.swp +*.orig +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..12d85fba --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +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. diff --git a/README.rdoc b/README.rdoc new file mode 100644 index 00000000..b6babda8 --- /dev/null +++ b/README.rdoc @@ -0,0 +1,53 @@ += RubyMotion + +RubyMotion is a commercial toolchain for iOS development using the Ruby +programming language. + +This repository contains the parts of the RubyMotion product that are +opensource. It does not contain the full product, which can be purchased at +http://www.rubymotion.com. + +== Contents + +The +lib+ directory contains the project Rakefile library and the build system. + +These files are installed as +/Library/RubyMotion/lib+. + +In order to use the GitHub version of these files in your RubyMotion project, +you can modify the project Rakefile like this. + + $:.unshift(ENV['RUBYMOTION_LIB'] || "/Library/RubyMotion/lib") + +Then, simply set the RUBYMOTION_LIB environment variable when using +rake+. + + $ cd MyProject + $ RUBYMOTION_LIB=~/src/RubyMotion/lib rake + +== Contributions + +Please use the GitHub pull-request mechanism to submit contributions. + +== License + + 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. diff --git a/lib/motion/project.rb b/lib/motion/project.rb new file mode 100644 index 00000000..2c781e3b --- /dev/null +++ b/lib/motion/project.rb @@ -0,0 +1,202 @@ +# 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/version' +require 'motion/project/app' +require 'motion/project/config' +require 'motion/project/builder' +require 'motion/project/vendor' +require 'motion/project/plist' + +App = Motion::Project::App + +# Check for software updates. +system('/usr/bin/motion update --check') +if $?.exitstatus == 2 + puts '=' * 80 + puts " A new version of RubyMotion is available. Run `sudo motion update' to upgrade." + puts '=' * 80 + 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 = App.config.deployment_target + + # 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'] == 'true' + + # Configure the SimulateDevice variable (the only way to specify if we want to run in retina mode or not). + sh "/usr/bin/defaults write com.apple.iphonesimulator \"SimulateDevice\" \"'#{App.config.device_family_string(family_int, retina)}'\"" + + # Launch the simulator. + xcode = App.config.xcode_dir + env = xcode.match(/^\/Applications/) ? "DYLD_FRAMEWORK_PATH=\"#{xcode}/../Frameworks\":\"#{xcode}/../OtherFrameworks\"" : '' + env << ' NO_FOREGROUND_SIM=1' if App.config.spec_mode + sim = File.join(App.config.bindir, 'sim') + debug = (ENV['debug'] || (App.config.spec_mode ? '0' : '2')).to_i + debug = 2 if debug < 0 or debug > 2 + App.info 'Simulate', app + at_exit { system("stty echo") } # 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 archives for everything" +task :archive => ['archive:development', 'archive:release'] + +def create_ipa + app_bundle = App.config.app_bundle('iPhoneOS') + archive = App.config.archive + if !File.exist?(archive) or File.mtime(app_bundle) > File.mtime(archive) + App.info 'Create', archive + tmp = "/tmp/ipa_root" + sh "/bin/rm -rf #{tmp}" + sh "/bin/mkdir -p #{tmp}/Payload" + sh "/bin/cp -r \"#{app_bundle}\" #{tmp}/Payload" + Dir.chdir(tmp) do + sh "/bin/chmod -R 755 Payload" + sh "/usr/bin/zip -q -r archive.zip Payload" + end + sh "/bin/cp #{tmp}/archive.zip \"#{archive}\"" + end +end + +namespace :archive do + desc "Create an .ipa archive for development" + task :development do + App.config_mode = :development + Rake::Task["build:device"].execute + App.archive + end + + desc "Create an .ipa for release (AppStore)" + task :release do + App.config_mode = :release + Rake::Task["build:device"].execute + App.archive + end +end + +desc "Run specs" +task :spec do + App.config.spec_mode = true + Rake::Task["simulator"].invoke +end + +desc "Deploy on the device" +task :device => 'archive:development' do + App.info 'Deploy', App.config.archive + unless App.config.provisioned_devices.include?(App.config.device_id) + App.fail "Connected device ID `#{App.config.device_id}' not provisioned in profile `#{App.config.provisioning_profile}'" + end + deploy = File.join(App.config.bindir, 'deploy') + flags = Rake.application.options.trace ? '-d' : '' + sh "#{deploy} #{flags} \"#{App.config.device_id}\" \"#{App.config.archive}\"" +end + +desc "Clear build objects" +task :clean do + App.info 'Delete', App.config.build_dir + rm_rf(App.config.build_dir) + App.config.vendor_projects.each { |vendor| vendor.clean } + Dir.glob(App.config.resources_dir + '/**/*.{nib,storyboardc,momd}').each do |p| + App.info 'Delete', p + rm_rf p + end +end + +desc "Show project config" +task :config do + map = App.config.variables + map.keys.sort.each do |key| + 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 + +=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 new file mode 100644 index 00000000..10b7e39b --- /dev/null +++ b/lib/motion/project/app.rb @@ -0,0 +1,168 @@ +# 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 App + VERBOSE = + begin + if Rake.send(:verbose) != true + Rake.send(:verbose, false) + false + else + true + end + rescue + true + end + + class << self + def config_mode + @config_mode or :development + end + + def config_mode=(mode) + @config_mode = mode + end + + def configs + @configs ||= { + :development => Motion::Project::Config.new('.', :development), + :release => Motion::Project::Config.new('.', :release) + } + end + + def config + configs[config_mode] + end + + def builder + @builder ||= Motion::Project::Builder.new + end + + def setup + configs.each_value { |x| yield x } + config.validate + end + + def build(platform) + builder.build(config, platform) + end + + def archive + builder.archive(config) + end + + def codesign(platform) + builder.codesign(config, platform) + end + + def create(app_name) + unless app_name.match(/^[a-zA-Z\d\s]+$/) + fail "Invalid app name" + end + + if File.exist?(app_name) + fail "Directory `#{app_name}' already exists" + end + + App.log 'Create', app_name + Dir.mkdir(app_name) + Dir.chdir(app_name) do + App.log 'Create', File.join(app_name, '.gitignore') + File.open('.gitignore', 'w') do |io| + io.puts ".repl_history" + io.puts "build" + io.puts "resources/*.nib" + io.puts "resources/*.momd" + io.puts "resources/*.storyboardc" + end + App.log 'Create', File.join(app_name, 'Rakefile') + File.open('Rakefile', 'w') do |io| + io.puts < File.mtime(objs_build_dir) + build_file = Proc.new do |path| + obj ||= File.join(objs_build_dir, "#{path}.o") + should_rebuild = (project_file_changed \ + or !File.exist?(obj) \ + or File.mtime(path) > File.mtime(obj) \ + or File.mtime(ruby) > File.mtime(obj)) + + # Generate or retrieve init function. + init_func = should_rebuild ? "MREP_#{`/usr/bin/uuidgen`.strip.gsub('-', '')}" : `#{config.locate_binary('nm')} #{obj}`.scan(/T\s+_(MREP_.*)/)[0][0] + + if should_rebuild + App.info 'Compile', path + FileUtils.mkdir_p(File.dirname(obj)) + arch_objs = [] + archs.each do |arch| + # Locate arch kernel. + kernel = File.join(datadir, platform, "kernel-#{arch}.bc") + raise "Can't locate kernel file" unless File.exist?(kernel) + + # 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}\" #{ruby} #{bs_flags} --emit-llvm \"#{bc}\" #{init_func} \"#{path}\"" + + # Assembly. + asm = File.join(objs_build_dir, "#{path}.#{arch}.s") + llc_arch = case arch + when 'i386'; 'x86' + when 'x86_64'; 'x86-64' + when /^arm/; 'arm' + else; arch + end + sh "#{llc} \"#{bc}\" -o=\"#{asm}\" -march=#{llc_arch} -relocation-model=pic -disable-fp-elim -jit-enable-eh -disable-cfi" + + # Object. + arch_obj = File.join(objs_build_dir, "#{path}.#{arch}.o") + sh "#{cc} -fexceptions -c -arch #{arch} \"#{asm}\" -o \"#{arch_obj}\"" + + [bc, asm].each { |x| File.unlink(x) } + arch_objs << arch_obj + end + + # Assemble fat binary. + arch_objs_list = arch_objs.map { |x| "\"#{x}\"" }.join(' ') + sh "/usr/bin/lipo -create #{arch_objs_list} -output \"#{obj}\"" + end + + [obj, init_func] + end + + # Create builders. + builders_count = + if jobs = ENV['jobs'] + jobs.to_i + else + `/usr/sbin/sysctl -n machdep.cpu.thread_count`.strip.to_i + end + builders_count = 1 if builders_count < 1 + builders = [] + builders_count.times do + queue = [] + th = Thread.new do + sleep + objs = [] + while path = queue.shift + objs << build_file.call(path) + end + queue.concat(objs) + end + builders << [queue, th] + end + + # Feed builders with work. + builder_i = 0 + config.ordered_build_files.each do |path| + builders[builder_i][0] << path + builder_i += 1 + builder_i = 0 if builder_i == builders_count + end + + # Start build. + builders.each do |queue, th| + sleep 0.01 while th.status != 'sleep' + th.wakeup + end + builders.each { |queue, th| th.join } + + # Merge the result (based on build order). + objs = [] + builder_i = 0 + config.ordered_build_files.each do |path| + objs << builders[builder_i][0].shift + builder_i += 1 + builder_i = 0 if builder_i == builders_count + end + + app_objs = objs + if config.spec_mode + # Build spec files too, but sequentially. + objs << build_file.call(File.expand_path(File.join(File.dirname(__FILE__), '../spec.rb'))) + spec_objs = config.spec_files.map { |path| build_file.call(path) } + objs += spec_objs + end + + # Generate main file. + main_txt = < + +extern "C" { + void ruby_sysinit(int *, char ***); + void ruby_init(void); + void ruby_init_loadpath(void); + void ruby_script(const char *); + void ruby_set_argv(int, char **); + void rb_vm_init_compiler(void); + void rb_vm_init_jit(void); + void rb_vm_aot_feature_provide(const char *, void *); + void *rb_vm_top_self(void); + void rb_rb2oc_exc_handler(void); + void rb_exit(int); +EOS + objs.each do |_, init_func| + main_txt << "void #{init_func}(void *, void *);\n" + end + main_txt << < File.mtime(main_exec) \ + or objs.any? { |path, _| File.mtime(path) > File.mtime(main_exec) } \ + or File.mtime(main_o) > File.mtime(main_exec) \ + or File.mtime(File.join(datadir, platform, 'libmacruby-static.a')) > File.mtime(main_exec) + App.info 'Link', main_exec + objs_list = objs.map { |path, _| path }.unshift(main_o).map { |x| "\"#{x}\"" }.join(' ') + frameworks = config.frameworks_dependencies.map { |x| "-framework #{x}" }.join(' ') + framework_stubs_objs = [] + config.frameworks_dependencies.each do |framework| + stubs_obj = File.join(datadir, platform, "#{framework}_stubs.o") + framework_stubs_objs << "\"#{stubs_obj}\"" if File.exist?(stubs_obj) + end + sh "#{cxx} -o \"#{main_exec}\" #{objs_list} #{framework_stubs_objs.join(' ')} #{config.ldflags(platform)} -L#{File.join(datadir, platform)} -lmacruby-static -lobjc -licucore #{frameworks} #{config.libs.join(' ')} #{vendor_libs.map { |x| '-force_load "' + x + '"' }.join(' ')}" + main_exec_created = true + end + + # Create bundle/Info.plist. + bundle_info_plist = File.join(bundle_path, 'Info.plist') + if !File.exist?(bundle_info_plist) or File.mtime(config.project_file) > File.mtime(bundle_info_plist) + App.info 'Create', bundle_info_plist + File.open(bundle_info_plist, 'w') { |io| io.write(config.info_plist_data) } + sh "/usr/bin/plutil -convert binary1 \"#{bundle_info_plist}\"" + end + + # Create bundle/PkgInfo. + bundle_pkginfo = File.join(bundle_path, 'PkgInfo') + if !File.exist?(bundle_pkginfo) or File.mtime(config.project_file) > File.mtime(bundle_pkginfo) + App.info 'Create', bundle_pkginfo + File.open(bundle_pkginfo, 'w') { |io| io.write(config.pkginfo_data) } + end + + # Compile IB resources. + if File.exist?(config.resources_dir) + ib_resources = [] + ib_resources.concat((Dir.glob(File.join(config.resources_dir, '*.xib')) + Dir.glob(File.join(config.resources_dir, '*.lproj', '*.xib'))).map { |xib| [xib, xib.sub(/\.xib$/, '.nib')] }) + ib_resources.concat(Dir.glob(File.join(config.resources_dir, '*.storyboard')).map { |storyboard| [storyboard, storyboard.sub(/\.storyboard$/, '.storyboardc')] }) + ib_resources.each do |src, dest| + if !File.exist?(dest) or File.mtime(src) > File.mtime(dest) + App.info 'Compile', src + sh "/usr/bin/ibtool --compile \"#{dest}\" \"#{src}\"" + end + end + end + + # Compile CoreData Model resources. + if File.exist?(config.resources_dir) + Dir.glob(File.join(config.resources_dir, '*.xcdatamodeld')).each do |model| + momd = model.sub(/\.xcdatamodeld$/, '.momd') + if !File.exist?(momd) or File.mtime(model) > File.mtime(momd) + App.info 'Compile', model + model = File.expand_path(model) # momc wants absolute paths. + momd = File.expand_path(momd) + sh "\"#{App.config.xcode_dir}/usr/bin/momc\" \"#{model}\" \"#{momd}\"" + end + end + end + + # Copy resources, handle subdirectories. + reserved_app_bundle_files = [ + '_CodeSignature/CodeResources', 'CodeResources', 'embedded.mobileprovision', + 'Info.plist', 'PkgInfo', 'ResourceRules.plist', + config.name + ] + resources_files = [] + if File.exist?(config.resources_dir) + resources_files = Dir.chdir(config.resources_dir) do + Dir.glob('**/*').reject { |x| ['.xib', '.storyboard', '.xcdatamodeld', '.lproj'].include?(File.extname(x)) } + end + resources_files.each do |res| + res_path = File.join(config.resources_dir, res) + 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) + 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 + FileUtils.cp_r(res_path, File.dirname(dest_path)) + end + end + end + + # Delete old resource files. + Dir.chdir(bundle_path) do + Dir.glob('**/*').each do |bundle_res| + next if File.directory?(bundle_res) + next if reserved_app_bundle_files.include?(bundle_res) + next if resources_files.include?(bundle_res) + App.warn "File `#{bundle_res}' found in app bundle but not in `#{config.resources_dir}', removing" + FileUtils.rm_rf(bundle_res) + end + end + + # Generate dSYM. + dsym_path = config.app_bundle_dsym(platform) + if !File.exist?(dsym_path) or File.mtime(main_exec) > File.mtime(dsym_path) + App.info "Create", dsym_path + sh "/usr/bin/dsymutil \"#{main_exec}\" -o \"#{dsym_path}\"" + end + + # Strip all symbols. Only in release mode. + if main_exec_created and config.release? + App.info "Strip", main_exec + sh "#{config.locate_binary('strip')} \"#{main_exec}\"" + end + end + + def codesign(config, platform) + bundle_path = config.app_bundle(platform) + raise unless File.exist?(bundle_path) + + # Create bundle/ResourceRules.plist. + resource_rules_plist = File.join(bundle_path, 'ResourceRules.plist') + unless File.exist?(resource_rules_plist) + App.info 'Create', resource_rules_plist + File.open(resource_rules_plist, 'w') do |io| + io.write(<<-PLIST) + + + + + rules + + .* + + Info.plist + + omit + + weight + 10 + + ResourceRules.plist + + omit + + weight + 100 + + + + +PLIST + end + end + + # Copy the provisioning profile. + bundle_provision = File.join(bundle_path, "embedded.mobileprovision") + if !File.exist?(bundle_provision) or File.mtime(config.provisioning_profile) > File.mtime(bundle_provision) + App.info 'Create', bundle_provision + FileUtils.cp config.provisioning_profile, bundle_provision + end + + # Codesign. + codesign_cmd = "CODESIGN_ALLOCATE=\"#{File.join(config.platform_dir(platform), 'Developer/usr/bin/codesign_allocate')}\" /usr/bin/codesign" + if File.mtime(config.project_file) > File.mtime(bundle_path) \ + or !system("#{codesign_cmd} --verify \"#{bundle_path}\" >& /dev/null") + App.info 'Codesign', bundle_path + entitlements = File.join(config.versionized_build_dir(platform), "Entitlements.plist") + File.open(entitlements, 'w') { |io| io.write(config.entitlements_data) } + sh "#{codesign_cmd} -f -s \"#{config.codesign_certificate}\" --resource-rules=\"#{resource_rules_plist}\" --entitlements #{entitlements} \"#{bundle_path}\"" + end + end + + def archive(config) + # Create .ipa archive. + app_bundle = config.app_bundle('iPhoneOS') + archive = config.archive + if !File.exist?(archive) or File.mtime(app_bundle) > File.mtime(archive) + App.info 'Create', archive + tmp = "/tmp/ipa_root" + sh "/bin/rm -rf #{tmp}" + sh "/bin/mkdir -p #{tmp}/Payload" + sh "/bin/cp -r \"#{app_bundle}\" #{tmp}/Payload" + Dir.chdir(tmp) do + sh "/bin/chmod -R 755 Payload" + sh "/usr/bin/zip -q -r archive.zip Payload" + end + sh "/bin/cp #{tmp}/archive.zip \"#{archive}\"" + end + +=begin + # Create .xcarchive. Only in release mode. + if config.release? + xcarchive = File.join(File.dirname(app_bundle), config.name + '.xcarchive') + if !File.exist?(xcarchive) or File.mtime(app_bundle) > File.mtime(xcarchive) + App.info 'Create', xcarchive + apps = File.join(xcarchive, 'Products', 'Applications') + FileUtils.mkdir_p apps + sh "/bin/cp -r \"#{app_bundle}\" \"#{apps}\"" + dsyms = File.join(xcarchive, 'dSYMs') + FileUtils.mkdir_p dsyms + sh "/bin/cp -r \"#{config.app_bundle_dsym('iPhoneOS')}\" \"#{dsyms}\"" + app_path = "Applications/#{config.name}.app" + info_plist = { + 'ApplicationProperties' => { + 'ApplicationPath' => app_path, + 'CFBundleIdentifier' => config.identifier, + 'IconPaths' => config.icons.map { |x| File.join(app_path, x) }, + }, + 'ArchiveVersion' => 1, + 'CreationDate' => Time.now, + 'Name' => config.name, + 'SchemeName' => config.name + } + File.open(File.join(xcarchive, 'Info.plist'), 'w') do |io| + io.write Motion::PropertyList.to_s(info_plist) + end + end + end +=end + end + end +end; end diff --git a/lib/motion/project/config.rb b/lib/motion/project/config.rb new file mode 100644 index 00000000..12a70a90 --- /dev/null +++ b/lib/motion/project/config.rb @@ -0,0 +1,577 @@ +# 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 Config + VARS = [] + + def self.variable(*syms) + syms.each do |sym| + attr_accessor sym + VARS << sym.to_s + end + end + + class Deps < Hash + def []=(key, val) + key = relpath(key) + val = [val] unless val.is_a?(Array) + val = val.map { |x| relpath(x) } + super + end + + def relpath(path) + /^\./.match(path) ? path : File.join('.', path) + end + end + + variable :files, :xcode_dir, :sdk_version, :deployment_target, :frameworks, + :libs, :delegate_class, :name, :build_dir, :resources_dir, :specs_dir, + :identifier, :codesign_certificate, :provisioning_profile, + :device_family, :interface_orientations, :version, :icons, + :prerendered_icon, :seed_id, :entitlements, :fonts + + attr_accessor :spec_mode + + def initialize(project_dir, build_mode) + @project_dir = project_dir + @files = Dir.glob(File.join(project_dir, 'app/**/*.rb')) + @dependencies = {} + @frameworks = ['UIKit', 'Foundation', 'CoreGraphics'] + @libs = [] + @delegate_class = 'AppDelegate' + @name = 'Untitled' + @resources_dir = 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' + @icons = [] + @prerendered_icon = false + @vendor_projects = [] + @entitlements = {} + @spec_mode = false + @build_mode = build_mode + end + + def variables + map = {} + VARS.each do |sym| + map[sym] = + begin + send(sym) + rescue Exception + 'Error' + end + end + map + 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 != xcode_dot_app_path 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 + end + + def build_dir + unless File.directory?(@build_dir) + tried = false + begin + FileUtils.mkdir_p(@build_dir) + rescue Errno::EACCES + raise if tried + require 'digest/sha1' + hash = Digest::SHA1.hexdigest(File.expand_path(project_dir)) + tmp = File.join(ENV['TMPDIR'], hash) + App.warn "Cannot create build_dir `#{@build_dir}'. Check the permissions. Using a temporary build directory instead: `#{tmp}'" + @build_dir = tmp + tried = true + retry + end + end + @build_dir + end + + def build_mode_name + @build_mode.to_s.capitalize + end + + def development? + @build_mode == :development + end + + def release? + @build_mode == :release + end + + def development + yield if development? + end + + def release + yield if release? + end + + def versionized_build_dir(platform) + File.join(build_dir, platform + '-' + deployment_target + '-' + build_mode_name) + end + + attr_reader :project_dir + + def project_file + File.join(@project_dir, 'Rakefile') + end + + def files_dependencies(deps_hash) + res_path = lambda do |x| + path = /^\./.match(x) ? x : File.join('.', x) + unless @files.include?(path) + App.fail "Can't resolve dependency `#{x}'" + end + path + end + deps_hash.each do |path, deps| + deps = [deps] unless deps.is_a?(Array) + @dependencies[res_path.call(path)] = deps.map(&res_path) + end + end + + attr_reader :vendor_projects + + def vendor_project(path, type, opts={}) + @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 file_dependencies(file) + deps = @dependencies[file] + if deps + deps = deps.map { |x| file_dependencies(x) } + else + deps = [] + end + deps << file + deps + end + + def ordered_build_files + @ordered_build_files ||= begin + flat_deps = @files.map { |file| file_dependencies(file) }.flatten + paths = flat_deps.dup + flat_deps.each do |path| + n = paths.count(path) + if n > 1 + (n - 1).times { paths.delete_at(paths.rindex(path)) } + end + end + paths + end + end + + def frameworks_dependencies + @frameworks_dependencies ||= begin + # Compute the list of frameworks, including dependencies, that the project uses. + deps = [] + slf = File.join(sdk('iPhoneSimulator'), 'System', 'Library', 'Frameworks') + frameworks.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] + end + end + end + deps << framework + end + deps.uniq.select { |dep| File.exist?(File.join(datadir, 'BridgeSupport', dep + '.bridgesupport')) } + end + end + + def bridgesupport_files + @bridgesupport_files ||= begin + bs_files = [] + deps = ['RubyMotion'] + frameworks_dependencies + deps.each do |framework| + bs_path = File.join(datadir, 'BridgeSupport', framework + '.bridgesupport') + if File.exist?(bs_path) + bs_files << bs_path + end + end + bs_files + end + end + + def spec_files + Dir.glob(File.join(specs_dir, '**', '*.rb')) + end + + def motiondir + @motiondir ||= File.expand_path(File.join(File.dirname(__FILE__), '../../..')) + end + + def bindir + File.join(motiondir, 'bin') + 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) + File.join(platform_dir(platform), 'Developer/SDKs', + platform + sdk_version + '.sdk') + 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') if platform == 'iPhoneSimulator' + + execs.each do |exec| + paths.each do |path| + cc = File.join(path, exec) + return cc if File.exist?(cc) + end + end + App.fail "Can't locate compilers for platform `#{platform}'" + end + + def archs(platform) + Dir.glob(File.join(datadir, platform, '*.bc')).map do |path| + path.scan(/kernel-(.+).bc$/)[0][0] + end + end + + def arch_flags(platform) + archs(platform).map { |x| "-arch #{x}" }.join(' ') + end + + def common_flags(platform) + "#{arch_flags(platform)} -isysroot \"#{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) + common_flags(platform) + 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/, '')}" + 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, retina) + device = case family + when :iphone, 1 + "iPhone" + when :ipad, 2 + "iPad" + end + retina ? device + " (Retina)" : device + 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 info_plist + @info_plist ||= { + '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' => @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' => 'UIStatusBarStyleDefault', + 'DTXcode' => '0431', + 'DTSDKName' => 'iphoneos5.0', + 'DTSDKBuild' => '9A334', + 'DTPlatformName' => 'iphoneos', + 'DTCompiler' => 'com.apple.compilers.llvm.clang.1_0', + 'DTPlatformVersion' => '5.1', + 'DTXcodeBuild' => '4E1019', + 'DTPlatformBuild' => '9B176' + } + end + + def info_plist_data + Motion::PropertyList.to_s(info_plist) + end + + def pkginfo_data + "AAPL#{@bundle_signature}" + end + + def codesign_certificate + @codesign_certificate ||= begin + cert_type = (development? ? 'Developer' : 'Distribution') + 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 release? + dict['application-identifier'] ||= seed_id + '.' + identifier + end + Motion::PropertyList.to_s(dict) + end + + def fonts + @fonts ||= begin + if File.exist?(resources_dir) + Dir.chdir(resources_dir) do + Dir.glob('*.{otf,ttf}') + end + else + [] + 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 + + line = "/usr/bin/gen_bridge_metadata --format complete --no-64-bit --cflags \"-isysroot #{sdk_path} -miphoneos-version-min=#{sdk_version} -D__ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__=#{sdk_version_headers} -I. #{includes.join(' ')}\" #{headers.join(' ')} -o \"#{bs_file}\"" + unless system(line) + App.fail "Error when generating bridge metadata: #{line}" + end + end + end +end; end diff --git a/lib/motion/project/plist.rb b/lib/motion/project/plist.rb new file mode 100644 index 00000000..a9313fb1 --- /dev/null +++ b/lib/motion/project/plist.rb @@ -0,0 +1,76 @@ +# 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 'time' # For Time#iso8601 + +module Motion + class PropertyList + class << self + def to_s(plist) + str = < + + +EOS + cat_element(plist, str, 1) + str << "\n" + return str + end + + def indent_line(line, indent) + ("\t" * indent) + line + "\n" + end + + def cat_element(plist, str, indent) + case plist + when Hash + str << indent_line("", indent) + plist.each do |key, val| + raise "Hash key must be a string" unless key.is_a?(String) + str << indent_line("#{key}", indent + 1) + cat_element(val, str, indent + 1) + end + str << indent_line("", indent) + when Array + str << indent_line("", indent) + plist.each do |elem| + cat_element(elem, str, indent + 1) + end + str << indent_line("", indent) + when String + str << indent_line("#{plist}", indent) + when TrueClass + str << indent_line("", indent) + when FalseClass + str << indent_line("", indent) + when Time + str << indent_line("#{plist.utc.iso8601}", indent) + when Integer + str << indent_line("#{plist}", indent) + else + raise "Invalid plist object of type `#{plist.class}' (must be either a Hash, Array, String, or boolean true/false value)" + end + end + end + end +end diff --git a/lib/motion/project/vendor.rb b/lib/motion/project/vendor.rb new file mode 100644 index 00000000..9a16077e --- /dev/null +++ b/lib/motion/project/vendor.rb @@ -0,0 +1,201 @@ +# 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 Vendor + include Rake::DSL if Rake.const_defined?(:DSL) + + def initialize(path, type, config, opts) + @path = path.to_s + @type = type + @config = config + @opts = opts + @libs = [] + @bs_files = [] + end + + attr_reader :path, :libs, :bs_files + + def build(platform) + App.info 'Build', @path + send gen_method('build'), platform, @opts + if @libs.empty? + App.fail "Building vendor project `#{@path}' failed to create at least one `.a' library." + end + end + + def clean + send gen_method('clean') + end + + def build_static(platform, opts) + Dir.chdir(@path) do + build_dir = "build-#{platform}" + + libs = (opts.delete(:products) or Dir.glob('*.a')) + source_files = (opts.delete(:source_files) or Dir.glob('**/*.{c,m,cpp,cxx,mm,h}')) + cflags = (opts.delete(:cflags) or '') + + source_files.each do |srcfile| + objfile = File.join(build_dir, srcfile + '.o') + next if File.exist?(objfile) and File.mtime(objfile) > File.mtime(srcfile) + cplusplus = false + compiler = + case File.extname(srcfile) + when '.c', '.m' + @config.locate_compiler(platform, 'clang', 'gcc') + when '.cpp', '.cxx', '.mm' + cplusplus = true + @config.locate_compiler(platform, 'clang++', 'g++') + else + # Not a valid source file, skip. + next + end + + pch = File.join(build_dir, File.basename(@path) + '.pch') + unless File.exist?(pch) + FileUtils.mkdir_p File.dirname(pch) + File.open(pch, 'w') do |io| + io.puts < +#endif +EOS + end + end + + App.info 'Compile', File.join(@path, srcfile) + FileUtils.mkdir_p File.dirname(objfile) + sh "#{compiler} #{cflags} #{@config.cflags(platform, cplusplus)} -I. -include \"#{pch}\" -c \"#{srcfile}\" -o \"#{objfile}\"" + end + + if File.exist?(build_dir) + libname = 'lib' + File.basename(@path) + '.a' + Dir.chdir(build_dir) do + objs = Dir.glob('**/*.o') + FileUtils.rm_rf libname + unless objs.empty? + sh "#{@config.locate_binary('ar')} cq #{libname} #{objs.join(' ')}" + end + end + libpath = File.join(build_dir, libname) + libs << libpath if File.exist?(libpath) + end + + headers = source_files.select { |p| File.extname(p) == '.h' } + bs_files = [] + unless headers.empty? + bs_file = File.basename(@path) + '.bridgesupport' + if !File.exist?(bs_file) or headers.any? { |h| File.mtime(h) > File.mtime(bs_file) } + @config.gen_bridge_metadata(headers, bs_file) + end + bs_files << bs_file + end + + @libs = libs.map { |x| File.expand_path(x) } + @bs_files = bs_files.map { |x| File.expand_path(x) } + end + end + + def clean_static + ['iPhoneSimulator', 'iPhoneOS'].each do |platform| + build_dir = File.join(@path, "build-#{platform}") + if File.exist?(build_dir) + App.info 'Delete', build_dir + FileUtils.rm_rf build_dir + end + end + end + + def build_xcode(platform, opts) + Dir.chdir(@path) do + build_dir = "build-#{platform}" + if !File.exist?(build_dir) + FileUtils.mkdir build_dir + + # Prepare Xcode project settings. + xcodeproj = opts.delete(:xcodeproj) || begin + projs = Dir.glob('*.xcodeproj') + if projs.size != 1 + App.fail "Can't locate Xcode project file for vendor project #{@path}" + end + projs[0] + end + target = opts.delete(:target) + scheme = opts.delete(:scheme) + if target and scheme + App.fail "Both :target and :scheme are provided" + end + configuration = opts.delete(:configuration) || 'Release' + + # Unset environment variables that could potentially make the build + # to fail. + %w{CC CXX CFLAGS CXXFLAGS LDFLAGS}.each { |f| ENV[f] &&= nil } + + # Build project into `build' directory. We delete the build directory + # each time because Xcode is too stupid to be trusted to use the + # same build directory for different platform builds. + rm_rf 'build' + xcopts = '' + xcopts << "-target \"#{target}\" " if target + xcopts << "-scheme \"#{scheme}\" " if scheme + sh "/usr/bin/xcodebuild -project \"#{xcodeproj}\" #{xcopts} -configuration \"#{configuration}\" -sdk #{platform.downcase}#{@config.sdk_version} #{@config.arch_flags(platform)} CONFIGURATION_BUILD_DIR=build build" + + # Copy .a files into the platform build directory. + prods = opts.delete(:products) + Dir.glob('build/*.a').each do |lib| + next if prods and !prods.include?(File.basename(lib)) + lib = File.readlink(lib) if File.symlink?(lib) + sh "/bin/cp \"#{lib}\" \"#{build_dir}\"" + end + end + + # Generate the bridgesupport file if we need to. + bs_file = File.expand_path(File.basename(@path) + '.bridgesupport') + headers_dir = opts.delete(:headers_dir) + if !File.exist?(bs_file) and headers_dir + project_dir = File.expand_path(@config.project_dir) + headers = Dir.glob(File.join(project_dir, headers_dir, '**/*.h')) + @config.gen_bridge_metadata(headers, bs_file) + end + + @bs_files = Dir.glob('*.bridgesupport').map { |x| File.expand_path(x) } + @libs = Dir.glob("#{build_dir}/*.a").map { |x| File.expand_path(x) } + end + end + + def clean_xcode + Dir.chdir(@path) do + ['build', 'build-iPhoneOS', 'build-iPhoneSimulator'].each { |x| rm_rf x } + end + end + + private + + def gen_method(prefix) + method = "#{prefix}_#{@type.to_s}".intern + raise "Invalid vendor project type: #{@type}" unless respond_to?(method) + method + end + end +end; end diff --git a/lib/motion/spec.rb b/lib/motion/spec.rb new file mode 100644 index 00000000..7e4536b8 --- /dev/null +++ b/lib/motion/spec.rb @@ -0,0 +1,551 @@ +# Bacon -- small RSpec clone. +# +# "Truth will sooner come out from error than from confusion." ---Francis Bacon +# +# Copyright (C) 2011 Eloy DurĂ¡n eloy.de.enige@gmail.com +# Copyright (C) 2007 - 2011 Christian Neukirchen +# +# Bacon is freely distributable under the terms of an MIT-style license. +# See COPYING or http://www.opensource.org/licenses/mit-license.php. + +module Bacon + VERSION = "1.3" + + Counter = Hash.new(0) + ErrorLog = "" + Shared = Hash.new { |_, name| + raise NameError, "no such context: #{name.inspect}" + } + + RestrictName = // unless defined? RestrictName + RestrictContext = // unless defined? RestrictContext + + Backtraces = true unless defined? Backtraces + + module SpecDoxOutput + def handle_specification_begin(name) + puts spaces + name + end + + def handle_specification_end + puts if Counter[:context_depth] == 1 + end + + def handle_requirement_begin(description) + print "#{spaces} - #{description}" + end + + def handle_requirement_end(error) + puts error.empty? ? "" : " [#{error}]" + end + + def handle_summary + print ErrorLog if Backtraces + puts "%d specifications (%d requirements), %d failures, %d errors" % + Counter.values_at(:specifications, :requirements, :failed, :errors) + end + + def spaces + " " * (Counter[:context_depth] - 1) + end + end + + module TestUnitOutput + def handle_specification_begin(name); end + def handle_specification_end ; end + + def handle_requirement_begin(description) end + def handle_requirement_end(error) + if error.empty? + print "." + else + print error[0..0] + end + end + + def handle_summary + puts "", "Finished in #{Time.now - @timer} seconds." + puts ErrorLog if Backtraces + puts "%d tests, %d assertions, %d failures, %d errors" % + Counter.values_at(:specifications, :requirements, :failed, :errors) + end + end + + module TapOutput + def handle_specification_begin(name); end + def handle_specification_end ; end + + def handle_requirement_begin(description) + ErrorLog.replace "" + end + + def handle_requirement_end(error) + if error.empty? + puts "ok %-3d - %s" % [Counter[:specifications], description] + else + puts "not ok %d - %s: %s" % + [Counter[:specifications], description, error] + puts ErrorLog.strip.gsub(/^/, '# ') if Backtraces + end + end + + def handle_summary + puts "1..#{Counter[:specifications]}" + puts "# %d tests, %d assertions, %d failures, %d errors" % + Counter.values_at(:specifications, :requirements, :failed, :errors) + end + end + + module KnockOutput + def handle_specification_begin(name); end + def handle_specification_end ; end + + def handle_requirement_begin(description) + ErrorLog.replace "" + end + + def handle_requirement_end(error) + if error.empty? + puts "ok - %s" % [description] + else + puts "not ok - %s: %s" % [description, error] + puts ErrorLog.strip.gsub(/^/, '# ') if Backtraces + end + end + + def handle_summary; end + end + + extend SpecDoxOutput # default + + class Error < RuntimeError + attr_accessor :count_as + + def initialize(count_as, message) + @count_as = count_as + super message + end + end + + class Specification + attr_reader :description + + def initialize(context, description, block, before_filters, after_filters) + @context, @description, @block = context, description, block + @before_filters, @after_filters = before_filters.dup, after_filters.dup + + @postponed_blocks_count = 0 + @ran_spec_block = false + @ran_after_filters = false + @exception_occurred = false + @error = "" + end + + def postponed? + @postponed_blocks_count != 0 + end + + def run_before_filters + execute_block { @before_filters.each { |f| @context.instance_eval(&f) } } + end + + def run_spec_block + @ran_spec_block = true + # If an exception occurred, we definitely don't need to perform the actual spec anymore + unless @exception_occurred + execute_block { @context.instance_eval(&@block) } + end + finish_spec unless postponed? + end + + def run_after_filters + @ran_after_filters = true + execute_block { @after_filters.each { |f| @context.instance_eval(&f) } } + end + + def run + Bacon.handle_requirement_begin(@description) + Counter[:depth] += 1 + run_before_filters + @number_of_requirements_before = Counter[:requirements] + run_spec_block unless postponed? + end + + def schedule_block(seconds, &block) + # If an exception occurred, we definitely don't need to schedule any more blocks + unless @exception_occurred + @postponed_blocks_count += 1 + performSelector("run_postponed_block:", withObject:block, afterDelay:seconds) + end + end + + def postpone_block(timeout = 1, &block) + # If an exception occurred, we definitely don't need to schedule any more blocks + unless @exception_occurred + if @postponed_block + raise "Only one indefinite `wait' block at the same time is allowed!" + else + @postponed_blocks_count += 1 + @postponed_block = block + performSelector("postponed_block_timeout_exceeded", withObject:nil, afterDelay:timeout) + end + end + end + + def postpone_block_until_change(object_to_observe, key_path, timeout = 1, &block) + # If an exception occurred, we definitely don't need to schedule any more blocks + unless @exception_occurred + if @postponed_block + raise "Only one indefinite `wait' block at the same time is allowed!" + else + @postponed_blocks_count += 1 + @postponed_block = block + @observed_object_and_key_path = [object_to_observe, key_path] + object_to_observe.addObserver(self, forKeyPath:key_path, options:0, context:nil) + performSelector("postponed_change_block_timeout_exceeded", withObject:nil, afterDelay:timeout) + end + end + end + + def observeValueForKeyPath(key_path, ofObject:object, change:_, context:__) + resume + end + + def postponed_change_block_timeout_exceeded + remove_observer! + postponed_block_timeout_exceeded + end + + def remove_observer! + if @observed_object_and_key_path + object, key_path = @observed_object_and_key_path + object.removeObserver(self, forKeyPath:key_path) + @observed_object_and_key_path = nil + end + end + + def postponed_block_timeout_exceeded + cancel_scheduled_requests! + execute_block { raise Error.new(:failed, "timeout exceeded: #{@context.name} - #{@description}") } + @postponed_blocks_count = 0 + finish_spec + end + + def resume + NSObject.cancelPreviousPerformRequestsWithTarget(self, selector:'postponed_block_timeout_exceeded', object:nil) + NSObject.cancelPreviousPerformRequestsWithTarget(self, selector:'postponed_change_block_timeout_exceeded', object:nil) + remove_observer! + block, @postponed_block = @postponed_block, nil + run_postponed_block(block) + end + + def run_postponed_block(block) + # If an exception occurred, we definitely don't need execute any more blocks + execute_block(&block) unless @exception_occurred + @postponed_blocks_count -= 1 + unless postponed? + if @ran_after_filters + exit_spec + elsif @ran_spec_block + finish_spec + else + run_spec_block + end + end + end + + def finish_spec + if !@exception_occurred && Counter[:requirements] == @number_of_requirements_before + # the specification did not contain any requirements, so it flunked + execute_block { raise Error.new(:missing, "empty specification: #{@context.name} #{@description}") } + end + run_after_filters + exit_spec unless postponed? + end + + def cancel_scheduled_requests! + NSObject.cancelPreviousPerformRequestsWithTarget(@context) + NSObject.cancelPreviousPerformRequestsWithTarget(self) + end + + def exit_spec + cancel_scheduled_requests! + Counter[:depth] -= 1 + Bacon.handle_requirement_end(@error) + @context.specification_did_finish(self) + end + + def execute_block + begin + yield + rescue Object => e + @exception_occurred = true + + ErrorLog << "#{e.class}: #{e.message}\n" + lines = $DEBUG ? e.backtrace : e.backtrace.find_all { |line| line !~ /bin\/macbacon|\/mac_bacon\.rb:\d+/ } + lines.each_with_index { |line, i| + ErrorLog << "\t#{line}#{i==0 ? ": #{@context.name} - #{@description}" : ""}\n" + } + ErrorLog << "\n" + + @error = if e.kind_of? Error + Counter[e.count_as] += 1 + e.count_as.to_s.upcase + else + Counter[:errors] += 1 + "ERROR: #{e.class}" + end + end + end + end + + def self.add_context(context) + (@contexts ||= []) << context + end + + def self.current_context_index + @current_context_index ||= 0 + end + + def self.current_context + @contexts[current_context_index] + end + + def self.run + @timer ||= Time.now + Counter[:context_depth] += 1 + handle_specification_begin(current_context.name) + current_context.performSelector("run", withObject:nil, afterDelay:0) + end + + def self.context_did_finish(context) + handle_specification_end + Counter[:context_depth] -= 1 + if (@current_context_index + 1) < @contexts.size + @current_context_index += 1 + run + else + # DONE + handle_summary + exit(Counter.values_at(:failed, :errors).inject(:+)) + end + end + + class Context + attr_reader :name, :block + + def initialize(name, before = nil, after = nil, &block) + @name = name + @before, @after = (before ? before.dup : []), (after ? after.dup : []) + @block = block + @specifications = [] + @current_specification_index = 0 + + Bacon.add_context(self) + + instance_eval(&block) + end + + def run + # TODO + #return unless name =~ RestrictContext + if spec = current_specification + spec.performSelector("run", withObject:nil, afterDelay:0) + else + Bacon.context_did_finish(self) + end + end + + def current_specification + @specifications[@current_specification_index] + end + + def specification_did_finish(spec) + if (@current_specification_index + 1) < @specifications.size + @current_specification_index += 1 + run + else + Bacon.context_did_finish(self) + end + end + + def before(&block); @before << block; end + def after(&block); @after << block; end + + def behaves_like(*names) + names.each { |name| instance_eval(&Shared[name]) } + end + + def it(description, &block) + return unless description =~ RestrictName + block ||= lambda { should.flunk "not implemented" } + Counter[:specifications] += 1 + @specifications << Specification.new(self, description, block, @before, @after) + end + + def should(*args, &block) + if Counter[:depth]==0 + it('should '+args.first,&block) + else + super(*args,&block) + end + end + + def describe(*args, &block) + context = Bacon::Context.new(args.join(' '), @before, @after, &block) + (parent_context = self).methods(false).each {|e| + class< e + e + else + false + end + + def throw?(sym) + catch(sym) { + call + return false + } + return true + end + + def change? + pre_result = yield + called = call + post_result = yield + pre_result != post_result + end +end + +class Numeric + def close?(to, delta) + (to.to_f - self).abs <= delta.to_f rescue false + end +end + + +class Object + def should(*args, &block) Should.new(self).be(*args, &block) end +end + +module Kernel + private + def describe(*args, &block) Bacon::Context.new(args.join(' '), &block) end + def shared(name, &block) Bacon::Shared[name] = block end +end + +class Should + # Kills ==, ===, =~, eql?, equal?, frozen?, instance_of?, is_a?, + # kind_of?, nil?, respond_to?, tainted? + instance_methods.each { |name| undef_method name if name =~ /\?|^\W+$/ } + + def initialize(object) + @object = object + @negated = false + end + + def not(*args, &block) + @negated = !@negated + + if args.empty? + self + else + be(*args, &block) + end + end + + def be(*args, &block) + if args.empty? + self + else + block = args.shift unless block_given? + satisfy(*args, &block) + end + end + + alias a be + alias an be + + def satisfy(*args, &block) + if args.size == 1 && String === args.first + description = args.shift + else + description = "" + end + + r = yield(@object, *args) + if Bacon::Counter[:depth] > 0 + Bacon::Counter[:requirements] += 1 + raise Bacon::Error.new(:failed, description) unless @negated ^ r + r + else + @negated ? !r : !!r + end + end + + def method_missing(name, *args, &block) + name = "#{name}?" if name.to_s =~ /\w[^?]\z/ + + desc = @negated ? "not " : "" + desc << @object.inspect << "." << name.to_s + desc << "(" << args.map{|x|x.inspect}.join(", ") << ") failed" + + satisfy(desc) { |x| x.__send__(name, *args, &block) } + end + + def equal(value) self == value end + def match(value) self =~ value end + def identical_to(value) self.equal? value end + alias same_as identical_to + + def flunk(reason="Flunked") + raise Bacon::Error.new(:failed, reason) + end +end diff --git a/lib/motion/version.rb b/lib/motion/version.rb new file mode 100644 index 00000000..5c4f3aea --- /dev/null +++ b/lib/motion/version.rb @@ -0,0 +1,26 @@ +# 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 + Version = "1.9" +end