thrust 0.0.5 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b6b885837e6c3dac116eca861cc6e772243c2d54
4
+ data.tar.gz: 625f5556939cdb39a575c648c9b85de3b4825a92
5
+ SHA512:
6
+ metadata.gz: 7f249ea7b0d1b30871c64de088427dccaf5a05fe66f1feb8707855c808374971b273b085ca1646b062e0358cac21866c9f9abc911425430c4c9ec2931a04f91d
7
+ data.tar.gz: 254b3ca985e6b4cee3c4d866d60e30d48fd9fe8f9fa3fdde3b665ad5b17a70eee309ec229c991ea0b64043444de9e0fcf9e1716e3079db67c84d225ae3554443
data/bin/thrust CHANGED
@@ -1,27 +1,33 @@
1
1
  #!/usr/bin/env ruby
2
+
3
+ require 'colorize'
4
+
5
+ command = ARGV.first
6
+
7
+ if command != 'install'
8
+ puts ''
9
+ puts ' USAGE: '.yellow + 'thrust install'
10
+ exit 0
11
+ end
12
+
13
+
2
14
  require 'fileutils'
3
15
  project_root = Dir.pwd
4
16
  thrust_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
5
17
 
6
- if (!Dir.exists?(File.join(project_root, 'thrust', 'lib', 'tasks')))
7
- Dir.mkdir(File.join(project_root, 'thrust'))
8
- Dir.mkdir(File.join(project_root, 'thrust', 'lib'))
9
- Dir.mkdir(File.join(project_root, 'thrust', 'lib', 'tasks'))
18
+ rakefile = File.join(project_root, 'Rakefile')
19
+ File.open(rakefile, 'a') do |f|
20
+ f.puts "require 'thrust/tasks'"
10
21
  end
11
22
 
12
- FileUtils.cp(Dir.glob(File.join(thrust_root, 'lib', 'tasks', '*.rake')), File.join(project_root, 'thrust', 'lib', 'tasks'))
13
- FileUtils.cp(File.join(thrust_root, 'lib', 'thrust_config.rb'), File.join(project_root, 'thrust', 'lib'))
14
23
  FileUtils.cp(File.join(thrust_root, 'lib', 'config', 'example.yml'), File.join(project_root, 'thrust.example.yml'))
24
+ puts ''
25
+ puts " To finish installation of " + "Thrust".green + ", rename:\n\n"
26
+ puts " #{project_root}/thrust.example.yml\n".blue
27
+ puts " to:\n\n"
28
+ puts " #{project_root}/thrust.yml\n".blue
29
+ puts " and edit it for your project."
15
30
 
16
- rakefile = File.join(project_root, 'Rakefile')
17
- if (File.exists?(rakefile))
18
- puts "You have an existing rakefile to add thrust tasks you should import its tasks:"
19
- puts "Dir.glob('thrust/lib/tasks/*.rake').each { |r| import r }"
20
- else
21
- File.open(rakefile, 'w') do |f|
22
- f.puts "Dir.glob('thrust/lib/tasks/*.rake').each { |r| import r }"
23
- end
24
- puts "a Rakefile was created for you type 'rake -T' to see a list of tasks"
25
- end
26
-
27
- puts "Rename #{project_root}/thrust.example.yml to #{project_root}/thrust.yml and edit it for your project."
31
+ puts ''
32
+ puts " Thrust rake tasks were generated in your Rakefile."
33
+ puts " Type 'rake -T' to see the list of tasks after you have created your thrust.yml configuration.".green
@@ -1,29 +1,36 @@
1
- # rename this file to thrust.yml
1
+ thrust_version: 0.2
2
2
  project_name: My Great Project
3
- app_name: My Great Project
4
- identity: iOS Signing Identity
3
+ app_name: My Great App
4
+ ios_distribution_certificate: 'Name of Distribution Signing Certificate'
5
+ ios_sim_binary: 'ios-sim' # or wax-sim. iOS only.
5
6
 
6
- specs:
7
- configuration: Release # or whichever iOS configuration you want to run specs under
8
- target: Specs # Name of the spec build target
9
- sdk: 6.1 # SDK version to build/run the specs with
10
- binary: 'Specs/bin/ios-sim' # or 'Specs/bin/waxim'
7
+ testflight:
8
+ api_token: 'testflight api token'
9
+ team_token: 'testflight team token'
11
10
 
12
- api_token: your testflight token here
13
- # Add testflight distrobution lists here. Rake tasks are built for all keys directly
14
- # under distribution.
15
- distributions:
16
- # builds rake testflight:devs task to deploy to the distrobution list. To create a second
17
- # deploy task, uncomment and modify the structure below.
18
- devs: # http://testflightapp.com/dashboard/team/edit
19
- team: TestFlight Team Name
20
- token: TestFlight Team Token
21
- default_list: Default Distribution List to Permission for the build
22
- configuration: iOS configuration to build with e.g. Debug, Release, etc.
11
+ deployment_targets:
12
+ staging:
13
+ distribution_list: Developers
14
+ notify: true
15
+ note_generation_method: autotag # If you set this value, it will auto-generate the deploy notes from the commit history. Optional.
16
+ ios_target: MyGreatAppTarget # Name of the build target. Optional, defaults to app name. iOS only.
17
+ ios_build_configuration: Release # iOS only
18
+ ios_provisioning_search_query: 'query to find Provisioning Profile' # iOS only. Optional.
23
19
 
