fir-cli 0.2.3.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -82
  3. data/CHANGELOG +36 -0
  4. data/Gemfile +3 -1
  5. data/LICENSE.txt +1 -1
  6. data/README.md +83 -103
  7. data/Rakefile +1 -0
  8. data/bin/fir +9 -11
  9. data/fir-cli.gemspec +47 -26
  10. data/lib/fir/api.yml +5 -0
  11. data/lib/fir/cli.rb +107 -0
  12. data/lib/fir/patches/bin/pngcrush +0 -0
  13. data/lib/fir/patches/native_patch.rb +199 -0
  14. data/lib/fir/patches/os_patch.rb +22 -0
  15. data/lib/fir/patches/parser_patch.rb +165 -0
  16. data/lib/fir/patches.rb +5 -0
  17. data/lib/fir/util/build.rb +158 -0
  18. data/lib/fir/util/info.rb +79 -0
  19. data/lib/fir/util/login.rb +17 -0
  20. data/lib/fir/util/publish.rb +103 -0
  21. data/lib/fir/util.rb +45 -0
  22. data/lib/fir/version.rb +5 -0
  23. data/lib/fir-cli.rb +3 -0
  24. data/lib/fir.rb +87 -0
  25. data/lib/fir_cli.rb +3 -0
  26. metadata +57 -109
  27. data/Gemfile.lock +0 -45
  28. data/fir-cli.rb +0 -27
  29. data/lib/fir-cli/version.rb +0 -5
  30. data/lib/fir-cli-commands/00-info.rb +0 -17
  31. data/lib/fir-cli-commands/00-login.rb +0 -30
  32. data/lib/fir-cli-commands/00-profile.rb +0 -31
  33. data/lib/fir-cli-commands/00-upgrade.rb +0 -15
  34. data/lib/fir-cli-commands/00-version.rb +0 -10
  35. data/lib/fir-cli-commands/01-config.rb +0 -17
  36. data/lib/fir-cli-commands/100-resign_codesign.rb +0 -59
  37. data/lib/fir-cli-commands/100-resign_tapbeta.rb +0 -85
  38. data/lib/fir-cli-commands/101-resign.rb +0 -26
  39. data/lib/fir-cli-commands/11-publish.rb +0 -73
  40. data/lib/fir-cli-commands/12-build_ipa.rb +0 -153
  41. data/lib/fir-cli.chk.rb +0 -33
  42. data/lib/fir-cli.core.rb +0 -96
  43. data/lib/fir-cli.fir.rb +0 -49
  44. data/lib/fir-cli.opt.rb +0 -35
  45. data/lib/fir-cli.output.rb +0 -55
  46. data/lib/fir-cli.utils.rb +0 -108
  47. data/lib/lagunitas.ext.rb +0 -56
  48. data/lib/lagunitas.patch.rb +0 -51
  49. data/lib/user_config.patch.rb +0 -10
