omt-cli 1.6.3 → 1.6.4

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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +8 -0
  3. data/.gitignore +24 -0
  4. data/.travis.yml +31 -0
  5. data/CHANGELOG +188 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +35 -0
  9. data/Rakefile +10 -0
  10. data/bin/console +11 -0
  11. data/bin/fir +14 -0
  12. data/bin/setup +7 -0
  13. data/doc/build_apk.md +42 -0
  14. data/doc/build_ipa.md +66 -0
  15. data/doc/help.md +34 -0
  16. data/doc/info.md +44 -0
  17. data/doc/install.md +65 -0
  18. data/doc/login.md +19 -0
  19. data/doc/mapping.md +22 -0
  20. data/doc/publish.md +35 -0
  21. data/doc/upgrade.md +7 -0
  22. data/lib/fir.rb +28 -0
  23. data/lib/fir/api.yml +13 -0
  24. data/lib/fir/api.yml.bak +13 -0
  25. data/lib/fir/cli.rb +195 -0
  26. data/lib/fir/patches.rb +10 -0
  27. data/lib/fir/patches/blank.rb +131 -0
  28. data/lib/fir/patches/concern.rb +146 -0
  29. data/lib/fir/patches/default_headers.rb +9 -0
  30. data/lib/fir/patches/hash.rb +79 -0
  31. data/lib/fir/patches/instance_variables.rb +30 -0
  32. data/lib/fir/patches/native_patch.rb +28 -0
  33. data/lib/fir/patches/os_patch.rb +28 -0
  34. data/lib/fir/patches/try.rb +102 -0
  35. data/lib/fir/util.rb +87 -0
  36. data/lib/fir/util/build_apk.rb +76 -0
  37. data/lib/fir/util/build_common.rb +93 -0
  38. data/lib/fir/util/build_ipa.rb +240 -0
  39. data/lib/fir/util/config.rb +42 -0
  40. data/lib/fir/util/http.rb +30 -0
  41. data/lib/fir/util/info.rb +39 -0
  42. data/lib/fir/util/login.rb +18 -0
  43. data/lib/fir/util/mapping.rb +98 -0
  44. data/lib/fir/util/me.rb +19 -0
  45. data/lib/fir/util/parser/apk.rb +43 -0
  46. data/lib/fir/util/parser/bin/pngcrush +0 -0
  47. data/lib/fir/util/parser/common.rb +24 -0
  48. data/lib/fir/util/parser/ipa.rb +188 -0
  49. data/lib/fir/util/parser/pngcrush.rb +23 -0
  50. data/lib/fir/util/publish.rb +106 -0
  51. data/lib/fir/util/publish.rb.bak +185 -0
  52. data/lib/fir/version.rb +5 -0
  53. data/lib/fir/xcode_wrapper.sh +29 -0
  54. data/lib/omt-cli.rb +3 -0
  55. data/lib/omt_cli.rb +3 -0
  56. data/omt-cli.gemspec +48 -0
  57. data/test/build_ipa_test.rb +17 -0
  58. data/test/cases/test_apk.apk +0 -0
  59. data/test/cases/test_apk_txt +1 -0
  60. data/test/cases/test_ipa.ipa +0 -0
  61. data/test/cases/test_ipa_dsym +0 -0
  62. data/test/info_test.rb +36 -0
  63. data/test/login_test.rb +12 -0
  64. data/test/mapping_test.rb +18 -0
  65. data/test/me_test.rb +17 -0
  66. data/test/publish_test.rb +44 -0
  67. data/test/test_helper.rb +98 -0
  68. metadata +84 -4
