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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +8 -0
- data/.gitignore +24 -0
- data/.travis.yml +31 -0
- data/CHANGELOG +188 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +10 -0
- data/bin/console +11 -0
- data/bin/fir +14 -0
- data/bin/setup +7 -0
- data/doc/build_apk.md +42 -0
- data/doc/build_ipa.md +66 -0
- data/doc/help.md +34 -0
- data/doc/info.md +44 -0
- data/doc/install.md +65 -0
- data/doc/login.md +19 -0
- data/doc/mapping.md +22 -0
- data/doc/publish.md +35 -0
- data/doc/upgrade.md +7 -0
- data/lib/fir.rb +28 -0
- data/lib/fir/api.yml +13 -0
- data/lib/fir/api.yml.bak +13 -0
- data/lib/fir/cli.rb +195 -0
- data/lib/fir/patches.rb +10 -0
- data/lib/fir/patches/blank.rb +131 -0
- data/lib/fir/patches/concern.rb +146 -0
- data/lib/fir/patches/default_headers.rb +9 -0
- data/lib/fir/patches/hash.rb +79 -0
- data/lib/fir/patches/instance_variables.rb +30 -0
- data/lib/fir/patches/native_patch.rb +28 -0
- data/lib/fir/patches/os_patch.rb +28 -0
- data/lib/fir/patches/try.rb +102 -0
- data/lib/fir/util.rb +87 -0
- data/lib/fir/util/build_apk.rb +76 -0
- data/lib/fir/util/build_common.rb +93 -0
- data/lib/fir/util/build_ipa.rb +240 -0
- data/lib/fir/util/config.rb +42 -0
- data/lib/fir/util/http.rb +30 -0
- data/lib/fir/util/info.rb +39 -0
- data/lib/fir/util/login.rb +18 -0
- data/lib/fir/util/mapping.rb +98 -0
- data/lib/fir/util/me.rb +19 -0
- data/lib/fir/util/parser/apk.rb +43 -0
- data/lib/fir/util/parser/bin/pngcrush +0 -0
- data/lib/fir/util/parser/common.rb +24 -0
- data/lib/fir/util/parser/ipa.rb +188 -0
- data/lib/fir/util/parser/pngcrush.rb +23 -0
- data/lib/fir/util/publish.rb +106 -0
- data/lib/fir/util/publish.rb.bak +185 -0
- data/lib/fir/version.rb +5 -0
- data/lib/fir/xcode_wrapper.sh +29 -0
- data/lib/omt-cli.rb +3 -0
- data/lib/omt_cli.rb +3 -0
- data/omt-cli.gemspec +48 -0
- data/test/build_ipa_test.rb +17 -0
- data/test/cases/test_apk.apk +0 -0
- data/test/cases/test_apk_txt +1 -0
- data/test/cases/test_ipa.ipa +0 -0
- data/test/cases/test_ipa_dsym +0 -0
- data/test/info_test.rb +36 -0
- data/test/login_test.rb +12 -0
- data/test/mapping_test.rb +18 -0
- data/test/me_test.rb +17 -0
- data/test/publish_test.rb +44 -0
- data/test/test_helper.rb +98 -0
- metadata +84 -4
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FIR
|
4
|
+
module Config
|
5
|
+
CONFIG_PATH = "#{ENV['HOME']}/.omt-cli"
|
6
|
+
APP_INFO_PATH = "#{ENV['HOME']}/.omt-cli-app"
|
7
|
+
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
|
+
@token ||= config[:token] if config
|
38
|
+
end
|
39
|
+
|
40
|
+
alias_method :☠, :exit
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module FIR
|
4
|
+
module Http
|
5
|
+
MAX_RETRIES = 5
|
6
|
+
|
7
|
+
%w(get post patch put).each do |_m|
|
8
|
+
class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
9
|
+
def #{_m}(url, params = {})
|
10
|
+
query = :#{_m} == :get ? { params: params } : params
|
11
|
+
begin
|
12
|
+
res = ::RestClient.#{_m}(url, query)
|
13
|
+
rescue => e
|
14
|
+
@retries ||= 0
|
15
|
+
logger.error(e.message.to_s)
|
16
|
+
if @retries < MAX_RETRIES
|
17
|
+
@retries += 1
|
18
|
+
logger.info("Retry \#{@retries} times......")
|
19
|
+
sleep 2
|
20
|
+
retry
|
21
|
+
else
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
JSON.parse(res.body.force_encoding('UTF-8'), symbolize_names: true)
|
26
|
+
end
|
27
|
+
METHOD
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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
|
+
|
29
|
+
ipa.cleanup
|
30
|
+
info
|
31
|
+
end
|
32
|
+
|
33
|
+
def apk_info(apk_path, options = {})
|
34
|
+
apk = FIR::Parser::Apk.new(apk_path)
|
35
|
+
info = apk.full_info(options)
|
36
|
+
info
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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: token)
|
13
|
+
reload_config
|
14
|
+
logger.info "Login succeed, current user's email: #{config[:email]}"
|
15
|
+
logger_info_blank_line
|
16
|
+
end
|
17
|
+
end
|
18
|
+
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
|
data/lib/fir/util/me.rb
ADDED
@@ -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,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative './common'
|
4
|
+
|
5
|
+
module FIR
|
6
|
+
module Parser
|
7
|
+
class Apk
|
8
|
+
include Parser::Common
|
9
|
+
|
10
|
+
def initialize(path)
|
11
|
+
Zip.warn_invalid_date = false
|
12
|
+
@apk = ::Android::Apk.new(path)
|
13
|
+
end
|
14
|
+
|
15
|
+
def full_info(options)
|
16
|
+
if options.fetch(:full_info, false)
|
17
|
+
basic_info.merge!(icons: tmp_icons)
|
18
|
+
end
|
19
|
+
|
20
|
+
basic_info
|
21
|
+
end
|
22
|
+
|
23
|
+
def basic_info
|
24
|
+
@basic_info ||= {
|
25
|
+
type: 'android',
|
26
|
+
identifier: @apk.manifest.package_name,
|
27
|
+
name: @apk.label,
|
28
|
+
build: @apk.manifest.version_code.to_s,
|
29
|
+
version: @apk.manifest.version_name.to_s
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
# @apk.icon is a hash, { icon_name: icon_binary_data }
|
34
|
+
def tmp_icons
|
35
|
+
begin
|
36
|
+
@apk.icon.map { |_, data| generate_tmp_icon(data, :apk) }
|
37
|
+
rescue
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
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
|