24
- # builds rake testflight:foo
25
- # foo:
26
- # team: Foos
27
- # token: somethingthatlookslikeyourapitoken
28
- # default_list: Foo People
29
- # configuration: Release
20
+ demo:
21
+ distribution_list: Beta Testers
22
+ notify: true
23
+
24
+ ios_spec_targets:
25
+ specs:
26
+ target: UISpecs # name of the build target
27
+ build_configuration: Debug # name of the build configuration
28
+ build_sdk: 6.1 # SDK used to build the target. Optional, defaults to iphonesimulator.
29
+ runtime_sdk: 7.0 # SDK used to run the target. Not optional.
30
+ device: ipad # Device to run the specs on. Optional, defaults to iPhone.
31
+
32
+ integration:
33
+ target: IntegrationSpecs
34
+ build_configuration: Release
35
+ build_sdk: macosx
36
+ runtime_sdk: macosx
data/lib/tasks/cedar.rake CHANGED
@@ -1,39 +1,66 @@
1
1
  require 'yaml'
2
2
  require 'tmpdir'
3
+ require File.expand_path('../../thrust', __FILE__)
3
4
 
4
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'thrust_config'))
5
-
6
- @thrust = ThrustConfig.new(Dir.getwd, File.join(Dir.getwd, 'thrust.yml'))
7
-
8
- task :default => [:trim, :specs]
5
+ @thrust = Thrust::Config.make(Dir.getwd, File.join(Dir.getwd, 'thrust.yml'))
9
6
 
10
7
  desc 'Trim whitespace'
11
8
  task :trim do
12
- @thrust.system_or_exit %Q[git status --short | awk '{if ($1 != "D" && $1 != "R") print $2}' | grep -e '.*\.[cmh]$' | xargs sed -i '' -e 's/ / /g;s/ *$//g;']
9
+ awk_statement = <<-AWK
10
+ {
11
+ if ($1 == "RM" || $1 == "R")
12
+ print $4;
13
+ else if ($1 != "D")
14
+ print $2;
15
+ }
16
+ AWK
17
+ awk_statement.gsub!(%r{\s+}, " ")
18
+
19
+ Thrust::Executor.system_or_exit %Q[git status --porcelain | awk '#{awk_statement}' | grep -e '.*\.[cmh]$' | xargs sed -i '' -e 's/ / /g;s/ *$//g;']
13
20
  end
14
21
 