@@ -0,0 +1,102 @@
1
+ # encoding: utf-8
2
+
3
+ class Object
4
+ # Invokes the public method whose name goes as first argument just like
5
+ # +public_send+ does, except that if the receiver does not respond to it the
6
+ # call returns +nil+ rather than raising an exception.
7
+ #
8
+ # This method is defined to be able to write
9
+ #
10
+ # @person.try(:name)
11
+ #
12
+ # instead of
13
+ #
14
+ # @person.name if @person
15
+ #
16
+ # +try+ calls can be chained:
17
+ #
18
+ # @person.try(:spouse).try(:name)
19
+ #
20
+ # instead of
21
+ #
22
+ # @person.spouse.name if @person && @person.spouse
23
+ #
24
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
25
+ #
26
+ # @person.try(:non_existing_method) #=> nil
27
+ #
28
+ # instead of
29
+ #
30
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) #=> nil
31
+ #
32
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
33
+ # to the method:
34
+ #
35
+ # nil.try(:to_i) # => nil, rather than 0
36
+ #
37
+ # Arguments and blocks are forwarded to the method if invoked:
38
+ #
39
+ # @posts.try(:each_slice, 2) do |a, b|
40
+ # ...
41
+ # end
42
+ #
43
+ # The number of arguments in the signature must match. If the object responds
44
+ # to the method the call is attempted and +ArgumentError+ is still raised
45
+ # in case of argument mismatch.
46
+ #
47
+ # If +try+ is called without arguments it yields the receiver to a given
48
+ # block unless it is +nil+:
49
+ #
50
+ # @person.try do |p|
51
+ # ...
52
+ # end
53
+ #
54
+ # You can also call try with a block without accepting an argument, and the block
55
+ # will be instance_eval'ed instead:
56
+ #
57
+ # @person.try { upcase.truncate(50) }
58
+ #
59
+ # Please also note that +try+ is defined on +Object+. Therefore, it won't work
60
+ # with instances of classes that do not have +Object+ among their ancestors,
61
+ # like direct subclasses of +BasicObject+. For example, using +try+ with
62
+ # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on
63
+ # the delegator itself.
64
+ def try(*a, &b)
65
+ try!(*a, &b) if a.empty? || respond_to?(a.first)
66
+ end
67
+
68
+ # Same as #try, but will raise a NoMethodError exception if the receiver is not +nil+ and
69
+ # does not implement the tried method.
70
+
71
+ def try!(*a, &b)
72
+ if a.empty? && block_given?
73
+ if b.arity.zero?
74
+ instance_eval(&b)
75
+ else
76
+ yield self
77
+ end
78
+ else
79
+ public_send(*a, &b)
80
+ end
81
+ end
82
+ end
83
+
84
+ class NilClass
85
+ # Calling +try+ on +nil+ always returns +nil+.
86
+ # It becomes especially helpful when navigating through associations that may return +nil+.
87
+ #
88
+ # nil.try(:name) # => nil
89
+ #
90
+ # Without +try+
91
+ # @person && @person.children.any? && @person.children.first.name
92
+ #
93
+ # With +try+
94
+ # @person.try(:children).try(:first).try(:name)
95
+ def try(*args)
96
+ nil
97
+ end
98
+
99
+ def try!(*args)
100
+ nil
101
+ end
102
+ end
@@ -0,0 +1,87 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative './util/http'
4
+ require_relative './util/config'
5
+ require_relative './util/parser/apk'
6
+ require_relative './util/parser/ipa'
7
+ require_relative './util/parser/pngcrush'
8
+ require_relative './util/login'
9
+ require_relative './util/me'
10
+ require_relative './util/info'
11
+ require_relative './util/build_common'
12
+ require_relative './util/build_ipa'
13
+ require_relative './util/build_apk'
14
+ require_relative './util/publish'
15
+ require_relative './util/mapping'
16
+
17
+ module FIR
18
+ module Util
19
+ extend ActiveSupport::Concern
20
+
21
+ module ClassMethods
22
+ include FIR::Http
23
+ include FIR::Config
24
+ include FIR::Login
25
+ include FIR::Me
26
+ include FIR::Info
27
+ include FIR::BuildCommon
28
+ include FIR::BuildIpa
29
+ include FIR::BuildApk
30
+ include FIR::Publish
31
+ include FIR::Mapping
32
+
33
+ attr_accessor :logger
34
+
35
+ def fetch_user_info(token)
36
+ get fir_api[:user_url], api_token: token
37
+ end
38
+
39
+ def fetch_user_uuid(token)
40
+ user_info = fetch_user_info(token)
41
+ user_info[:uuid]
42
+ end
43
+
44
+ def check_file_exist(path)
45
+ return if File.file?(path)
46
+
47
+ logger.error 'File does not exist'
48
+ exit 1
49
+ end
50
+
51
+ def check_supported_file(path)
52
+ return if APP_FILE_TYPE.include?(File.extname(path))
53
+
54
+ logger.error 'Unsupported file type'
55
+ exit 1
56
+ end
57
+
58
+ def check_token_cannot_be_blank(token)
59
+ return unless token.blank?
60
+
61
+ logger.error 'Token can not be blank'
62
+ exit 1
63
+ end
64
+
65
+ def check_logined
66
+ return unless current_token.blank?
67
+
68
+ logger.error 'Please use `fir login` first'
69
+ exit 1
70
+ end
71
+
72
+ def logger_info_blank_line
73
+ logger.info ''
74
+ end
75
+
76
+ def logger_info_dividing_line
77
+ logger.info '✈ -------------------------------------------- ✈'
78
+ end
79
+
80
+ def generate_rqrcode(string, png_file_path)
81
+ qrcode = ::RQRCode::QRCode.new(string.to_s)
82
+ qrcode.as_png(size: 500, border_modules: 2, file: png_file_path)
83
+ png_file_path
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module BuildApk
5
+
6
+ def build_apk(*args, options)
7
+ initialize_build_common_options(args, options)
8
+ set_flavor(options)
9
+
10
+ Dir.chdir(@build_dir)
11
+ @build_cmd = initialize_apk_build_cmd
12
+
13
+ logger_info_and_run_build_command
14
+
15
+ output_apk
16
+ publish_build_app(options) if options.publish?
17
+
18
+ logger_info_blank_line
19
+ end
20
+
21
+ private
22
+
23
+ def set_flavor(options)
24
+ unless options.flavor.blank?
25
+ @flavor = options.flavor
26
+ unless @flavor =~ /^assemble(.+)/
27
+ @flavor = "assemble#{@flavor}Release"
28
+ end
29
+ end
30
+ end
31
+
32
+ def initialize_apk_build_cmd
33
+ check_build_gradle_exist
34
+
35
+ cmd = "./gradlew build"
36
+ cmd = "./gradlew #{@flavor}" unless @flavor.blank?
37
+ cmd
38
+ end
39
+
40
+ def gradle_build_path
41
+ "#{@build_dir}/build/outputs/apk"
42
+ end
43
+
44
+ def prefix_gradle_build_path
45
+ "#{@build_dir}/app/build/outputs/apk"
46
+ end
47
+
48
+ def output_apk
49
+ @builded_apk ||= Dir["#{gradle_build_path}/*.apk"].find { |i| i =~ /release/ }
50
+ @builded_apk ||= Dir["#{prefix_gradle_build_path}/*.apk"].find { |i| i =~ /release/ }
51
+ @builded_apk ||= Dir["#{@build_dir}/*.apk"].find { |i| i =~ /release/ }
52
+
53
+ check_no_output_apk
54
+
55
+ apk_info = FIR.apk_info(@builded_apk)
56
+ @apk_name = @name.blank? ? "#{apk_info[:name]}-#{apk_info[:version]}-Build-#{apk_info[:build]}" : @name
57
+
58
+ @builded_app_path = "#{@output_path}/#{@apk_name}.apk"
59
+ FileUtils.cp(@builded_apk, @builded_app_path)
60
+ end
61
+
62
+ def check_no_output_apk
63
+ unless @builded_apk
64
+ logger.error 'Builded has no output apk'
65
+ exit 1
66
+ end
67
+ end
68
+
69
+ def check_build_gradle_exist
70
+ return if File.exist?("#{@build_dir}/build.gradle")
71
+
72
+ logger.error "The build.gradle isn't exit, please use gradle and edit"
73
+ exit 1
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,93 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module BuildCommon
5
+
6
+ def initialize_build_common_options(args, options)
7
+ @build_dir = initialize_build_dir(args, options)
8
+ @output_path = initialize_output_path(options)
9
+ @token = options[:token] || current_token
10
+ @changelog = options[:changelog].to_s
11
+ @short = options[:short].to_s
12
+ @name = options[:name].to_s
13
+ @proj = options[:proj].to_s
14
+ @export_qrcode = options[:qrcode]
15
+ end
16
+
17
+ def initialize_build_dir(args, options)
18
+ build_dir = args.first.to_s
19
+ if File.extname(build_dir) == '.git'
20
+ args.shift && git_clone_build_dir(build_dir, options)
21
+ elsif build_dir.blank? || !File.exist?(build_dir)
22
+ Dir.pwd
23
+ else
24
+ args.shift && File.absolute_path(build_dir)
25
+ end
26
+ end
27
+
28
+ def git_clone_build_dir(ssh_url, options)
29
+ repo_name = File.basename(ssh_url, '.git') + "_#{Time.now.strftime('%Y%m%dT%H%M%S')}"
30
+ branch = options[:branch].blank? ? 'master' : options[:branch]
31
+ git_cmd = "git clone --depth=50 --branch=#{branch} #{ssh_url} #{repo_name}"
32
+
33
+ logger.info git_cmd
34
+ logger_info_dividing_line
35
+
36
+ if system(git_cmd)
37
+ File.absolute_path(repo_name)
38
+ else
39
+ logger.error 'Git clone failed'
40
+ exit 1
41
+ end
42
+ end
43
+
44
+ def initialize_output_path(options)
45
+ if options[:output].blank?
46
+ output_path = "#{@build_dir}/fir_build"
47
+ FileUtils.mkdir_p(output_path) unless File.exist?(output_path)
48
+ output_path
49
+ else
50
+ output_path = options[:output].to_s
51
+ unless File.exist?(output_path)
52
+ logger.warn "The output path not exist and fir-cli will autocreate it..."
53
+ end
54
+ File.absolute_path(output_path)
55
+ end
56
+ end
57
+
58
+ def publish_build_app(options)
59
+ logger_info_blank_line
60
+ publish @builded_app_path, options
61
+ end
62
+
63
+ def logger_info_and_run_build_command
64
+ puts @build_cmd if $DEBUG
65
+
66
+ logger.info 'Building......'
67
+ logger_info_dividing_line
68
+
69
+ logger.info `#{@build_cmd}`
70
+
71
+ if $?.to_i != 0
72
+ logger.error 'Build failed'
73
+ exit 1
74
+ end
75
+ end
76
+
77
+ # split ['a=1', 'b=2'] => { 'a' => '1', 'b' => '2' }
78
+ def split_assignment_array_to_hash(arr)
79
+ hash = {}
80
+ arr.each do |assignment|
81
+ k, v = assignment.split('=', 2).map(&:strip)
82
+ hash[k] = v
83
+ end
84
+
85
+ hash
86
+ end
87
+
88
+ # convert { "a" => "1", "b" => "2" } => "a='1' b='2'"
89
+ def convert_hash_to_assignment_string(hash)
90
+ hash.collect { |k, v| "#{k}='#{v}'" }.join(' ')
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,240 @@
1
+ # encoding: utf-8
2
+ require 'xcodeproj'
3
+
4
+ module FIR
5
+ module BuildIpa
6
+
7
+ def build_ipa(*args, options)
8
+ initialize_build_common_options(args, options)
9
+
10
+ @build_tmp_dir = Dir.mktmpdir
11
+ @build_cmd = initialize_ipa_build_cmd(args, options)
12
+
13
+ logger_info_and_run_build_command
14
+
15
+ output_ipa_and_dsym
16
+ publish_build_app(options) if options.publish?
17
+ upload_build_dsym_mapping_file if options.mapping?
18
+
19
+ logger_info_blank_line
20
+ end
21
+
22
+ private
23
+
24
+ def initialize_ipa_build_cmd(args, options)
25
+ @configuration = options[:configuration] || 'Release'
26
+ @target_name = options[:target]
27
+ @scheme_name = options[:scheme]
28
+ @profile_name = options[:profile]
29
+ @destination = options[:destination]
30
+ @export_method = options[:export_method] || 'ad-hoc'
31
+ @optionPlistPath = options[:optionPlistPath]
32
+
33
+ build_cmd = 'xcodebuild archive -sdk iphoneos'
34
+ build_cmd += initialize_xcode_build_path(options)
35
+
36
+ build_settings = find_build_setting
37
+ @team_id = build_settings['DEVELOPMENT_TEAM']
38
+ provisioning_profile_id = build_settings['PROVISIONING_PROFILE']
39
+
40
+ check_ios_scheme(@scheme_name)
41
+ build_cmd += " -scheme '#{@scheme_name}'" unless @scheme_name.blank?
42
+ build_cmd += " -configuration '#{@configuration}'" unless @configuration.blank?
43
+
44
+ # build_cmd += " -target '#{@target_name}'" unless @target_name.blank?
45
+
46
+ # xcarchive name for build -exportArchive
47
+ @xcarchive_name = @scheme_name.nil? ? @target_name : @scheme_name + '.xcarchive'
48
+ @xcarchive_path = "#{@build_dir}/fir_build/#{@xcarchive_name}"
49
+
50
+ build_cmd += " -archivePath #{@xcarchive_path}"
51
+ build_cmd += " PROVISIONING_PROFILE='#{provisioning_profile_id}'"
52
+ build_cmd += " #{ipa_custom_settings(args)} 2>&1"
53
+ build_cmd
54
+ end
55
+
56
+ def ipa_custom_settings(args)
57
+ custom_settings = split_assignment_array_to_hash(args)
58
+
59
+ setting_str = convert_hash_to_assignment_string(custom_settings)
60
+ setting_str += " TARGET_BUILD_DIR='#{@build_tmp_dir}'" unless custom_settings['TARGET_BUILD_DIR']
61
+ setting_str += " CONFIGURATION_BUILD_DIR='#{@build_tmp_dir}'" unless custom_settings['CONFIGURATION_BUILD_DIR']
62
+ setting_str += " DWARF_DSYM_FOLDER_PATH='#{@output_path}'" unless custom_settings['DWARF_DSYM_FOLDER_PATH']
63
+ setting_str
64
+ end
65
+
66
+ def output_ipa_and_dsym
67
+ apps = Dir["#{@build_tmp_dir}/*.app"].sort_by(&:size)
68
+ # check_no_output_app(apps)
69
+
70
+ @temp_ipa = "#{@build_tmp_dir}/#{Time.now.to_i}.ipa"
71
+ archive_ipa(apps)
72
+
73
+ # check_archived_ipa_is_exist
74
+ rename_ipa_and_dsym
75
+
76
+ FileUtils.rm_rf(@build_tmp_dir) unless $DEBUG
77
+ logger.info 'Build Success'
78
+ end
79
+
80
+ def archive_ipa(apps)
81
+ logger.info 'Archiving......'
82
+ logger_info_dividing_line
83
+
84
+ option_plist_path = @optionPlistPath || gen_option_plist
85
+
86
+ @xcrun_cmd = "#{FIR::Config::XCODE_WRAPPER_PATH} -exportArchive"
87
+ @xcrun_cmd += " -archivePath #{@xcarchive_path}"
88
+ @xcrun_cmd += " -exportOptionsPlist #{option_plist_path}"
89
+ @xcrun_cmd += " -exportPath #{@build_dir}/fir_build"
90
+
91
+ puts @xcrun_cmd if $DEBUG
92
+ logger.info `#{@xcrun_cmd}`
93
+ end
94
+
95
+ def check_archived_ipa_is_exist
96
+ unless File.exist?(@temp_ipa)
97
+ logger.error 'Archive failed'
98
+ exit 1
99
+ end
100
+ end
101
+
102
+ def gen_option_plist
103
+ plist = "
104
+ <plist version=\"1.0\">
105
+ <dict>
106
+ <key>teamID</key>
107
+ <string>#{@team_id}</string>
108
+ <key>method</key>
109
+ <string>#{@export_method}</string>
110
+ <key>uploadSymbols</key>
111
+ <true/>
112
+ <key>compileBitcode</key>
113
+ <false/>
114
+ <key>uploadBitcode</key>
115
+ <false/>
116
+ </dict>
117
+ </plist>"
118
+
119
+ logger.info 'Generated plist file for exportOptionsPlist argument'
120
+ logger.info "{"
121
+ logger.info " teamID:#{@team_id}" unless @team_id.nil?
122
+ logger.info " method:#{@export_method}"
123
+ logger.info " uploadSymbols:true"
124
+ logger.info " uploadBitcode:false"
125
+ logger.info "}"
126
+
127
+ begin
128
+ plist_path = "#{@build_dir}/fir_build/exportOptions.plist"
129
+ file = File.open(plist_path, "w")
130
+ file.write(plist)
131
+ plist_path
132
+ rescue IOError => e
133
+ raise e
134
+ ensure
135
+ file.close unless file.nil?
136
+ end
137
+ end
138
+
139
+ # Find build setting from xcode project by target and configuration
140
+ def find_build_setting
141
+ project = Xcodeproj::Project.open(@xc_project)
142
+
143
+ # find target
144
+ used_target = project.targets[0]
145
+ if @target_name
146
+ project.targets.each do |target|
147
+ if target.name == @target_name
148
+ used_target = target
149
+ end
150
+ end
151
+ end
152
+
153
+ # find configuration settings
154
+ used_target.build_configurations.each do |config|
155
+ if config.name == @configuration
156
+ return config.build_settings
157
+ end
158
+ end
159
+
160
+ logger.info "configuration '#{@configuration}' not found"
161
+ end
162
+
163
+ def rename_ipa_and_dsym
164
+ @temp_ipa = "#{@build_dir}/fir_build/#{@scheme_name}.ipa"
165
+ ipa_info = FIR.ipa_info(@temp_ipa)
166
+
167
+ if @name.blank?
168
+ @ipa_name = "#{ipa_info[:name]}-#{ipa_info[:version]}-build-#{ipa_info[:build]}"
169
+ else
170
+ @ipa_name = @name
171
+ end
172
+
173
+ @builded_app_path = "#{@output_path}/#{@ipa_name}.ipa"
174
+ dsym_name = " #{@output_path}/#{ipa_info[:name]}.app.dSYM"
175
+
176
+ FileUtils.mv(@temp_ipa, @builded_app_path, force: true)
177
+ if File.exist?(dsym_name)
178
+ FileUtils.mv(dsym_name, "#{@output_path}/#{@ipa_name}.app.dSYM", force: true)
179
+ end
180
+ end
181
+
182
+ def upload_build_dsym_mapping_file
183
+ logger_info_blank_line
184
+
185
+ @app_info = ipa_info(@builded_app_path)
186
+ @mapping_file = Dir["#{@output_path}/#{@ipa_name}.app.dSYM/Contents/Resources/DWARF/*"].first
187
+
188
+ mapping @mapping_file, proj: @proj,
189
+ build: @app_info[:build],
190
+ version: @app_info[:version],
191
+ token: @token
192
+ end
193
+
194
+ def initialize_xcode_build_path(options)
195
+ @xc_workspace = check_and_find_ios_xcworkspace(@build_dir)
196
+ @xc_project = check_and_find_ios_xcodeproj(@build_dir)
197
+
198
+ if options.workspace?
199
+ " -workspace '#{@xc_workspace}'"
200
+ else
201
+ " -project '#{@xc_project}'"
202
+ end
203
+ end
204
+
205
+ %w(xcodeproj xcworkspace).each do |workplace|
206
+ define_method "check_and_find_ios_#{workplace}" do |path|
207
+ unless File.exist?(path)
208
+ logger.error "The first param BUILD_DIR must be a #{workplace} directory"
209
+ exit 1
210
+ end
211
+
212
+ if File.extname(path) == ".#{workplace}"
213
+ build_dir = path
214
+ else
215
+ build_dir = Dir["#{path}/*.#{workplace}"].first
216
+ if build_dir.blank?
217
+ logger.error "The #{workplace} file is missing, check the BUILD_DIR"
218
+ exit 1
219
+ end
220
+ end
221
+
222
+ build_dir
223
+ end
224
+ end
225
+
226
+ def check_ios_scheme(scheme_name)
227
+ if scheme_name.blank?
228
+ logger.error 'Must provide a scheme by `-S` option when build a workspace'
229
+ exit 1
230
+ end
231
+ end
232
+
233
+ def check_no_output_app(apps)
234
+ if apps.length == 0
235
+ logger.error 'Builded has no output app, Can not be packaged'
236
+ exit 1
237
+ end
238
+ end
239
+ end
240
+ end