fir-cli-x 1.7.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +8 -0
  3. data/.dockerignore +2 -0
  4. data/.flow-plugin.yml +14 -0
  5. data/.gitignore +27 -0
  6. data/.travis.yml +23 -0
  7. data/CHANGELOG +194 -0
  8. data/Dockerfile +12 -0
  9. data/Gemfile +10 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +63 -0
  12. data/Rakefile +10 -0
  13. data/bin/console +11 -0
  14. data/bin/fir +14 -0
  15. data/bin/setup +7 -0
  16. data/doc/help.md +34 -0
  17. data/doc/info.md +44 -0
  18. data/doc/install.md +67 -0
  19. data/doc/login.md +19 -0
  20. data/doc/publish.md +35 -0
  21. data/doc/upgrade.md +7 -0
  22. data/fir-cli.gemspec +52 -0
  23. data/fir.sh +46 -0
  24. data/install.sh +210 -0
  25. data/lib/fir-cli.rb +3 -0
  26. data/lib/fir.rb +28 -0
  27. data/lib/fir/api.yml +7 -0
  28. data/lib/fir/cli.rb +181 -0
  29. data/lib/fir/patches.rb +10 -0
  30. data/lib/fir/patches/blank.rb +131 -0
  31. data/lib/fir/patches/concern.rb +146 -0
  32. data/lib/fir/patches/default_headers.rb +9 -0
  33. data/lib/fir/patches/hash.rb +79 -0
  34. data/lib/fir/patches/instance_variables.rb +30 -0
  35. data/lib/fir/patches/native_patch.rb +28 -0
  36. data/lib/fir/patches/os_patch.rb +28 -0
  37. data/lib/fir/patches/try.rb +102 -0
  38. data/lib/fir/util.rb +86 -0
  39. data/lib/fir/util/build_apk.rb +77 -0
  40. data/lib/fir/util/build_common.rb +93 -0
  41. data/lib/fir/util/build_ipa.rb +11 -0
  42. data/lib/fir/util/config.rb +43 -0
  43. data/lib/fir/util/http.rb +23 -0
  44. data/lib/fir/util/info.rb +38 -0
  45. data/lib/fir/util/login.rb +17 -0
  46. data/lib/fir/util/mapping.rb +98 -0
  47. data/lib/fir/util/me.rb +19 -0
  48. data/lib/fir/util/parser/apk.rb +46 -0
  49. data/lib/fir/util/parser/bin/pngcrush +0 -0
  50. data/lib/fir/util/parser/common.rb +24 -0
  51. data/lib/fir/util/parser/ipa.rb +188 -0
  52. data/lib/fir/util/parser/pngcrush.rb +23 -0
  53. data/lib/fir/util/publish.rb +253 -0
  54. data/lib/fir/version.rb +5 -0
  55. data/lib/fir/xcode_wrapper.sh +29 -0
  56. data/lib/fir_cli.rb +3 -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 +273 -0
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FIR
4
+ module BuildIpa
5
+ def build_ipa(*_args, _options)
6
+ logger.error "fir build ipa \b功能已过期, 请及时迁移打包部分, 推荐使用 fastlane gym 生成ipa 后再使用 fir-cli 上传"
7
+ end
8
+
9
+ private
10
+ end
11
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Config
5
+ CONFIG_PATH = "#{ENV['HOME']}/.fir-cli"
6
+ APP_INFO_PATH = "#{ENV['HOME']}/.fir-cli-app"
7
+ API_YML_PATH = ENV['API_YML_PATH'] || File.expand_path('../../', __FILE__) + '/api.yml'
8
+ XCODE_WRAPPER_PATH = File.expand_path('../../', __FILE__) + '/xcode_wrapper.sh'
9
+ APP_FILE_TYPE = %w(.ipa .apk).freeze
10
+
11
+ def fir_api
12
+ @fir_api ||= YAML.load_file(API_YML_PATH).deep_symbolize_keys[:fir]
13
+ end
14
+
15
+ def bughd_api
16
+ @bughd_api ||= YAML.load_file(API_YML_PATH).deep_symbolize_keys[:bughd]
17
+ end
18
+
19
+ def config
20
+ return unless File.exist?(CONFIG_PATH)
21
+ @config ||= YAML.load_file(CONFIG_PATH).deep_symbolize_keys
22
+ end
23
+
24
+ def reload_config
25
+ @config = YAML.load_file(CONFIG_PATH).deep_symbolize_keys
26
+ end
27
+
28
+ def write_config(hash)
29
+ File.open(CONFIG_PATH, 'w+') { |f| f << YAML.dump(hash) }
30
+ end
31
+
32
+ def write_app_info(hash)
33
+ File.open(APP_INFO_PATH, 'w+') { |f| f << YAML.dump(hash) }
34
+ end
35
+
36
+ def current_token
37
+ return @token = ENV["API_TOKEN"] if ENV["API_TOKEN"]
38
+ @token ||= config[:token] if config
39
+ end
40
+
41
+ alias_method :☠, :exit
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ require 'api_tools'
5
+
6
+ module FIR
7
+ module Http
8
+ include ApiTools::DefaultRestModule
9
+
10
+ alias old_default_options default_options
11
+ def default_options
12
+ @default_options = old_default_options.merge(timeout: 300)
13
+ if ENV['FIR_TIMEOUT']
14
+ @default_options[:timeout] = ENV['FIR_TIMEOUT'].to_i
15
+ end
16
+ unless ENV['UPLOAD_VERIFY_SSL']
17
+ @default_options.merge!(other_base_execute_option: {
18
+ verify_ssl: OpenSSL::SSL::VERIFY_NONE
19
+ })
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,38 @@
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_file_exist file_path
11
+ check_supported_file file_path
12
+
13
+ file_type = File.extname(file_path).delete('.')
14
+
15
+ logger.info "Analyzing #{file_type} file......"
16
+ logger_info_dividing_line
17
+
18
+ app_info = send("#{file_type}_info", file_path, full_info: is_all)
19
+ app_info.each { |k, v| logger.info "#{k}: #{v}" }
20
+
21
+ logger_info_blank_line
22
+ end
23
+
24
+ def ipa_info(ipa_path, options = {})
25
+ ipa = FIR::Parser::Ipa.new(ipa_path)
26
+ app = ipa.app
27
+ info = app.full_info(options)
28
+ ipa.cleanup
29
+ info
30
+ end
31
+
32
+ def apk_info(apk_path, options = {})
33
+ apk = FIR::Parser::Apk.new(apk_path)
34
+ info = apk.full_info(options)
35
+ info
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,17 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Login
5
+ def login(token)
6
+ check_token_cannot_be_blank token
7
+
8
+ user_info = fetch_user_info(token)
9
+
10
+ logger.info "Login succeed, previous user's email: #{config[:email]}" unless config.blank?
11
+ write_config(email: user_info.fetch(:email, ''), token: token)
12
+ reload_config
13
+ logger.info "Login succeed, current user's email: #{config[:email]}"
14
+ logger_info_blank_line
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Mapping
5
+
6
+ def mapping(*args, options)
7
+ initialize_and_check_mapping_options(args, options)
8
+ check_file_and_token
9
+
10
+ logger.info "Creating bughd project's version......."
11
+ logger_info_dividing_line
12
+
13
+ @full_version = find_or_create_bughd_full_version
14
+
15
+ logger.info 'Uploading mapping file.......'
16
+
17
+ upload_mapping_file
18
+ logger_info_dividing_line
19
+
20
+ logger.info "Uploaded succeed: #{bughd_api[:domain]}/project/#{@proj}/settings"
21
+ logger_info_blank_line
22
+ end
23
+
24
+ def find_or_create_bughd_full_version
25
+ url = bughd_api[:project_url] + "/#{@proj}/full_versions"
26
+ post url, version: @version, build: @build, uuid: uuid
27
+ end
28
+
29
+ def upload_mapping_file
30
+ tmp_file_path = generate_temp_mapping_file
31
+
32
+ url = bughd_api[:full_version_url] + "/#{@full_version[:id]}"
33
+ patch url, file: File.new(tmp_file_path, 'rb'), project_id: @proj, uuid: uuid
34
+ end
35
+
36
+ private
37
+
38
+ def initialize_and_check_mapping_options(args, options)
39
+ @file_path = File.absolute_path(args.first.to_s)
40
+ @token = options[:token] || current_token
41
+ @proj = options[:proj].to_s
42
+ @version = options[:version].to_s
43
+ @build = options[:build].to_s
44
+ end
45
+
46
+ def check_file_and_token
47
+ check_file_exist(@file_path)
48
+ check_token_cannot_be_blank(@token)
49
+ check_project_id_cannot_be_blank
50
+ end
51
+
52
+ def check_project_id_cannot_be_blank
53
+ return unless @proj.blank?
54
+
55
+ logger.error "Project id can't be blank"
56
+ exit 1
57
+ end
58
+
59
+ def uuid
60
+ @uuid ||= fetch_user_uuid(@token)
61
+ end
62
+
63
+ def generate_temp_mapping_file
64
+ tmp_file_path = "#{Dir.tmpdir}/fircli-#{File.basename(@file_path)}"
65
+ FileUtils.cp(@file_path, tmp_file_path)
66
+
67
+ tmp_file_path = zip_mapping_file(tmp_file_path)
68
+ tmp_file_path = dsym_or_txt_file(tmp_file_path)
69
+
70
+ tmp_file_path
71
+ end
72
+
73
+ def zip_mapping_file(tmp_file_path)
74
+ if File.size?(tmp_file_path) > 50 * 1000 * 1000
75
+ logger.info 'Zipping mapping file.......'
76
+
77
+ system("zip -qr #{tmp_file_path}.zip #{tmp_file_path}")
78
+ tmp_file_path += '.zip'
79
+
80
+ logger.info "Zipped Mapping file size - #{File.size?(tmp_file_path)}"
81
+ end
82
+
83
+ tmp_file_path
84
+ end
85
+
86
+ def dsym_or_txt_file(tmp_file_path)
87
+ if File.dsym?(@file_path)
88
+ FileUtils.mv(tmp_file_path, tmp_file_path + '.dSYM')
89
+ tmp_file_path += '.dSYM'
90
+ elsif File.text?(@file_path)
91
+ FileUtils.mv(tmp_file_path, tmp_file_path + '.txt')
92
+ tmp_file_path += '.txt'
93
+ end
94
+
95
+ tmp_file_path
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Me
5
+
6
+ def me
7
+ check_logined
8
+
9
+ user_info = fetch_user_info(current_token)
10
+
11
+ email = user_info.fetch(:email, '')
12
+ name = user_info.fetch(:name, '')
13
+
14
+ logger.info "Login succeed, current user's email: #{email}"
15
+ logger.info "Login succeed, current user's name: #{name}"
16
+ logger_info_blank_line
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './common'
4
+ module FIR
5
+ module Parser
6
+ class Apk
7
+ include Parser::Common
8
+
9
+ def initialize(path)
10
+ Zip.warn_invalid_date = false
11
+ @apk = ::Android::Apk.new(path)
12
+ end
13
+
14
+ def full_info(options)
15
+ basic_info[:icons] = tmp_icons if options.fetch(:full_info, false)
16
+
17
+ basic_info
18
+ end
19
+
20
+ def basic_info
21
+ @basic_info ||= {
22
+ type: 'android',
23
+ name: fetch_label,
24
+ identifier: @apk.manifest.package_name,
25
+ build: @apk.manifest.version_code.to_s,
26
+ version: @apk.manifest.version_name.to_s
27
+ }
28
+ @basic_info.reject! { |_k, v| v.nil? }
29
+ @basic_info
30
+ end
31
+
32
+ # @apk.icon is a hash, { icon_name: icon_binary_data }
33
+ def tmp_icons
34
+ @apk.icon.map { |_, data| generate_tmp_icon(data, :apk) }
35
+ rescue StandardError
36
+ []
37
+ end
38
+
39
+ def fetch_label
40
+ @apk.label
41
+ rescue NoMethodError
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
Binary file
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+
3
+ module FIR
4
+ module Parser
5
+ module Common
6
+
7
+ # when type is ipa, the icon data is a png file.
8
+ # when type is apk, the icon data is a binary data.
9
+ def generate_tmp_icon data, type
10
+ tmp_icon_path = "#{Dir.tmpdir}/icon-#{SecureRandom.hex[4..9]}.png"
11
+
12
+ if type == :ipa
13
+ FileUtils.cp(data, tmp_icon_path)
14
+ elsif type == :apk
15
+ File.open(tmp_icon_path, 'w+') { |f| f << data }
16
+ else
17
+ return
18
+ end
19
+
20
+ tmp_icon_path
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,188 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative './common'
4
+
5
+ module FIR
6
+ module Parser
7
+ class Ipa
8
+ include Parser::Common
9
+
10
+ def initialize(path)
11
+ @path = path
12
+ end
13
+
14
+ def app
15
+ @app ||= App.new(app_path, is_stored)
16
+ end
17
+
18
+ def app_path
19
+ @app_path ||= Dir.glob(File.join(contents, 'Payload', '*.app')).first
20
+ end
21
+
22
+ def cleanup
23
+ return unless @contents
24
+ FileUtils.rm_rf(@contents)
25
+ @contents = nil
26
+ end
27
+
28
+ def metadata
29
+ return unless has_metadata?
30
+ @metadata ||= CFPropertyList.native_types(CFPropertyList::List.new(file: metadata_path).value)
31
+ end
32
+
33
+ def has_metadata?
34
+ File.file?(metadata_path)
35
+ end
36
+
37
+ def metadata_path
38
+ @metadata_path ||= File.join(@contents, 'iTunesMetadata.plist')
39
+ end
40
+
41
+ def is_stored
42
+ has_metadata? ? true : false
43
+ end
44
+
45
+ def contents
46
+ return if @contents
47
+ @contents = "#{Dir.tmpdir}/ipa_files-#{Time.now.to_i}"
48
+
49
+ Zip::File.open(@path) do |zip_file|
50
+ zip_file.each do |f|
51
+ f_path = File.join(@contents, f.name)
52
+ FileUtils.mkdir_p(File.dirname(f_path))
53
+ zip_file.extract(f, f_path) unless File.exist?(f_path)
54
+ end
55
+ end
56
+
57
+ @contents
58
+ end
59
+
60
+ class App
61
+ include Parser::Common
62
+
63
+ def initialize(path, is_stored = false)
64
+ @path = path
65
+ @is_stored = is_stored
66
+ end
67
+
68
+ def full_info(options)
69
+ if options.fetch(:full_info, false)
70
+ basic_info.merge!(icons: tmp_icons)
71
+ end
72
+
73
+ basic_info
74
+ end
75
+
76
+ def basic_info
77
+ @basic_info ||= {
78
+ type: 'ios',
79
+ identifier: identifier,
80
+ name: name,
81
+ display_name: display_name,
82
+ build: version.to_s,
83
+ version: short_version.to_s,
84
+ devices: devices,
85
+ release_type: release_type,
86
+ distribution_name: distribution_name
87
+ }
88
+ end
89
+
90
+ def info
91
+ @info ||= CFPropertyList.native_types(
92
+ CFPropertyList::List.new(file: File.join(@path, 'Info.plist')).value)
93
+ end
94
+
95
+ def name
96
+ info['CFBundleName']
97
+ end
98
+
99
+ def identifier
100
+ info['CFBundleIdentifier']
101
+ end
102
+
103
+ def display_name
104
+ info['CFBundleDisplayName']
105
+ end
106
+
107
+ def version
108
+ info['CFBundleVersion']
109
+ end
110
+
111
+ def short_version
112
+ info['CFBundleShortVersionString']
113
+ end
114
+
115
+ def tmp_icons
116
+ icons.map { |data| generate_tmp_icon(data, :ipa) }
117
+ end
118
+
119
+ def icons
120
+ @icons ||= begin
121
+ icons = []
122
+ info['CFBundleIcons']['CFBundlePrimaryIcon']['CFBundleIconFiles'].each do |name|
123
+ icons << get_image(name)
124
+ icons << get_image("#{name}@2x")
125
+ end
126
+ icons.delete_if &:!
127
+ rescue NoMethodError
128
+ []
129
+ end
130
+ end
131
+
132
+ def mobileprovision
133
+ return unless has_mobileprovision?
134
+ return @mobileprovision if @mobileprovision
135
+
136
+ cmd = "security cms -D -i \"#{mobileprovision_path}\""
137
+ begin
138
+ @mobileprovision = CFPropertyList.native_types(CFPropertyList::List.new(data: `#{cmd}`).value)
139
+ rescue CFFormatError
140
+ @mobileprovision = {}
141
+ end
142
+ end
143
+
144
+ def has_mobileprovision?
145
+ File.file? mobileprovision_path
146
+ end
147
+
148
+ def mobileprovision_path
149
+ @mobileprovision_path ||= File.join(@path, 'embedded.mobileprovision')
150
+ end
151
+
152
+ def hide_developer_certificates
153
+ mobileprovision.delete('DeveloperCertificates') if has_mobileprovision?
154
+ end
155
+
156
+ def devices
157
+ mobileprovision['ProvisionedDevices'] if has_mobileprovision?
158
+ end
159
+
160
+ def distribution_name
161
+ "#{mobileprovision['Name']} - #{mobileprovision['TeamName']}" if has_mobileprovision?
162
+ end
163
+
164
+ def release_type
165
+ if @is_stored
166
+ 'store'
167
+ else
168
+ if has_mobileprovision?
169
+ if devices
170
+ 'adhoc'
171
+ else
172
+ 'inhouse'
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ private
179
+
180
+ def get_image(name)
181
+ path = File.join(@path, "#{name}.png")
182
+ return nil unless File.exist?(path)
183
+ path
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end