15
- desc 'Clean all targets'
16
- task :clean do
17
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -alltargets -configuration 'AdHoc' -sdk iphoneos clean", @thrust.output_file("clean")
18
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -alltargets -configuration 'Debug' -sdk iphonesimulator clean", @thrust.output_file("clean")
19
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -alltargets -configuration 'Release' -sdk iphonesimulator clean", @thrust.output_file("clean")
22
+ desc "Remove any focus from specs"
23
+ task :nof do
24
+ substitutions = focused_methods.map do |method|
25
+ unfocused_method = method.sub(/^f/, '')
26
+ "-e 's/#{method}/#{unfocused_method}/g;'"
27
+ end
28
+
29
+ Thrust::Executor.system_or_exit %Q[ rake focused_specs | xargs -I filename sed -i '' #{substitutions.join(' ')} "filename" ]
20
30
  end
21
31
 
22
- desc 'Build specs'
23
- task :build_specs do
24
- @thrust.kill_simulator
25
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -target #{@thrust.spec_config['target']} -configuration #{@thrust.spec_config['configuration']} -sdk iphonesimulator build", @thrust.output_file("specs")
32
+ desc "Print out names of files containing focused specs"
33
+ task :focused_specs do
34
+ pattern = focused_methods.join("\\|")
35
+ directories = @thrust.app_config['ios_spec_targets'].values.map {|h| h['target']}.join(' ')
36
+ Thrust::Executor.system_or_exit %Q[ grep -l -r -e "\\(#{pattern}\\)" #{directories} | grep -v 'Frameworks' ; exit 0 ]
26
37
  end
27
38
 
28
- desc 'Run specs'
29
- task :specs => :build_specs do
30
- binary = @thrust.spec_config['binary']
31
- if binary =~ /waxim%/
32
- @thrust.grep_cmd_for_failure(%Q[#{binary} -s #{@thrust.spec_config['sdk']} -f iphone -e CFFIXED_USER_HOME=#{Dir.tmpdir} -e CEDAR_HEADLESS_SPECS=1 -e CEDAR_REPORTER_CLASS=CDRDefaultReporter #{File.join(sim_dir, "#{@thrust.spec_config['target']}.app")}])
33
- elsif binary =~ /ios-sim$/
34
- @thrust.grep_cmd_for_failure(%Q[#{binary} launch #{File.join(@thrust.sim_dir, "#{@thrust.spec_config['target']}.app")} --sdk #{@thrust.spec_config['sdk']} --family iphone --retina --tall --setenv CFFIXED_USER_HOME=#{Dir.tmpdir} --setenv CEDAR_HEADLESS_SPECS=1 --setenv CEDAR_REPORTER_CLASS=CDRDefaultReporter])
35
- else
36
- puts "Uknown binary for running specs: '#{binary}'"
37
- exit(1)
39
+ desc 'Clean all targets'
40
+ task :clean_build do
41
+ Thrust::IOS::XCodeTools.build_configurations(@thrust.app_config['project_name']).each do |config|
42
+ xcode_tools = Thrust::IOS::XCodeTools.new($stdout, config, @thrust.build_dir, @thrust.app_config['project_name'])
43
+ xcode_tools.clean_build
38
44
  end
39
45
  end
46
+
47
+ (@thrust.app_config['ios_spec_targets'] || []).each do |task_name, target_info|
48
+ desc "Run the #{target_info['target']} target"
49
+ task task_name do
50
+ build_configuration = target_info['build_configuration']
51
+ target = target_info['target']
52
+ build_sdk = target_info['build_sdk'] || 'iphonesimulator' #build sdk - version you compile the code with
53
+ runtime_sdk = target_info['runtime_sdk'] #runtime sdk
54
+
55
+ xcode_tools = Thrust::IOS::XCodeTools.new($stdout, build_configuration, @thrust.build_dir, @thrust.app_config['project_name'])
56
+ xcode_tools.clean_and_build_target(target, build_sdk)
57
+
58
+ cedar_success = Thrust::IOS::Cedar.run($stdout, build_configuration, target, runtime_sdk, build_sdk, target_info['device'], @thrust.build_dir, @thrust.app_config['ios_sim_binary'])
59
+
60
+ exit(cedar_success ? 0 : 1)
61
+ end
62
+ end
63
+
64
+ def focused_methods
65
+ ["fit", "fcontext", "fdescribe"].map { |method| "#{method}(@" }
66
+ end
@@ -1,63 +1,40 @@
1
1
  require 'yaml'
2
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'thrust_config'))
2
+ require 'tempfile'
3
+ require File.expand_path('../../thrust', __FILE__)
3
4
 
4
- @thrust = ThrustConfig.new(Dir.getwd, File.join(Dir.getwd, 'thrust.yml'))
5
+ @thrust = Thrust::Config.make(Dir.getwd, File.join(Dir.getwd, 'thrust.yml'))
5
6
 
6
- namespace :bump do
7
- desc 'Bumps the build'
8
- task :build do
9
- @thrust.run_git_with_message 'Bumped build to $(agvtool what-version -terse)' do
10
- @thrust.system_or_exit 'agvtool bump -all'
11
- end
12
- end
13
-
14
- namespace :version do
15
- desc 'Bumps the major marketing version in (major.minor.patch)'
16
- task :major do
17
- @thrust.update_version(:major)
18
- end
19
-
20
- desc 'Bumps the minor marketing version in (major.minor.patch)'
21
- task :minor do
22
- @thrust.update_version(:minor)
23
- end
24
-
25
- desc 'Bumps the patch marketing version in (major.minor.patch)'
26
- task :patch do
27
- @thrust.update_version(:patch)
7
+ namespace :testflight do
8
+ android_project = File.exists?('AndroidManifest.xml')
9
+
10
+ @thrust.app_config['deployment_targets'].each do |task_name, deployment_config|
11
+ if android_project
12
+ desc "Deploy Android build to #{task_name} (use NOTIFY=false to prevent team notification)"
13
+ task task_name do |_, _|
14
+ Thrust::Android::Deploy.make(@thrust, deployment_config, task_name).run
15
+
16
+ Rake::Task['autotag:create'].invoke(task_name)
17
+ end
18
+ else
19
+ desc "Deploy iOS build to #{task_name} (use NOTIFY=false to prevent team notification)"
20
+ task task_name do |_, _|
21
+ Thrust::IOS::Deploy.make(@thrust, deployment_config, task_name).run
22
+
23
+ Rake::Task['autotag:create'].invoke(task_name)
24
+ end
28
25
  end
29
26
  end
30
27
  end
31
28
 
32
- desc 'Build custom configuration'
33
- task :build_configuration, :configuration do |task_name, args|
34
- build_prefix = @thrust.build_prefix_for(args[:configuration])
35
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -alltargets -configuration '#{args[:configuration]}' -sdk iphoneos clean", @thrust.output_file("clean")
36
- @thrust.kill_simulator
37
- @thrust.system_or_exit "xcodebuild -project #{@thrust.config['project_name']}.xcodeproj -target #{@thrust.config['app_name']} -configuration '#{args[:configuration]}' -sdk iphoneos build", @thrust.output_file(args[:configuration])
38
- @thrust.system_or_exit "/usr/bin/xcrun -sdk iphoneos PackageApplication -v '#{build_prefix}.app' -o '#{build_prefix}.ipa' --sign '#{@thrust.config['identity']}'"
39
- @thrust.system_or_exit "zip -r -T -y '#{build_prefix}.app.dSYM.zip' '#{build_prefix}.app.dSYM'"
40
- end
41
-
42
- namespace :testflight do
43
- @thrust.config['distributions'].each do |task_name, info|
44
- desc "Deploy build to testflight #{info['team']} team (use NOTIFY=false to prevent team notification)"
45
- task task_name do
46
- Rake::Task["testflight:deploy"].invoke(info['token'], info['default_list'], info['configuration'])
47
- end
29
+ namespace :autotag do
30
+ task :create, :stage do |_, args|
31
+ `autotag create #{args[:stage]}`
48
32
  end
49
33
 
50
- task :deploy, :team, :distribution_list, :configuration do |task, args|
51
- build_prefix = @thrust.build_prefix_for(args[:configuration])
52
- Rake::Task["bump:build"].invoke
53
- Rake::Task["build_configuration"].invoke(args[:configuration])
54
- @thrust.system_or_exit "curl http://testflightapp.com/api/builds.json\
55
- -F file=@#{build_prefix}.ipa\
56
- -F dsym=@#{build_prefix}.app.dSYM.zip\
57
- -F api_token='#{@thrust.config['api_token']}'\
58
- -F team_token='#{args[:team]}'\
59
- -F notes='This build was uploaded via the upload API'\
60
- -F notify=#{(ENV['NOTIFY'] || 'true').downcase.capitalize}\
61
- #{"-F distribution_lists='#{args[:distribution_list]}'" if args[:distribution_list]}"
34
+ desc 'Show the commit that is currently deployed to each environment'
35
+ task :list do
36
+ @thrust.app_config['deployment_targets'].each do |deployment_target, _|
37
+ puts Thrust::Git.new($stdout).commit_summary_for_last_deploy(deployment_target)
38
+ end
62
39
  end
63
40
  end
@@ -0,0 +1,33 @@
1
+ class Thrust::Android::Deploy
2
+ def self.make(thrust_config, deployment_config, deployment_target)
3
+ tools = Thrust::Android::Tools.new($stdout)
4
+ git = Thrust::Git.new($stdout)
5
+
6
+ testflight_config = thrust_config.app_config['testflight']
7
+ testflight = Thrust::Testflight.new($stdout, $stdin, testflight_config['api_token'], testflight_config['team_token'])
8
+
9
+ autogenerate_notes = deployment_config['note_generation_method'] == 'autotag'
10
+ new($stdout, tools, git, testflight, deployment_config['notify'], deployment_config['distribution_list'], autogenerate_notes, deployment_target)
11
+ end
12
+
13
+ def initialize(out, tools, git, testflight, notify, distribution_list, autogenerate_notes, deployment_target)
14
+ @out = out
15
+ @tools = tools
16
+ @git = git
17
+ @testflight = testflight
18
+ @notify = notify
19
+ @distribution_list = distribution_list
20
+ @deployment_target = deployment_target
21
+ @autogenerate_notes = autogenerate_notes
22
+ end
23
+
24
+ def run
25
+ @git.ensure_clean
26
+ @tools.change_build_number(Time.now.utc.strftime('%y%m%d%H%M'), @git.current_commit)
27
+ apk_path = @tools.build_signed_release
28
+
29
+ @testflight.upload(apk_path, @notify, @distribution_list, @autogenerate_notes, @deployment_target)
30
+
31
+ @git.reset
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'colorize'
2
+
3
+ class Thrust::Android::Tools
4
+ def initialize(out)
5
+ @out = out
6
+
7
+ if ENV['ANDROID_HOME'].nil?
8
+ if File.directory?('/usr/local/opt/android-sdk')
9
+ @out.puts 'Setting /usr/local/opt/android-sdk as ANDROID_HOME...'.magenta
10
+ ENV['ANDROID_HOME'] = '/usr/local/opt/android-sdk'
11
+ else
12
+ raise('**********Android is not installed. Run `brew install android`.**********')
13
+ end
14
+ end
15
+ end
16
+
17
+ def change_build_number(version_code, version_name)
18
+ Thrust::Executor.system_or_exit(
19
+ "sed -i ''" +
20
+ " -e 's/android:versionCode=\"[0-9]*\"/android:versionCode=\"#{version_code}\"/'" +
21
+ " -e 's/android:versionName=\"\\([^ \"]*\\)[^\"]*\"/android:versionName=\"\\1 (#{version_name})\"/'" +
22
+ " AndroidManifest.xml")
23
+ Thrust::Executor.system_or_exit(
24
+ "sed -i ''" +
25
+ " '1,/<version>/s/<version>\\([^- <]*\\)[^<]*<\\/version>/<version>\\1 (#{version_name})<\\/version>/'" +
26
+ " pom.xml")
27
+ end
28
+
29
+ def build_signed_release
30
+ Thrust::Executor.system_or_exit('mvn clean package -Prelease')
31
+ Dir.glob('target/*-signed-aligned.apk').first or raise 'Signed APK was not generated'
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ require 'colorize'
2
+
3
+ class Thrust::Config
4
+ attr_reader :project_root, :app_config, :build_dir
5
+ THRUST_VERSION = 0.2
6
+ THRUST_ROOT = File.expand_path('../..', __FILE__)
7
+
8
+ def self.make(relative_project_root, config_file)
9
+ begin
10
+ config_file_contents = YAML.load_file(config_file)
11
+ rescue Errno::ENOENT
12
+ puts ""
13
+ puts " Missing thrust.yml. Create by running:\n".red
14
+ puts " cp thrust.example.yml thrust.yml".blue
15
+ exit 1
16
+ rescue Psych::SyntaxError
17
+ puts ""
18
+ puts " Malformed thrust.yml.".red
19
+ exit 1
20
+ end
21
+ new(relative_project_root, config_file_contents)
22
+ end
23
+
24
+ def initialize(relative_project_root, config)
25
+ @project_root = File.expand_path(relative_project_root)
26
+ @build_dir = File.join(project_root, 'build')
27
+ @app_config = config
28
+ verify_configuration(@app_config)
29
+ end
30
+
31
+ private
32
+
33
+ def verify_configuration(config)
34
+ config['thrust_version'] ||= 0
35
+ if config['thrust_version'] != THRUST_VERSION
36
+ fail "Invalid configuration. Have you updated thrust recently? Your thrust.yml specifies version #{config['thrust_version']}, but thrust is at version #{THRUST_VERSION}. See README for details.".red
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,27 @@
1
+ module Thrust::Executor
2
+ def self.system_or_exit(cmd, output_file = nil)
3
+ self.system(cmd, output_file) or raise '******** Build failed ********'
4
+ end
5
+
6
+ def self.system(cmd, output_file = nil)
7
+ STDERR.puts "Executing #{cmd}"
8
+ cmd += " > #{output_file}" if output_file
9
+ Kernel::system(cmd)
10
+ end
11
+
12
+ def self.capture_output_from_system(cmd)
13
+ captured_output = `#{cmd}`
14
+ raise '******** Build failed ********' if $?.exitstatus > 0
15
+
16
+ captured_output
17
+ end
18
+
19
+ def self.check_command_for_failure(cmd)
20
+ STDERR.puts "Executing #{cmd} and checking for FAILURE"
21
+ result = %x[#{cmd} 2>&1]
22
+ STDERR.puts "Results:"
23
+ STDERR.puts result
24
+
25
+ result.include?("Finished") && !result.include?("FAILURE") && !result.include?("EXCEPTION")
26
+ end
27
+ end
data/lib/thrust/git.rb ADDED
@@ -0,0 +1,66 @@
1
+ require 'colorize'
2
+
3
+ class Thrust::Git
4
+ def initialize(out)
5
+ @out = out
6
+ end
7
+
8
+ def ensure_clean
9
+ if ENV['IGNORE_GIT']
10
+ @out.puts 'WARNING NOT CHECKING FOR CLEAN WORKING DIRECTORY'.red
11
+ else
12
+ @out.puts 'Checking for clean working tree...'
13
+ Thrust::Executor.system_or_exit 'git diff-index --quiet HEAD'
14
+ end
15
+ end
16
+
17
+ def current_commit
18
+ Thrust::Executor.capture_output_from_system('git log --format=format:%h -1').strip
19
+ end
20
+
21
+ def reset
22
+ Thrust::Executor.system_or_exit('git reset --hard')
23
+ end
24
+
25
+ def checkout_file(filename)
26
+ Thrust::Executor.system_or_exit("git checkout #{filename}")
27
+ end
28
+
29
+ def commit_summary_for_last_deploy(deployment_target)
30
+ sha_of_latest_deployed_commit = latest_deployed_commit(deployment_target)
31
+ if sha_of_latest_deployed_commit
32
+ "#{deployment_target}:".blue + " #{summary_for_commit(sha_of_latest_deployed_commit)}"
33
+ else
34
+ "#{deployment_target}:".blue + ' Never deployed'
35
+ end
36
+ end
37
+
38
+ def generate_notes_for_deployment(deployment_target)
39
+ sha_of_latest_commit = Thrust::Executor.capture_output_from_system('git rev-parse HEAD').strip
40
+ sha_of_latest_deployed_commit = latest_deployed_commit(deployment_target)
41
+
42
+ notes = Tempfile.new('deployment_notes')
43
+
44
+ if sha_of_latest_deployed_commit
45
+ Thrust::Executor.system_or_exit("git log --oneline #{sha_of_latest_deployed_commit}...#{sha_of_latest_commit}", notes.path)
46
+ else
47
+ notes.puts(summary_for_commit(sha_of_latest_commit))
48
+ notes.close
49
+ end
50
+
51
+ notes.path
52
+ end
53
+
54
+ private
55
+
56
+ def summary_for_commit(sha)
57
+ Thrust::Executor.capture_output_from_system("git log --oneline -n 1 #{sha}")
58
+ end
59
+
60
+ def latest_deployed_commit(deployment_target)
61
+ list = Thrust::Executor.capture_output_from_system("autotag list #{deployment_target}")
62
+ unless list.strip.empty?
63
+ list.split("\n").last.split(" ").first
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,21 @@
1
+ class Thrust::IOS::Cedar
2
+
3
+ def self.run(out, build_configuration, target, runtime_sdk, build_sdk, device, build_dir, simulator_binary)
4
+ if build_sdk == 'macosx'
5
+ build_path = File.join(build_dir, build_configuration)
6
+ app_dir = File.join(build_path, target)
7
+ Thrust::Executor.check_command_for_failure("DYLD_FRAMEWORK_PATH=#{build_path.inspect} #{app_dir}")
8
+ else
9
+ app_executable = File.join(build_dir, "#{build_configuration}-#{build_sdk}", "#{target}.app")
10
+
11
+ if simulator_binary =~ /waxim%/
12
+ Thrust::Executor.check_command_for_failure(%Q[#{simulator_binary} -s #{runtime_sdk} -f #{device} -e CFFIXED_USER_HOME=#{Dir.tmpdir} -e CEDAR_HEADLESS_SPECS=1 -e CEDAR_REPORTER_CLASS=CDRDefaultReporter #{app_executable}])
13
+ elsif simulator_binary =~ /ios-sim$/
14
+ Thrust::Executor.check_command_for_failure(%Q[#{simulator_binary} launch #{app_executable} --sdk #{runtime_sdk} --family #{device} --retina --tall --setenv CFFIXED_USER_HOME=#{Dir.tmpdir} --setenv CEDAR_HEADLESS_SPECS=1 --setenv CEDAR_REPORTER_CLASS=CDRDefaultReporter])
15
+ else
16
+ out.puts "Unknown binary for running specs: '#{simulator_binary}'"
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,36 @@
1
+ class Thrust::IOS::Deploy
2
+ def self.make(thrust_config, deployment_config, deployment_target)
3
+ build_configuration = deployment_config['ios_build_configuration']
4
+ x_code_tools = Thrust::IOS::XCodeTools.new($stdout, build_configuration, thrust_config.build_dir, thrust_config.app_config['project_name'])
5
+ git = Thrust::Git.new($stdout)
6
+ testflight_config = thrust_config.app_config['testflight']
7
+ testflight = Thrust::Testflight.new($stdout, $stdin, testflight_config['api_token'], testflight_config['team_token'])
8
+
9
+ new($stdout, x_code_tools, git, testflight, thrust_config, deployment_config, deployment_target)
10
+ end
11
+
12
+ def initialize(out, x_code_tools, git, testflight, thrust_config, deployment_config, deployment_target)
13
+ @out = out
14
+ @x_code_tools = x_code_tools
15
+ @git = git
16
+ @testflight = testflight
17
+ @thrust_config = thrust_config
18
+ @deployment_config = deployment_config
19
+ @deployment_target = deployment_target
20
+ end
21
+
22
+ def run
23
+ @git.ensure_clean
24
+ @x_code_tools.change_build_number(@git.current_commit)
25
+
26
+ app_name = @thrust_config.app_config['app_name']
27
+ target = @deployment_config['ios_target'] || app_name
28
+ ipa_file = @x_code_tools.cleanly_create_ipa(target, app_name, @thrust_config.app_config['ios_distribution_certificate'], @deployment_config['ios_provisioning_search_query'])
29
+
30
+ dsym_path = "#{@x_code_tools.build_configuration_directory}/#{app_name}.app.dSYM"
31
+
32
+ autogenerate_notes = @deployment_config['note_generation_method'] == 'autotag'
33
+ @testflight.upload(ipa_file, @deployment_config['notify'], @deployment_config['distribution_list'], autogenerate_notes, @deployment_target, dsym_path)
34
+ @git.reset
35
+ end
36
+ end
@@ -0,0 +1,117 @@
1
+ class Thrust::IOS::XCodeTools
2
+ ProvisioningProfileNotFound = Class.new(StandardError)
3
+
4
+ def self.build_configurations(project_name) #TODO: Backfill a test
5
+ output = Thrust::Executor.capture_output_from_system("xcodebuild -project #{project_name}.xcodeproj -list")
6
+ match = /Build Configurations:(.+?)\n\n/m.match(output)
7
+ if match
8
+ match[1].strip.split("\n").map { |line| line.strip }
9
+ else
10
+ []
11
+ end
12
+ end
13
+
14
+ def initialize(out, build_configuration, build_directory, project_name)
15
+ @out = out
16
+ @git = Thrust::Git.new(out)
17
+ @build_configuration = build_configuration
18
+ @build_directory = build_directory
19
+ @project_name = project_name
20
+ end
21
+
22
+ def change_build_number(build_number)
23
+ Thrust::Executor.system_or_exit "agvtool new-version -all '#{build_number}'"
24
+ @git.checkout_file('*.xcodeproj')
25
+ end
26
+
27
+ def cleanly_create_ipa(target, app_name, signing_identity, provision_search_query = nil)
28
+ clean_build
29
+ kill_simulator
30
+ build_target(target, 'iphoneos')
31
+ create_ipa(app_name, signing_identity, provision_search_query)
32
+ end
33
+
34
+ def build_configuration_directory
35
+ "#{@build_directory}/#{@build_configuration}-iphoneos"
36
+ end
37
+
38
+ def clean_build
39
+ @out.puts 'Cleaning...'
40
+ run_xcode('clean')
41
+ FileUtils.rm_rf(build_configuration_directory)
42
+ end
43
+
44
+ def clean_and_build_target(target, build_sdk)
45
+ clean_build
46
+ build_target(target, build_sdk)
47
+ end
48
+
49
+ private
50
+
51
+ def build_target(target, build_sdk)
52
+ @out.puts "Building..."
53
+ run_xcode('build', build_sdk, target)
54
+ end
55
+
56
+ def kill_simulator
57
+ @out.puts('Killing simulator...')
58
+ Thrust::Executor.system %q[killall -m -KILL "gdb"]
59
+ Thrust::Executor.system %q[killall -m -KILL "otest"]
60
+ Thrust::Executor.system %q[killall -m -KILL "iPhone Simulator"]
61
+ end
62
+
63
+ def provision_path(provision_search_query)
64
+ provision_search_path = File.expand_path("~/Library/MobileDevice/Provisioning Profiles/")
65
+ command = %Q(grep -rl "#{provision_search_query}" "#{provision_search_path}")
66
+ paths = `#{command}`.split("\n")
67
+ paths.first or raise(ProvisioningProfileNotFound, "\nCouldn't find provisioning profiles matching #{provision_search_query}.\n\nThe command used was:\n\n#{command}")
68
+ end
69
+
70
+ def create_ipa(app_name, signing_identity, provision_search_query)
71
+ @out.puts 'Packaging...'
72
+ ipa_filename = "#{build_configuration_directory}/#{app_name}.ipa"
73
+ cmd = [
74
+ "xcrun",
75
+ "-sdk iphoneos",
76
+ "-v PackageApplication",
77
+ "'#{build_configuration_directory}/#{app_name}.app'",
78
+ "-o '#{ipa_filename}'",
79
+ "--sign '#{signing_identity}'",
80
+ "--embed '#{provision_path(provision_search_query)}'"
81
+ ].join(' ')
82
+ Thrust::Executor.system_or_exit(cmd)
83
+ ipa_filename
84
+ end
85
+
86
+
87
+ def run_xcode(build_command, sdk = nil, target = nil)
88
+ target_flag = target ? "-target #{target}" : "-alltargets"
89
+ sdk_flag = sdk ? "-sdk #{sdk}" : ''
90
+
91
+ Thrust::Executor.system_or_exit(
92
+ [
93
+ 'set -o pipefail &&',
94
+ 'xcodebuild',
95
+ "-project #{@project_name}.xcodeproj",
96
+ target_flag,
97
+ "-configuration #{@build_configuration}",
98
+ sdk_flag,
99
+ "#{build_command}",
100
+ "SYMROOT=#{@build_directory.inspect}",
101
+ '2>&1',
102
+ "| grep -v 'backing file'"
103
+ ].join(' '),
104
+ output_file("#{@build_configuration}-#{build_command}")
105
+ )
106
+ end
107
+
108
+ def output_file(target)
109
+ output_dir = if ENV['IS_CI_BOX']
110
+ ENV['CC_BUILD_ARTIFACTS']
111
+ else
112
+ File.exists?(@build_directory) ? @build_directory : FileUtils.mkdir_p(@build_directory)
113
+ end
114
+
115
+ File.join(output_dir, "#{target}.output").tap { |file| @out.puts "Output: #{file}" }
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.expand_path("../../tasks/*.rake", __FILE__)].each do |file|
2
+ load file
3
+ end
@@ -0,0 +1,36 @@
1
+ class Thrust::Testflight
2
+ def initialize(out, input, api_token, team_token)
3
+ @out = out
4
+ @in = input
5
+ @git = Thrust::Git.new(@out)
6
+ @api_token = api_token
7
+ @team_token = team_token
8
+ end
9
+
10
+ def upload(package_file, notify, distribution_list, autogenerate_deploy_notes, deployment_target, dsym_path = nil)
11
+ if dsym_path
12
+ @out.puts 'Zipping dSYM...'
13
+ zipped_dsym_path = "#{dsym_path}.zip"
14
+ Thrust::Executor.system_or_exit "zip -r -T -y '#{zipped_dsym_path}' '#{dsym_path}'"
15
+ @out.puts 'Done!'
16
+ end
17
+
18
+ if autogenerate_deploy_notes
19
+ message_file_path = @git.generate_notes_for_deployment(deployment_target)
20
+ else
21
+ message_file_path = Thrust::UserPrompt.get_user_input('Deploy Notes: ', @out, @in)
22
+ end
23
+
24
+
25
+ Thrust::Executor.system_or_exit [
26
+ 'curl http://testflightapp.com/api/builds.json',
27
+ "-F file=@#{package_file}",
28
+ ("-F dsym=@#{zipped_dsym_path}" if dsym_path),
29
+ "-F api_token='#{@api_token}'",
30
+ "-F team_token='#{@team_token}'",
31
+ "-F notes=@#{message_file_path}",
32
+ "-F notify=#{(ENV['NOTIFY'] || notify).to_s.downcase.capitalize}",
33
+ ("-F distribution_lists='#{distribution_list}'" if distribution_list)
34
+ ].compact.join(' ')
35
+ end
36
+ end
@@ -0,0 +1,12 @@
1
+ require 'colorize'
2
+
3
+ module Thrust::UserPrompt
4
+ def self.get_user_input(prompt, out, stdin = $stdin)
5
+ out.print prompt.yellow
6
+ message = stdin.gets
7
+ message_file = Tempfile.new('message')
8
+ message_file << message
9
+ message_file.close
10
+ message_file.path
11
+ end
12
+ end
data/lib/thrust.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Thrust; end
2
+ module Thrust::Android; end
3
+ module Thrust::IOS; end
4
+
5
+ require 'thrust/config'
6
+ require 'thrust/executor'
7
+ require 'thrust/git'
8
+ require 'thrust/testflight'
9
+ require 'thrust/user_prompt'
10
+
11
+ require 'thrust/android/deploy'
12
+ require 'thrust/android/tools'
13
+
14
+ require 'thrust/ios/cedar'
15
+ require 'thrust/ios/deploy'
16
+ require 'thrust/ios/x_code_tools'
metadata CHANGED
@@ -1,8 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thrust
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
5
- prerelease:
4
+ version: 0.2.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Michael McCormick
@@ -14,45 +13,149 @@ authors:
14
13
  - Brandon Liu
15
14
  - Jeff Hui
16
15
  - Philip Kuryloski
16
+ - Andrew Bruce
17
+ - Aaron Levine
18
+ - Eugenia Dellapenna
19
+ - Aaron VonderHaar
20
+ - Sheel Choksi
21
+ - Rachel Bobbins
22
+ - Molly Trombley-McCann
17
23
  autorequire:
18
24
  bindir: bin
19
25
  cert_chain: []
20
26
  date: 2013-07-24 00:00:00.000000000 Z
21
- dependencies: []
22
- description: ''
27
+ dependencies:
28
+ - !ruby/object:Gem::Dependency
29
+ name: colorize
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ~>
33
+ - !ruby/object:Gem::Version
34
+ version: '0.6'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ~>
40
+ - !ruby/object:Gem::Version
41
+ version: '0.6'
42
+ - !ruby/object:Gem::Dependency
43
+ name: auto_tagger
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ~>
47
+ - !ruby/object:Gem::Version
48
+ version: '0.2'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: '0.2'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ~>
61
+ - !ruby/object:Gem::Version
62
+ version: '10.1'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '10.1'
70
+ - !ruby/object:Gem::Dependency
71
+ name: fakefs
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ version: '0.5'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ~>
82
+ - !ruby/object:Gem::Version
83
+ version: '0.5'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: timecop
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ description: Thrust provides a collection of rake tasks for iOS and Android projects. These
113
+ include tasks for running Cedar test suites (iOS) and for deploying apps to Testflight
114
+ (iOS and Android).
23
115
  email: mc+jbritz@pivotallabs.com
24
116
  executables:
25
117
  - thrust
26
118
  extensions: []
27
119
  extra_rdoc_files: []
28
120
  files:
29
- - lib/thrust_config.rb
121
+ - bin/thrust
122
+ - lib/config/example.yml
30
123
  - lib/tasks/cedar.rake
31
124
  - lib/tasks/testflight.rake
32
- - lib/config/example.yml
33
- - bin/thrust
34
- homepage: http://github.com/dipolesource/thrust
35
- licenses: []
125
+ - lib/thrust.rb
126
+ - lib/thrust/android/deploy.rb
127
+ - lib/thrust/android/tools.rb
128
+ - lib/thrust/config.rb
129
+ - lib/thrust/executor.rb
130
+ - lib/thrust/git.rb
131
+ - lib/thrust/ios/cedar.rb
132
+ - lib/thrust/ios/deploy.rb
133
+ - lib/thrust/ios/x_code_tools.rb
134
+ - lib/thrust/tasks.rb
135
+ - lib/thrust/testflight.rb
136
+ - lib/thrust/user_prompt.rb
137
+ homepage: http://github.com/pivotal/thrust
138
+ licenses:
139
+ - MIT
140
+ metadata: {}
36
141
  post_install_message:
37
142
  rdoc_options: []
38
143
  require_paths:
39
144
  - lib
40
145
  required_ruby_version: !ruby/object:Gem::Requirement
41
- none: false
42
146
  requirements:
43
- - - ! '>='
147
+ - - '>='
44
148
  - !ruby/object:Gem::Version
45
- version: '0'
149
+ version: 1.9.3
46
150
  required_rubygems_version: !ruby/object:Gem::Requirement
47
- none: false
48
151
  requirements:
49
- - - ! '>='
152
+ - - '>='
50
153
  - !ruby/object:Gem::Version
51
154
  version: '0'
52
155
  requirements: []
53
156
  rubyforge_project:
54
- rubygems_version: 1.8.25
157
+ rubygems_version: 2.2.1
55
158
  signing_key:
56
- specification_version: 3
57
- summary: iOS raketasks
159
+ specification_version: 4
160
+ summary: Thrust is a collection of rake tasks for iOS/Android development and deployment
58
161
  test_files: []
data/lib/thrust_config.rb DELETED
@@ -1,129 +0,0 @@
1
- class ThrustConfig
2
- attr_reader :project_root, :config, :spec_config, :build_dir
3
-
4
- def initialize(proj_root, config_file)
5
- @project_root = File.expand_path(proj_root)
6
- @build_dir = File.join(project_root, 'build')
7
- @config = YAML.load_file(config_file)
8
- @spec_config = config['specs']
9
- end
10
-
11
- def build_prefix_for(configuration)
12
- "#{build_dir}/#{configuration}-iphoneos/#{config['app_name']}"
13
- end
14
-
15
- # Xcode 4.3 stores its /Developer inside /Applications/Xcode.app, Xcode 4.2 stored it in /Developer
16
- def xcode_developer_dir
17
- `xcode-select -print-path`.strip
18
- end
19
-
20
- def sim_dir
21
- File.join(build_dir, spec_config['configuration'] + '-iphonesimulator')
22
- end
23
-
24
- def system_or_exit(cmd, stdout = nil)
25
- puts "Executing #{cmd}"
26
- cmd += " >#{stdout}" if stdout
27
- system(cmd) or raise '******** Build failed ********'
28
- end
29
-
30
- def run(cmd)
31
- puts "Executing #{cmd}"
32
- `#{cmd}`
33
- end
34
-
35
- def grep_cmd_for_failure(cmd)
36
- 1.times do
37
- puts "Executing #{cmd} and checking for FAILURE"
38
- %x[#{cmd} > #{Dir.tmpdir}/cmd.out 2>&1]
39
- status = $?
40
- result = File.read("#{Dir.tmpdir}/cmd.out")
41
- if status.success?
42
- puts 'Results:'
43
- puts result
44
- if result.include?('FAILURE')
45
- exit(1)
46
- else
47
- exit(0)
48
- end
49
- elsif status == 256
50
- redo
51
- else
52
- puts "Failed to launch: #{status}"
53
- exit(1)
54
- end
55
- end
56
- end
57
-
58
- def with_env_vars(env_vars)
59
- old_values = {}
60
- env_vars.each do |key,new_value|
61
- old_values[key] = ENV[key]
62
- ENV[key] = new_value
63
- end
64
-
65
- yield
66
-
67
- env_vars.each_key do |key|
68
- ENV[key] = old_values[key]
69
- end
70
- end
71
-
72
- def output_file(target)
73
- output_dir = if ENV['IS_CI_BOX']
74
- ENV['CC_BUILD_ARTIFACTS']
75
- else
76
- Dir.mkdir(build_dir) unless File.exists?(build_dir)
77
- build_dir
78
- end
79
-
80
- output_file = File.join(output_dir, "#{target}.output")
81
- puts "Output: #{output_file}"
82
- output_file
83
- end
84
-
85
- def kill_simulator
86
- system %q[killall -m -KILL "gdb"]
87
- system %q[killall -m -KILL "otest"]
88
- system %q[killall -m -KILL "iPhone Simulator"]
89
- end
90
-
91
- def update_version(release)
92
- run_git_with_message('Changes version to $(agvtool what-marketing-version -terse)') do
93
- version = run "agvtool what-marketing-version -terse | head -n1 |cut -f2 -d\="
94
- puts "version !#{version}!"
95
- build_regex = %r{^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))$}
96
- if (match = build_regex.match(version))
97
- puts "found match #{match.inspect}"
98
- v = {:major => match[:major].to_i, :minor => match[:minor].to_i, :patch => match[:patch].to_i}
99
- case(release)
100
- when :major then new_build_version(v[:major] + 1, 0, 0)
101
- when :minor then new_build_version(v[:major], v[:minor] + 1, 0)
102
- when :patch then new_build_version(v[:major], v[:minor], v[:patch] + 1)
103
- when :clear then new_build_version(v[:major], v[:minor], v[:patch])
104
- end
105
- else
106
- raise "Unknown version #{version} it should match major.minor.patch"
107
- end
108
- end
109
- end
110
-
111
- def new_build_version(major, minor, patch)
112
- version = [major, minor, patch].join(".")
113
- system_or_exit "agvtool new-marketing-version \"#{version}\""
114
- end
115
-
116
- def run_git_with_message(message, &block)
117
- if ENV['IGNORE_GIT']
118
- puts 'WARNING NOT CHECKING FOR CLEAN WORKING DIRECTORY'
119
- block.call
120
- else
121
- puts 'Checking for clean working tree...'
122
- system_or_exit 'git diff-index --quiet HEAD'
123
- puts 'Checking that the master branch is up to date...'
124
- system_or_exit 'git fetch && git diff --quiet HEAD origin/master'
125
- block.call
126
- system_or_exit "git commit -am \"#{message}\" && git push origin head"
127
- end
128
- end
129
- end