@@ -0,0 +1,199 @@
1
+ # encoding: utf-8
2
+
3
+ class Object
4
+ # activesupport/lib/active_support/core_ext/object/blank.rb
5
+ # An object is blank if it's false, empty, or a whitespace string.
6
+ # For example, '', ' ', +nil+, [], and {} are all blank.
7
+ #
8
+ # This simplifies
9
+ #
10
+ # address.nil? || address.empty?
11
+ #
12
+ # to
13
+ #
14
+ # address.blank?
15
+ #
16
+ # @return [true, false]
17
+ def blank?
18
+ respond_to?(:empty?) ? !!empty? : !self
19
+ end
20
+
21
+ # An object is present if it's not blank.
22
+ #
23
+ # @return [true, false]
24
+ def present?
25
+ !blank?
26
+ end
27
+
28
+ # Returns the receiver if it's present otherwise returns +nil+.
29
+ # <tt>object.presence</tt> is equivalent to
30
+ #
31
+ # object.present? ? object : nil
32
+ #
33
+ # For example, something like
34
+ #
35
+ # state = params[:state] if params[:state].present?
36
+ # country = params[:country] if params[:country].present?
37
+ # region = state || country || 'US'
38
+ #
39
+ # becomes
40
+ #
41
+ # region = params[:state].presence || params[:country].presence || 'US'
42
+ #
43
+ # @return [Object]
44
+ def presence
45
+ self if present?
46
+ end
47
+ end
48
+
49
+ class NilClass
50
+ # +nil+ is blank:
51
+ #
52
+ # nil.blank? # => true
53
+ #
54
+ # @return [true]
55
+ def blank?
56
+ true
57
+ end
58
+ end
59
+
60
+ class FalseClass
61
+ # +false+ is blank:
62
+ #
63
+ # false.blank? # => true
64
+ #
65
+ # @return [true]
66
+ def blank?
67
+ true
68
+ end
69
+ end
70
+
71
+ class TrueClass
72
+ # +true+ is not blank:
73
+ #
74
+ # true.blank? # => false
75
+ #
76
+ # @return [false]
77
+ def blank?
78
+ false
79
+ end
80
+ end
81
+
82
+ class Array
83
+ # An array is blank if it's empty:
84
+ #
85
+ # [].blank? # => true
86
+ # [1,2,3].blank? # => false
87
+ #
88
+ # @return [true, false]
89
+ alias_method :blank?, :empty?
90
+ end
91
+
92
+ class Hash
93
+ # A hash is blank if it's empty:
94
+ #
95
+ # {}.blank? # => true
96
+ # { key: 'value' }.blank? # => false
97
+ #
98
+ # @return [true, false]
99
+ alias_method :blank?, :empty?
100
+ end
101
+
102
+ class String
103
+ BLANK_RE = /\A[[:space:]]*\z/
104
+
105
+ # A string is blank if it's empty or contains whitespaces only:
106
+ #
107
+ # ''.blank? # => true
108
+ # ' '.blank? # => true
109
+ # "\t\n\r".blank? # => true
110
+ # ' blah '.blank? # => false
111
+ #
112
+ # Unicode whitespace is supported:
113
+ #
114
+ # "\u00a0".blank? # => true
115
+ #
116
+ # @return [true, false]
117
+ def blank?
118
+ BLANK_RE === self
119
+ end
120
+ end
121
+
122
+ class Numeric #:nodoc:
123
+ # No number is blank:
124
+ #
125
+ # 1.blank? # => false
126
+ # 0.blank? # => false
127
+ #
128
+ # @return [false]
129
+ def blank?
130
+ false
131
+ end
132
+ end
133
+
134
+ class Hash
135
+ # Returns a new hash with all keys converted using the block operation.
136
+ #
137
+ # hash = { name: 'Rob', age: '28' }
138
+ #
139
+ # hash.transform_keys{ |key| key.to_s.upcase }
140
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
141
+ def transform_keys
142
+ return enum_for(:transform_keys) unless block_given?
143
+ result = self.class.new
144
+ each_key do |key|
145
+ result[yield(key)] = self[key]
146
+ end
147
+ result
148
+ end
149
+
150
+ # Returns a new hash with all keys converted to symbols, as long as
151
+ # they respond to +to_sym+.
152
+ #
153
+ # hash = { 'name' => 'Rob', 'age' => '28' }
154
+ #
155
+ # hash.symbolize_keys
156
+ # # => {:name=>"Rob", :age=>"28"}
157
+ def symbolize_keys
158
+ transform_keys{ |key| key.to_sym rescue key }
159
+ end
160
+
161
+ # Returns a new hash with all keys converted by the block operation.
162
+ # This includes the keys from the root hash and from all
163
+ # nested hashes and arrays.
164
+ #
165
+ # hash = { person: { name: 'Rob', age: '28' } }
166
+ #
167
+ # hash.deep_transform_keys{ |key| key.to_s.upcase }
168
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
169
+ def deep_transform_keys(&block)
170
+ _deep_transform_keys_in_object(self, &block)
171
+ end
172
+
173
+ # Returns a new hash with all keys converted to symbols, as long as
174
+ # they respond to +to_sym+. This includes the keys from the root hash
175
+ # and from all nested hashes and arrays.
176
+ #
177
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
178
+ #
179
+ # hash.deep_symbolize_keys
180
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
181
+ def deep_symbolize_keys
182
+ deep_transform_keys{ |key| key.to_sym rescue key }
183
+ end
184
+
185
+ private
186
+ # support methods for deep transforming nested hashes and arrays
187
+ def _deep_transform_keys_in_object(object, &block)
188
+ case object
189
+ when Hash
190
+ object.each_with_object({}) do |(key, value), result|
191
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
192
+ end
193
+ when Array
194
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
195
+ else
196
+ object
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+
3
+ module OS
4
+
5
+ class << self
6
+ def windows?
7
+ (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) != nil
8
+ end
9
+
10
+ def mac?
11
+ (/darwin/ =~ RUBY_PLATFORM) != nil
12
+ end
13
+
14
+ def unix?
15
+ !OS.windows?
16
+ end
17
+
18
+ def linux?
19
+ OS.unix? && !OS.mac?
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,165 @@
1
+ # encoding: utf-8
2
+
3
+ module Parser
4
+
5
+ class << self
6
+
7
+ def png_bin
8
+ @png_bin ||= File.expand_path("../bin/pngcrush", __FILE__)
9
+ end
10
+
11
+ def uncrush_icon crushed_icon_path, uncrushed_icon_path
12
+ system("#{png_bin} -revert-iphone-optimizations #{crushed_icon_path} #{uncrushed_icon_path} &> /dev/null")
13
+ end
14
+
15
+ def crush_icon uncrushed_icon_path, crushed_icon_path
16
+ system("#{png_bin} -iphone #{uncrushed_icon_path} #{crushed_icon_path} &> /dev/null")
17
+ end
18
+ end
19
+
20
+ class IPA
21
+
22
+ def initialize(path)
23
+ @path = path
24
+ end
25
+
26
+ def app
27
+ @app ||= App.new(app_path)
28
+ end
29
+
30
+ def app_path
31
+ @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
32
+ end
33
+
34
+ def cleanup
35
+ return unless @contents
36
+ FileUtils.rm_rf(@contents)
37
+ @contents = nil
38
+ end
39
+
40
+ def metadata
41
+ return unless has_metadata?
42
+ @metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
43
+ end
44
+
45
+ def has_metadata?
46
+ File.file? metadata_path
47
+ end
48
+
49
+ def metadata_path
50
+ @metadata_path ||= File.join(@contents, 'iTunesMetadata.plist')
51
+ end
52
+
53
+ def release_type
54
+ has_metadata? ? 'store' : 'adhoc'
55
+ end
56
+
57
+ private
58
+
59
+ def contents
60
+ return if @contents
61
+ @contents = "tmp/ipa_files-#{Time.now.to_i}"
62
+
63
+ Zip::File.open(@path) do |zip_file|
64
+ zip_file.each do |f|
65
+ f_path = File.join(@contents, f.name)
66
+ FileUtils.mkdir_p(File.dirname(f_path))
67
+ zip_file.extract(f, f_path) unless File.exist?(f_path)
68
+ end
69
+ end
70
+
71
+ @contents
72
+ end
73
+ end
74
+
75
+ class App
76
+
77
+ def initialize(path)
78
+ @path = path
79
+ end
80
+
81
+ def info
82
+ @info ||= CFPropertyList.native_types(
83
+ CFPropertyList::List.new(file: File.join(@path, 'Info.plist')).value)
84
+ end
85
+
86
+ def name
87
+ info['CFBundleName']
88
+ end
89
+
90
+ def identifier
91
+ info['CFBundleIdentifier']
92
+ end
93
+
94
+ def display_name
95
+ info['CFBundleDisplayName']
96
+ end
97
+
98
+ def version
99
+ info['CFBundleVersion']
100
+ end
101
+
102
+ def short_version
103
+ info['CFBundleShortVersionString']
104
+ end
105
+
106
+ def icons
107
+ @icons ||= begin
108
+ icons = []
109
+ info['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles'].each do |name|
110
+ icons << get_image(name)
111
+ icons << get_image("#{name}@2x")
112
+ end
113
+ icons.delete_if { |i| !i }
114
+ rescue NoMethodError
115
+ []
116
+ end
117
+ end
118
+
119
+ def mobileprovision
120
+ return unless has_mobileprovision?
121
+ return @mobileprovision if @mobileprovision
122
+
123
+ @mobileprovision = CFPropertyList.native_types(
124
+ CFPropertyList::List.new(data: `security cms -D -i #{mobileprovision_path}`).value)
125
+ end
126
+
127
+ def has_mobileprovision?
128
+ File.file? mobileprovision_path
129
+ end
130
+
131
+ def mobileprovision_path
132
+ @mobileprovision_path ||= File.join(@path, 'embedded.mobileprovision')
133
+ end
134
+
135
+ def hide_developer_certificates
136
+ mobileprovision.delete('DeveloperCertificates') if has_mobileprovision?
137
+ end
138
+
139
+ def devices
140
+ mobileprovision['ProvisionedDevices'] if has_mobileprovision?
141
+ end
142
+
143
+ def distribution_name
144
+ "#{mobileprovision['Name']} - #{mobileprovision['TeamName']}" if has_mobileprovision?
145
+ end
146
+
147
+ def release_type
148
+ if has_mobileprovision?
149
+ if devices
150
+ 'adhoc'
151
+ else
152
+ 'inhouse'
153
+ end
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ def get_image name
160
+ path = File.join(@path, "#{name}.png")
161
+ return nil unless File.exist?(path)
162
+ path
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative './patches/native_patch'
4
+ require_relative './patches/os_patch'
5
+ require_relative './patches/parser_patch'
@@ -0,0 +1,158 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Build
5
+
6
+ def build_ipa *args, options
7
+ # initialize build options
8
+ if args.first.blank? || !File.exist?(args.first)
9
+ build_dir = Dir.pwd
10
+ else
11
+ build_dir = File.absolute_path(args.shift.to_s) # pop the first param
12
+ end
13
+
14
+ build_cmd = "xcodebuild build -sdk iphoneos"
15
+ build_tmp_dir = Dir.mktmpdir
16
+ custom_settings = parse_custom_settings(args) # convert ['a=1', 'b=2'] => { 'a' => '1', 'b' => '2' }
17
+ configuration = options[:configuration]
18
+ target_name = options[:target]
19
+ scheme_name = options[:scheme]
20
+ output_path = options[:output].blank? ? "#{build_dir}/build_ipa" : File.absolute_path(options[:output].to_s)
21
+
22
+ # check build environment and make build cmd
23
+ check_osx
24
+ if options.workspace?
25
+ workspace = check_and_find_workspace(build_dir)
26
+ check_scheme(scheme_name)
27
+ build_cmd += " -workspace '#{workspace}' -scheme '#{scheme_name}'"
28
+ else
29
+ project = check_and_find_project(build_dir)
30
+ build_cmd += " -project '#{project}'"
31
+ end
32
+
33
+ build_cmd += " -configuration '#{configuration}'" unless configuration.blank?
34
+ build_cmd += " -target '#{target_name}'" unless target_name.blank?
35
+
36
+ # convert { "a" => "1", "b" => "2" } => "a='1' b='2'"
37
+ setting_str = custom_settings.collect { |k, v| "#{k}='#{v}'" }.join(' ')
38
+ setting_str += " TARGET_BUILD_DIR='#{build_tmp_dir}'" unless custom_settings['TARGET_BUILD_DIR']
39
+ setting_str += " CONFIGURATION_BUILD_DIR='#{build_tmp_dir}'" unless custom_settings['CONFIGURATION_BUILD_DIR']
40
+ setting_str += " DWARF_DSYM_FOLDER_PATH='#{output_path}'" unless custom_settings['DWARF_DSYM_FOLDER_PATH']
41
+
42
+ build_cmd += " #{setting_str} 2>&1"
43
+ puts build_cmd if $DEBUG
44
+
45
+ logger.info "Building......"
46
+ logger_info_dividing_line
47
+
48
+ logger.info `#{build_cmd}`
49
+
50
+ FileUtils.mkdir_p(output_path) unless File.exist?(output_path)
51
+ Dir.chdir(build_tmp_dir) do
52
+ apps = Dir["*.app"]
53
+ if apps.length == 0
54
+ logger.error "Builded has no output app, Can not be packaged"
55
+ exit 1
56
+ end
57
+
58
+ apps.each do |app|
59
+ ipa_path = File.join(output_path, "#{File.basename(app, '.app')}.ipa")
60
+ zip_app2ipa(File.join(build_tmp_dir, app), ipa_path)
61
+ end
62
+ end
63
+
64
+ logger.info "Build Success"
65
+
66
+ if options.publish?
67
+ ipa_path = Dir["#{output_path}/*.ipa"].first
68
+ publish(ipa_path, short: options[:short], changelog: options[:changelog], token: options[:token])
69
+ end
70
+ end
71
+
72
+ def build_apk *args, options
73
+ end
74
+
75
+ private
76
+
77
+ def parse_custom_settings args
78
+ hash = {}
79
+ args.each do |setting|
80
+ k, v = setting.split('=', 2).map(&:strip)
81
+ hash[k] = v
82
+ end
83
+ hash
84
+ end
85
+
86
+ def check_osx
87
+ unless OS.mac?
88
+ logger.error "Unsupported OS type, `build_ipa` only support for OSX"
89
+ exit 1
90
+ end
91
+ end
92
+
93
+ def check_and_find_project path
94
+ unless File.exist?(path)
95
+ logger.error "The first param BUILD_DIR must be a xcodeproj directory"
96
+ exit 1
97
+ end
98
+
99
+ if is_project?(path)
100
+ project = path
101
+ else
102
+ project = Dir["#{path}/*.xcodeproj"].first
103
+ if project.blank?
104
+ logger.error "The xcodeproj file is missing, check the BUILD_DIR"
105
+ exit 1
106
+ end
107
+ end
108
+
109
+ project
110
+ end
111
+
112
+ def check_and_find_workspace path
113
+ unless File.exist?(path)
114
+ logger.error "The first param BUILD_DIR must be a xcworkspace directory"
115
+ exit 1
116
+ end
117
+
118
+ if is_workspace?(path)
119
+ workspace = path
120
+ else
121
+ workspace = Dir["#{path}/*.xcworkspace"].first
122
+ if workspace.blank?
123
+ logger.error "The xcworkspace file is missing, check the BUILD_DIR"
124
+ exit 1
125
+ end
126
+ end
127
+
128
+ workspace
129
+ end
130
+
131
+ def check_scheme scheme_name
132
+ if scheme_name.blank?
133
+ logger.error "Must provide a scheme by `-s` option when build a workspace"
134
+ exit 1
135
+ end
136
+ end
137
+
138
+ def is_project? path
139
+ File.extname(path) == '.xcodeproj'
140
+ end
141
+
142
+ def is_workspace? path
143
+ File.extname(path) == '.xcworkspace'
144
+ end
145
+
146
+ def zip_app2ipa app_path, ipa_path
147
+ Dir.mktmpdir do |tmpdir|
148
+ Dir.chdir(tmpdir) do
149
+ Dir.mkdir("Payload")
150
+ FileUtils.cp_r(app_path, "Payload")
151
+ system("rm -rf #{ipa_path}") if File.file? ipa_path
152
+ system("zip -qr #{ipa_path} Payload")
153
+ end
154
+ end
155
+ end
156
+
157
+ end
158
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Info
5
+
6
+ def info *args, options
7
+ file_path = File.absolute_path(args.first.to_s)
8
+ is_all = !options[:all].blank?
9
+
10
+ check_supported_file file_path
11
+
12
+ file_type = File.extname(file_path).delete('.')
13
+
14
+ logger.info "Analyzing #{file_type} file......"
15
+ logger_info_dividing_line
16
+
17
+ app_info = send("#{file_type}_info", file_path, is_all)
18
+ app_info.each { |k, v| logger.info "#{k}: #{v}" }
19
+ end
20
+
21
+ def ipa_info ipa_path, is_all
22
+ ipa = Parser::IPA.new(ipa_path)
23
+ app = ipa.app
24
+
25
+ info = {
26
+ type: 'ios',
27
+ identifier: app.identifier,
28
+ name: app.name,
29
+ display_name: app.display_name,
30
+ version: app.version,
31
+ short_version: app.short_version,
32
+ devices: app.devices,
33
+ release_type: app.release_type || ipa.release_type,
34
+ distribution_name: app.distribution_name
35
+ }
36
+
37
+ if is_all
38
+ info[:icons] = []
39
+ app.icons.each do |icon|
40
+ tmp_icon_path = "#{Dir.tmpdir}/icon-#{SecureRandom.hex[4..9]}.png"
41
+ FileUtils.cp(icon, tmp_icon_path)
42
+ info[:icons] << tmp_icon_path
43
+ end
44
+
45
+ app.hide_developer_certificates
46
+
47
+ info[:plist] = app.info
48
+ info[:mobileprovision] = app.mobileprovision
49
+ end
50
+
51
+ ipa.cleanup
52
+ info
53
+ end
54
+
55
+ def apk_info apk_path, is_all
56
+ apk = Android::Apk.new(apk_path)
57
+ info = {
58
+ type: 'android',
59
+ identifier: apk.manifest.package_name,
60
+ name: apk.label,
61
+ version: apk.manifest.version_code,
62
+ short_version: apk.manifest.version_name
63
+ }
64
+
65
+ # apk.icon is a hash, { icon_name: icon_data }
66
+ if is_all
67
+ info[:icons] = []
68
+ apk.icon.each do |name, data|
69
+ tmp_icon_path = "#{Dir.tmpdir}/icon-#{SecureRandom.hex[4..9]}.png"
70
+ File.open(tmp_icon_path, 'w+') { |f| f << data }
71
+ info[:icons] << tmp_icon_path
72
+ end
73
+ end
74
+
75
+ info
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Login
5
+
6
+ def login token
7
+ check_token_cannot_be_blank token
8
+
9
+ user_info = fetch_user_info(token)
10
+
11
+ logger.info "Login succeed, previous user's email: #{config[:email]}" unless config.blank?
12
+ write_config(email: user_info.fetch(:email, ''), token: user_info.fetch(:token, ''))
13
+ reload_config
14
+ logger.info "Login succeed, current user's email: #{config[:email]}"
15
+ end
16
+ end
17
+ end