mirror of
https://github.com/zhigang1992/RubyMotion.git
synced 2026-01-12 22:51:55 +08:00
initial revision
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.swp
|
||||
*.orig
|
||||
.DS_Store
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@@ -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.
|
||||
53
README.rdoc
Normal file
53
README.rdoc
Normal file
@@ -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.
|
||||
202
lib/motion/project.rb
Normal file
202
lib/motion/project.rb
Normal file
@@ -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
|
||||
168
lib/motion/project/app.rb
Normal file
168
lib/motion/project/app.rb
Normal file
@@ -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 <<EOS
|
||||
$:.unshift(\"#{$motion_libdir}\")
|
||||
require 'motion/project'
|
||||
|
||||
Motion::Project::App.setup do |app|
|
||||
# Use `rake config' to see complete project settings.
|
||||
app.name = '#{app_name}'
|
||||
end
|
||||
EOS
|
||||
end
|
||||
App.log 'Create', File.join(app_name, 'app')
|
||||
Dir.mkdir('app')
|
||||
App.log 'Create', File.join(app_name, 'app/app_delegate.rb')
|
||||
File.open('app/app_delegate.rb', 'w') do |io|
|
||||
io.puts <<EOS
|
||||
class AppDelegate
|
||||
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
||||
true
|
||||
end
|
||||
end
|
||||
EOS
|
||||
end
|
||||
App.log 'Create', File.join(app_name, 'resources')
|
||||
Dir.mkdir('resources')
|
||||
App.log 'Create', File.join(app_name, 'spec')
|
||||
Dir.mkdir('spec')
|
||||
App.log 'Create', File.join(app_name, 'spec/main_spec.rb')
|
||||
File.open('spec/main_spec.rb', 'w') do |io|
|
||||
io.puts <<EOS
|
||||
describe "Application '#{app_name}'" do
|
||||
before do
|
||||
@app = UIApplication.sharedApplication
|
||||
end
|
||||
|
||||
it "has one window" do
|
||||
@app.windows.size.should == 1
|
||||
end
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def log(what, msg)
|
||||
require 'thread'
|
||||
@print_mutex ||= Mutex.new
|
||||
# Because this method can be called concurrently, we don't want to mess any output.
|
||||
@print_mutex.synchronize do
|
||||
what = "\e[1m" + what.rjust(10) + "\e[0m" # bold
|
||||
$stderr.puts what + ' ' + msg
|
||||
end
|
||||
end
|
||||
|
||||
def warn(msg)
|
||||
log 'WARNING!', msg
|
||||
end
|
||||
|
||||
def fail(msg)
|
||||
log 'ERROR!', msg
|
||||
exit 1
|
||||
end
|
||||
|
||||
def info(what, msg)
|
||||
log what, msg unless VERBOSE
|
||||
end
|
||||
end
|
||||
end
|
||||
end; end
|
||||
487
lib/motion/project/builder.rb
Normal file
487
lib/motion/project/builder.rb
Normal file
@@ -0,0 +1,487 @@
|
||||
# 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 'thread'
|
||||
|
||||
module Motion; module Project;
|
||||
class Builder
|
||||
include Rake::DSL if Rake.const_defined?(:DSL)
|
||||
|
||||
def build(config, platform)
|
||||
datadir = config.datadir
|
||||
archs = config.archs(platform)
|
||||
|
||||
ruby = File.join(config.bindir, 'ruby')
|
||||
llc = File.join(config.bindir, 'llc')
|
||||
|
||||
if config.spec_mode and config.spec_files.empty?
|
||||
App.fail "No spec files in `#{config.specs_dir}'"
|
||||
end
|
||||
|
||||
# Locate SDK and compilers.
|
||||
sdk = config.sdk(platform)
|
||||
cc = config.locate_compiler(platform, 'gcc')
|
||||
cxx = config.locate_compiler(platform, 'g++')
|
||||
|
||||
build_dir = File.join(config.versionized_build_dir(platform))
|
||||
App.info 'Build', build_dir
|
||||
|
||||
# Prepare the list of BridgeSupport files needed.
|
||||
bs_files = config.bridgesupport_files
|
||||
|
||||
# Build vendor libraries.
|
||||
vendor_libs = []
|
||||
config.vendor_projects.each do |vendor_project|
|
||||
vendor_project.build(platform)
|
||||
vendor_libs.concat(vendor_project.libs)
|
||||
bs_files.concat(vendor_project.bs_files)
|
||||
end
|
||||
|
||||
# Build object files.
|
||||
objs_build_dir = File.join(build_dir, 'objs')
|
||||
FileUtils.mkdir_p(objs_build_dir)
|
||||
project_file_changed = File.mtime(config.project_file) > 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 = <<EOS
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
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 << <<EOS
|
||||
}
|
||||
EOS
|
||||
|
||||
if config.spec_mode
|
||||
main_txt << <<EOS
|
||||
@interface SpecLauncher : NSObject
|
||||
@end
|
||||
|
||||
@implementation SpecLauncher
|
||||
|
||||
+ (id)launcher
|
||||
{
|
||||
[UIApplication sharedApplication];
|
||||
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.1];
|
||||
}
|
||||
|
||||
- (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 << <<EOS
|
||||
}
|
||||
|
||||
@end
|
||||
EOS
|
||||
end
|
||||
main_txt << <<EOS
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
{
|
||||
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
|
||||
const char *progname = argv[0];
|
||||
ruby_init();
|
||||
ruby_init_loadpath();
|
||||
ruby_script(progname);
|
||||
int retval = 0;
|
||||
try {
|
||||
void *self = rb_vm_top_self();
|
||||
EOS
|
||||
main_txt << "[SpecLauncher launcher];\n" if config.spec_mode
|
||||
app_objs.each do |_, init_func|
|
||||
main_txt << "#{init_func}(self, 0);\n"
|
||||
end
|
||||
main_txt << <<EOS
|
||||
retval = UIApplicationMain(argc, argv, nil, @"#{config.delegate_class}");
|
||||
rb_exit(retval);
|
||||
}
|
||||
catch (...) {
|
||||
rb_rb2oc_exc_handler();
|
||||
}
|
||||
[pool release];
|
||||
return retval;
|
||||
}
|
||||
EOS
|
||||
|
||||
# Compile main file.
|
||||
main = File.join(objs_build_dir, 'main.mm')
|
||||
main_o = File.join(objs_build_dir, 'main.o')
|
||||
if !(File.exist?(main) and File.exist?(main_o) and File.read(main) == main_txt)
|
||||
File.open(main, 'w') { |io| io.write(main_txt) }
|
||||
sh "#{cxx} \"#{main}\" #{config.cflags(platform, true)} -c -o \"#{main_o}\""
|
||||
end
|
||||
|
||||
# Prepare bundle.
|
||||
bundle_path = config.app_bundle(platform)
|
||||
unless File.exist?(bundle_path)
|
||||
App.info 'Create', bundle_path
|
||||
FileUtils.mkdir_p(bundle_path)
|
||||
end
|
||||
|
||||
# Link executable.
|
||||
main_exec = config.app_bundle_executable(platform)
|
||||
main_exec_created = false
|
||||
if !File.exist?(main_exec) \
|
||||
or File.mtime(config.project_file) > 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)
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>rules</key>
|
||||
<dict>
|
||||
<key>.*</key>
|
||||
<true/>
|
||||
<key>Info.plist</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>10</real>
|
||||
</dict>
|
||||
<key>ResourceRules.plist</key>
|
||||
<dict>
|
||||
<key>omit</key>
|
||||
<true/>
|
||||
<key>weight</key>
|
||||
<real>100</real>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
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
|
||||
577
lib/motion/project/config.rb
Normal file
577
lib/motion/project/config.rb
Normal file
@@ -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(<<EOS) unless @xcode_error_printed
|
||||
===============================================================================
|
||||
It appears that you have a version of Xcode installed in /Applications that has
|
||||
not been set as the default version. It is possible that RubyMotion may be
|
||||
using old versions of certain tools which could eventually cause issues.
|
||||
|
||||
To fix this problem, you can type the following command in the terminal:
|
||||
$ sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer
|
||||
===============================================================================
|
||||
EOS
|
||||
@xcode_error_printed = true
|
||||
end
|
||||
return path if File.exist?(path)
|
||||
end
|
||||
|
||||
# Since xcode-select is borked, we assume the user installed Xcode
|
||||
# as an app (new in Xcode 4.3).
|
||||
return xcode_dot_app_path if File.exist?(xcode_dot_app_path)
|
||||
|
||||
App.fail "Can't locate any version of Xcode on the system."
|
||||
end
|
||||
end
|
||||
|
||||
def locate_binary(name)
|
||||
[File.join(xcode_dir, 'usr/bin'), '/usr/bin'].each do |dir|
|
||||
path = File.join(dir, name)
|
||||
return path if File.exist?(path)
|
||||
end
|
||||
App.fail "Can't locate binary `#{name}' on the system."
|
||||
end
|
||||
|
||||
def validate
|
||||
# Xcode version
|
||||
ary = `#{locate_binary('xcodebuild')} -version`.scan(/Xcode\s+([^\n]+)\n/)
|
||||
if ary and ary[0] and xcode_version = ary[0][0]
|
||||
App.fail "Xcode 4.x or greater is required" if xcode_version < '4.0'
|
||||
end
|
||||
|
||||
# sdk_version
|
||||
['iPhoneSimulator', 'iPhoneOS'].each do |platform|
|
||||
sdk_path = File.join(platforms_dir, platform + '.platform',
|
||||
"Developer/SDKs/#{platform}#{sdk_version}.sdk")
|
||||
unless File.exist?(sdk_path)
|
||||
App.fail "Can't locate #{platform} SDK #{sdk_version} at `#{sdk_path}'"
|
||||
end
|
||||
end
|
||||
|
||||
# deployment_target
|
||||
if deployment_target > 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(/<key>\s*Name\s*<\/key>\s*<string>\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(/<key>\s*#{key}\s*<\/key>\s*<array>(.*?)\s*<\/array>/m)[0][0].scan(/<string>(.*?)<\/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
|
||||
76
lib/motion/project/plist.rb
Normal file
76
lib/motion/project/plist.rb
Normal file
@@ -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
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
EOS
|
||||
cat_element(plist, str, 1)
|
||||
str << "</plist>\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("<dict>", indent)
|
||||
plist.each do |key, val|
|
||||
raise "Hash key must be a string" unless key.is_a?(String)
|
||||
str << indent_line("<key>#{key}</key>", indent + 1)
|
||||
cat_element(val, str, indent + 1)
|
||||
end
|
||||
str << indent_line("</dict>", indent)
|
||||
when Array
|
||||
str << indent_line("<array>", indent)
|
||||
plist.each do |elem|
|
||||
cat_element(elem, str, indent + 1)
|
||||
end
|
||||
str << indent_line("</array>", indent)
|
||||
when String
|
||||
str << indent_line("<string>#{plist}</string>", indent)
|
||||
when TrueClass
|
||||
str << indent_line("<true/>", indent)
|
||||
when FalseClass
|
||||
str << indent_line("<false/>", indent)
|
||||
when Time
|
||||
str << indent_line("<date>#{plist.utc.iso8601}</date>", indent)
|
||||
when Integer
|
||||
str << indent_line("<real>#{plist}</real>", 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
|
||||
201
lib/motion/project/vendor.rb
Normal file
201
lib/motion/project/vendor.rb
Normal file
@@ -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 <<EOS
|
||||
#ifdef __OBJC__
|
||||
# import <UIKit/UIKit.h>
|
||||
#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
|
||||
551
lib/motion/spec.rb
Normal file
551
lib/motion/spec.rb
Normal file
@@ -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 <purl.org/net/chneukirchen>
|
||||
#
|
||||
# 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<<context; self end.send(:define_method, e) {|*args| parent_context.send(e, *args)}
|
||||
}
|
||||
context
|
||||
end
|
||||
|
||||
def wait(seconds = nil, &block)
|
||||
if seconds
|
||||
current_specification.schedule_block(seconds, &block)
|
||||
else
|
||||
current_specification.postpone_block(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def wait_max(timeout, &block)
|
||||
current_specification.postpone_block(timeout, &block)
|
||||
end
|
||||
|
||||
def wait_for_change(object_to_observe, key_path, timeout = 1, &block)
|
||||
current_specification.postpone_block_until_change(object_to_observe, key_path, timeout, &block)
|
||||
end
|
||||
|
||||
def resume
|
||||
current_specification.resume
|
||||
end
|
||||
|
||||
def raise?(*args, &block); block.raise?(*args); end
|
||||
def throw?(*args, &block); block.throw?(*args); end
|
||||
def change?(*args, &block); block.change?(*args); end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
class Object
|
||||
def true?; false; end
|
||||
def false?; false; end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def true?; true; end
|
||||
end
|
||||
|
||||
class FalseClass
|
||||
def false?; true; end
|
||||
end
|
||||
|
||||
class Proc
|
||||
def raise?(*exceptions)
|
||||
call
|
||||
rescue *(exceptions.empty? ? RuntimeError : exceptions) => 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
|
||||
26
lib/motion/version.rb
Normal file
26
lib/motion/version.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user