fir-cli 0.2.3.1 → 1.0.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.
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