thrust 0.0.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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