fir-cli-x 1.7.2.1

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 +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