ioscmpr 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7dcaff4f48afc96701e9e1e885e764f99aff5cb283d4059e4b44a3f31420757e
4
+ data.tar.gz: 53bcbcad67e62b03e4cb4fee995120bf1aa2c44df52a058058a7c85a1e8cd48e
5
+ SHA512:
6
+ metadata.gz: aa85d316e9a00dbdd2163c061bb61d833e6c5b9dde6364b197ad44d526576d4e31d42caba5ca8ee49ad7caff51733d256b2a1c779b5d180f55ba11ecc2373805
7
+ data.tar.gz: '0885a5c2de27d7d592e7cc765788ec7ded69945d908572c4bf1eb03b0b7a96a3c05cc784707275f81f6120c2e3d0f99166263853c32845ce88ca24aa60c852e3'
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in ipathin.gemspec
8
+ gemspec
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ioscmpr (0.1.1)
5
+ claide
6
+ colorize
7
+ terminal-table
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ claide (1.0.3)
13
+ colorize (0.8.1)
14
+ rake (10.5.0)
15
+ terminal-table (1.8.0)
16
+ unicode-display_width (~> 1.1, >= 1.1.1)
17
+ unicode-display_width (1.6.0)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ bundler (~> 1.17)
24
+ ioscmpr!
25
+ rake (~> 10.0)
26
+
27
+ BUNDLED WITH
28
+ 1.17.3
@@ -0,0 +1,19 @@
1
+ ## Usage
2
+
3
+ iOS 包瘦身命令行辅助工具:
4
+
5
+ ```
6
+ Usage:
7
+
8
+ $ ioscmpr COMMAND
9
+
10
+ ipa 包瘦身命令行辅助工具.
11
+
12
+ Commands:
13
+
14
+ + car 对比 car
15
+ + ipa 对比 ipa
16
+ + linkmap 对比 linkmap
17
+ + macho 对比 macho
18
+
19
+ ```
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ task default: :spec
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ioscmpr'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ # require "irb"
15
+ # IRB.start(__FILE__)
16
+
17
+ Ioscmpr::Command.run(ARGV)
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ioscmpr/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ioscmpr'
9
+ spec.version = Ioscmpr::VERSION
10
+ spec.authors = ['songruiwang']
11
+ spec.email = ['songruiwang@kuaishou.com']
12
+
13
+ spec.summary = 'Write a short summary, because RubyGems requires one.'
14
+ spec.description = 'Write a longer description or delete this line.'
15
+ spec.homepage = 'https://github.com/tripleCC/ioscmpr'
16
+
17
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
19
+
20
+ # Specify which files should be added to the gem when it is released.
21
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
22
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
23
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
24
+ end
25
+ spec.bindir = 'bin'
26
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'claide'
30
+ spec.add_dependency 'colorize'
31
+ spec.add_dependency 'terminal-table'
32
+ spec.add_development_dependency 'bundler', '~> 1.17'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ioscmpr/command'
4
+ require 'claide'
5
+
6
+ module Ioscmpr
7
+ def root_dir
8
+ __dir__
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'claide'
4
+
5
+ module Ioscmpr
6
+ class Command < CLAide::Command
7
+ require 'ioscmpr/command/car'
8
+ require 'ioscmpr/command/ipa'
9
+ require 'ioscmpr/command/macho'
10
+ require 'ioscmpr/command/linkmap'
11
+
12
+ self.abstract_command = true
13
+ self.command = 'ioscmpr'
14
+
15
+ self.description = 'ipa 包瘦身命令行辅助工具.'
16
+
17
+ def self.options
18
+ super
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'ioscmpr/size_diff'
6
+
7
+ module Ioscmpr
8
+ class Command
9
+ class Car < Command
10
+ self.summary = '对比 car'
11
+ self.description = '对比 car 中各文件差异.'
12
+
13
+ self.arguments = [
14
+ CLAide::Argument.new('PATHS', true, true)
15
+ ]
16
+
17
+ def self.options
18
+ [
19
+ ['--visible-same', '显示大小一致的对比'],
20
+ ['--with-compression', '展示使用不同压缩算法的数据'],
21
+ ['--with-thin', '瘦身后进行对比, 以 iPhoneX, 8.0 为基准 (设备和支持最低系统版本不一样, 瘦身参数也不一, 可以通过 Adhoc 的 App thining 生成的 Packaging.log 查看实际参数)']
22
+ ].concat(super)
23
+ end
24
+
25
+ def initialize(argv)
26
+ @paths = argv.arguments!
27
+ @visible_same = argv.flag?('visible-same')
28
+ @with_compression = argv.flag?('with-compression')
29
+ @with_thin = argv.flag?('with-thin')
30
+ super
31
+ end
32
+
33
+ def validate!
34
+ files = @paths.reject { |path| File.file?(path) }
35
+ help! '必须传入至少一个 car 文件路径' unless @paths.any?
36
+ help! "#{files} 需要为 car 压缩文件" unless files.empty?
37
+ end
38
+
39
+ def run
40
+ prepare
41
+
42
+ @cars = @paths.map do |path|
43
+ output = `xcrun --sdk iphoneos assetutil --info '#{path}'`
44
+ car = CarEntity.new(JSON.parse(output.scrub))
45
+ car
46
+ end
47
+
48
+ clean
49
+
50
+ key = @with_compression ? :unique_name : :name
51
+ names = @cars.map(&:image_sets).flatten.sort_by(&:size).map(&key)
52
+ items_accessor = lambda do |group|
53
+ group.image_sets.each_with_object({}) do |i, hash|
54
+ hash[i.send(key)] = i
55
+ end
56
+ end
57
+
58
+ options = {
59
+ filter_zero_gap_sum: !@visible_same,
60
+ items_accessor: items_accessor,
61
+ item_finder: ->(items, name) { items[name] }
62
+ }
63
+ diff = SizeDiff.new(@cars, names, options)
64
+ diff.run
65
+ end
66
+
67
+ private
68
+
69
+ def work_dir
70
+ File.join(Dir.pwd, '.ioscmpr.car')
71
+ end
72
+
73
+ def prepare
74
+ if @with_thin
75
+ Dir.mkdir(work_dir) unless File.directory?(work_dir)
76
+
77
+ outputs = []
78
+ @paths.each_with_index do |path, index|
79
+ output = File.join(work_dir, "#{index}.car")
80
+ `xcrun --sdk iphoneos assetutil --idiom phone --subtype 570 --scale 3 --display-gamut srgb --graphicsclass MTL2,2 --graphicsclassfallbacks MTL1,2:GLES2,0 --memory 1 --hostedidioms car,watch '#{path}' -o '#{output}'`
81
+ outputs << output
82
+ end
83
+ @paths = outputs
84
+ end
85
+ end
86
+
87
+ def clean
88
+ FileUtils.rm_rf(work_dir) if @with_thin && File.directory?(work_dir)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ module Ioscmpr
95
+ class CarEntity
96
+ attr_reader :image_sets
97
+ attr_reader :assets
98
+ attr_reader :compressions
99
+ attr_reader :size
100
+
101
+ def initialize(car)
102
+ @image_sets = car.group_by { |asset| asset['Name'] }.map do |name, assets|
103
+ assets = assets.map { |asset| CarEntity::Asset.new(asset) }
104
+ image_set = CarEntity::ImageSet.new(name, assets)
105
+ image_set
106
+ end
107
+
108
+ @assets = @image_sets.map(&:assets).flatten
109
+ @compressions = @image_sets.map(&:compressions).uniq
110
+ @size = @image_sets.map(&:size).compact.reduce(&:+)
111
+ end
112
+
113
+ class ImageSet
114
+ attr_reader :name
115
+ attr_reader :size
116
+ attr_reader :assets
117
+
118
+ def initialize(name, assets = [])
119
+ @name = name || ''
120
+ @assets = assets
121
+ @size = assets.map(&:size).compact.reduce(&:+) || 0
122
+ end
123
+
124
+ def unique_name
125
+ rendition_names = assets.map do |asset|
126
+ " #{asset.rendition_name}(#{asset.compression})"
127
+ end.join("\n")
128
+ "#{name}\n#{rendition_names}"
129
+ end
130
+
131
+ def compressions
132
+ assets.map(&:compression).compact.uniq
133
+ end
134
+ end
135
+
136
+ class Asset
137
+ attr_reader :asset_type
138
+ attr_reader :compression
139
+ attr_reader :name
140
+ attr_reader :rendition_name
141
+ attr_reader :scale
142
+ attr_reader :size
143
+ attr_reader :relative_name
144
+
145
+ def initialize(asset)
146
+ @asset = asset
147
+ @size = asset['SizeOnDisk'] || 0
148
+ @asset_type = asset['AssetType']
149
+ @compression = asset['Compression']
150
+ @name = asset['Name'] || ''
151
+ @scale = asset['Scale']
152
+ @rendition_name = asset['RenditionName']
153
+ end
154
+
155
+ def unique_name
156
+ "#{@name}/#{compression}/#{@rendition_name}"
157
+ end
158
+
159
+ def to_s
160
+ @asset
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ioscmpr/size_diff'
4
+
5
+ module Ioscmpr
6
+ class Command
7
+ class Ipa < Command
8
+ self.summary = '对比 ipa'
9
+ self.description = '对比 ipa 中各文件差异.'
10
+
11
+ self.arguments = [
12
+ CLAide::Argument.new('PATHS', true, true)
13
+ ]
14
+
15
+ def self.options
16
+ [
17
+ ['--except-symbols', '不对比符号文件'],
18
+ ['--visible-zero', '显示大小为 0 的文件(单 ipa 生效)'],
19
+ ['--visible-same', '显示大小一致的对比']
20
+ ].concat(super)
21
+ end
22
+
23
+ def initialize(argv)
24
+ @paths = argv.arguments!
25
+ @except_symbols = argv.flag?('except-symbols')
26
+ @visible_zero = argv.flag?('visible-zero')
27
+ @visible_same = argv.flag?('visible-same')
28
+ super
29
+ end
30
+
31
+ def validate!
32
+ files = @paths.reject { |path| File.file?(path) }
33
+ help! '必须传入至少一个 ipa 文件路径' unless @paths.any?
34
+ help! "#{files} 需要为 ipa 压缩文件" unless files.empty?
35
+ end
36
+
37
+ def run
38
+ @ipas = Array(@paths).map { |path| IpaEntity.new(path) }
39
+
40
+ names = @ipas.map(&:files).flatten.sort_by(&:size).reject do |file|
41
+ next true if !@visible_zero && file.size.zero?
42
+
43
+ File.extname(file.name) == '.symbols' && @except_symbols
44
+ end.map(&:name).uniq
45
+
46
+ options = {
47
+ filter_zero_gap_sum: !@visible_same,
48
+ items_accessor: ->(ipa) { ipa.files }
49
+ }
50
+
51
+ diff = SizeDiff.new(@ipas, names, options)
52
+ diff.run
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ module Ioscmpr
59
+ class IpaEntity
60
+ attr_reader :length
61
+ attr_reader :size
62
+ attr_reader :cmpr
63
+ attr_reader :files
64
+
65
+ def initialize(path)
66
+ @path = path
67
+
68
+ analyze_cmpr
69
+ end
70
+
71
+ private
72
+
73
+ def analyze_cmpr
74
+ result = `unzip -lv '#{@path}'`
75
+ lines = result.scrub.split("\n")
76
+ @files = lines.drop(3).reverse.drop(2).reverse.map do |line|
77
+ splited = line.split(' ', 8)
78
+ item = IpaEntity::File.new(splited)
79
+ item
80
+ end
81
+
82
+ @length, @size, @cmpr = lines.last&.split(' ')
83
+ end
84
+
85
+ def to_s
86
+ "#{length} #{size} #{cmpr} #{files.count}"
87
+ end
88
+
89
+ class File
90
+ attr_reader :length
91
+ attr_reader :method
92
+ attr_reader :size
93
+ attr_reader :cmpr
94
+ attr_reader :date
95
+ attr_reader :time
96
+ attr_reader :crc32
97
+ attr_reader :name
98
+
99
+ def initialize(params)
100
+ @length,
101
+ @method,
102
+ @size,
103
+ @cmpr,
104
+ @date,
105
+ @time,
106
+ @crc32,
107
+ @name = params
108
+
109
+ @size = @size.to_i
110
+ @length = @length.to_i
111
+ end
112
+
113
+ def empty?
114
+ length == 0
115
+ end
116
+
117
+ def to_s
118
+ "#{cmpr}\tBefore: #{length}\t\tAfter: #{size} #{name}"
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+ require 'ioscmpr/size_diff'
5
+
6
+ module Ioscmpr
7
+ class Command
8
+ class Linkmap < Command
9
+ PARTS = %w[libraries segments sections object_files symbols].freeze
10
+
11
+ self.summary = '对比 linkmap'
12
+ self.description = '对比 linkmap 中各部分差异.'
13
+
14
+ self.arguments = [
15
+ CLAide::Argument.new('PATHS', true, true)
16
+ ]
17
+
18
+ def self.options
19
+ [
20
+ ['--visible-same', '显示大小一致的对比'],
21
+ ["--parts=#{PARTS.join(',')}", '需要对比的部分']
22
+ ].concat(super)
23
+ end
24
+
25
+ def initialize(argv)
26
+ @paths = argv.arguments!
27
+ @parts = argv.option('parts', PARTS.first).split(',')
28
+ @visible_same = argv.flag?('visible-same')
29
+ super
30
+ end
31
+
32
+ def validate!
33
+ files = @paths.reject { |path| File.file?(path) }
34
+ invalid_parts = @parts.reject { |part| PARTS.find { |dp| dp == part } }
35
+
36
+ help! '必须传入至少一个 linkmap 文件路径' unless @paths.any?
37
+ help! "#{files} 需要为 linkmap 文件" unless files.empty?
38
+ help! "无法对比 linkmap 的 #{invalid_parts} 部分" unless invalid_parts.empty?
39
+ end
40
+
41
+ def run
42
+ @results = @paths.map { |path| LinkmapEntity.new(path).result }
43
+
44
+ @parts.map(&:to_sym).each do |part|
45
+ names = @results.map(&part).flatten.sort_by(&:size).map(&:name).uniq
46
+ sets = @results.map { |r| r.send(part) }
47
+
48
+ items_accessor = lambda do |group|
49
+ group.each_with_object({}) do |item, hash|
50
+ hash[item.name] = item
51
+ end
52
+ end
53
+ options = {
54
+ filter_zero_gap_sum: !@visible_same,
55
+ head: part.to_s.capitalize,
56
+ items_accessor: items_accessor
57
+ }
58
+
59
+ diff = SizeDiff.new(sets, names, options)
60
+ diff.run
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ module Ioscmpr
68
+ class LinkmapEntity
69
+ class Result
70
+ PREFIX = '#'
71
+
72
+ attr_reader :sections
73
+ attr_reader :object_files
74
+ attr_reader :symbols
75
+ attr_reader :segments
76
+
77
+ def initialize
78
+ @sections = []
79
+ @object_file_index_hash = {}
80
+ end
81
+
82
+ def segments
83
+ @segments ||= sections.group_by(&:segment).map do |key, value|
84
+ Segment.new(key, value)
85
+ end.sort_by(&:size).reverse
86
+ end
87
+
88
+ def symbols
89
+ @symbols ||= object_files.map(&:symbols).flatten.uniq.sort_by(&:size).reverse
90
+ end
91
+
92
+ def object_files
93
+ @object_files ||= @object_file_index_hash.values.uniq.sort_by(&:size).reverse
94
+ end
95
+
96
+ def libraries
97
+ @libraries ||= object_files.group_by(&:extra_name).map do |key, value|
98
+ Library.new(key, value)
99
+ end.sort_by(&:size).reverse
100
+ end
101
+
102
+ def add_section(section)
103
+ @sections << section
104
+ end
105
+
106
+ def add_symbol(symbol)
107
+ object_file = @object_file_index_hash[symbol.index]
108
+ object_file.add_symbol(symbol)
109
+ end
110
+
111
+ def add_object_file(object_file)
112
+ @object_file_index_hash[object_file.index] = object_file
113
+ end
114
+
115
+ class Base
116
+ attr_reader :line
117
+
118
+ def initialize(line)
119
+ @line = line
120
+ end
121
+
122
+ def self.from_line(line)
123
+ new line
124
+ end
125
+ end
126
+
127
+ class Library
128
+ attr_reader :object_files
129
+ attr_reader :size
130
+ attr_reader :name
131
+
132
+ def initialize(name, object_files)
133
+ @name = name
134
+ @object_files = object_files || []
135
+ end
136
+
137
+ def add_object_file(object_file)
138
+ @object_files << object_file
139
+ end
140
+
141
+ def size
142
+ @size ||= @object_files.map(&:size).reduce(&:+) || 0
143
+ end
144
+
145
+ def to_s
146
+ "#{name} #{size}"
147
+ end
148
+ end
149
+
150
+ class Segment
151
+ attr_reader :name
152
+ attr_reader :sections
153
+
154
+ def initialize(name, sections)
155
+ @name = name
156
+ @sections = sections
157
+ end
158
+
159
+ def size
160
+ @size ||= sections.map(&:size).reduce(&:+) || 0
161
+ end
162
+
163
+ def to_s
164
+ "#{name} #{size}"
165
+ end
166
+ end
167
+
168
+ class ObjectFile < Base
169
+ DECLARE = 'Object files:'
170
+
171
+ attr_reader :index
172
+ attr_reader :name
173
+ attr_reader :extra_name
174
+ attr_reader :symbols
175
+ attr_reader :size
176
+
177
+ def initialize(line)
178
+ @index = line.match(/\[\s*(\d+)\]/)[1]
179
+ basename = File.basename(line.split(' ').last)
180
+ match_result = basename.match(/(.*)\((.*)\)/)
181
+ if match_result
182
+ @extra_name, @name = match_result[1, 2]
183
+ else
184
+ @extra_name = @name = basename
185
+ end
186
+
187
+ @symbols = []
188
+
189
+ super line
190
+ end
191
+
192
+ def size
193
+ @size ||= @symbols.map(&:size).reduce(&:+) || 0
194
+ end
195
+
196
+ def add_symbol(symbol)
197
+ @symbols << symbol
198
+ end
199
+
200
+ def to_s
201
+ s = "#{index} #{size} #{extra_name}"
202
+ s << " #{name}" unless name == extra_name
203
+ s
204
+ end
205
+ end
206
+
207
+ class Section < Base
208
+ DECLARE = 'Sections:'
209
+
210
+ attr_reader :address
211
+ attr_reader :size
212
+ attr_reader :segment
213
+ attr_reader :name
214
+
215
+ def initialize(line)
216
+ @address,
217
+ @size,
218
+ @segment,
219
+ @name = line.split(' ')
220
+ @size = @size.hex
221
+
222
+ super line
223
+ end
224
+
225
+ def to_s
226
+ @line
227
+ end
228
+ end
229
+
230
+ class Symbol < Base
231
+ DECLARE = 'Symbols:'
232
+
233
+ attr_reader :address
234
+ attr_reader :size
235
+ attr_reader :index
236
+ attr_reader :name
237
+
238
+ def initialize(line)
239
+ first_part, second_part = line.split('[', 2)
240
+ @address, @size = first_part.split(' ')
241
+ @index, @name = second_part.split(']', 2)
242
+ @index = @index.strip
243
+ @has_analyze_oc_method = false
244
+
245
+ super line
246
+ end
247
+
248
+ def class_name
249
+ analyze_oc_method
250
+ @class_name
251
+ end
252
+
253
+ def method_name
254
+ analyze_oc_method
255
+ @method_name
256
+ end
257
+
258
+ def oc_method?
259
+ analyze_oc_method
260
+ @oc_method_analyze_result
261
+ end
262
+
263
+ def instance_method?
264
+ analyze_oc_method
265
+ @is_instance_method
266
+ end
267
+
268
+ def size
269
+ @size.hex || 0
270
+ end
271
+
272
+ def dead?
273
+ @address == '<<dead>>'
274
+ end
275
+
276
+ def to_s
277
+ "#{address} #{size} [#{index}] #{name}"
278
+ end
279
+
280
+ private
281
+
282
+ def analyze_oc_method
283
+ return if @has_analyze_oc_method
284
+
285
+ @oc_method_analyze_result = @name.match(/([+|-])\[(.*)\s(.*)\]/)
286
+ if @oc_method_analyze_result
287
+ op, @class_name, @method_name = @oc_method_analyze_result[1, 3]
288
+ end
289
+ @is_instance_method = op == '-' if op
290
+ @has_analyze_oc_method = true
291
+ end
292
+ end
293
+ end
294
+
295
+ class Parser
296
+ attr_reader :result
297
+
298
+ def initialize(file)
299
+ @file = file
300
+ @result = Result.new
301
+ end
302
+
303
+ def parse
304
+ lines = open(@file).read.scrub.split("\n")
305
+
306
+ declare = nil
307
+ lines.each do |line|
308
+ next if line.empty?
309
+
310
+ if line.start_with?(Result::PREFIX)
311
+ find_result = [
312
+ Result::ObjectFile::DECLARE,
313
+ Result::Section::DECLARE,
314
+ Result::Symbol::DECLARE
315
+ ].find { |dec| line.include?(dec) }
316
+
317
+ declare = find_result if find_result
318
+ next if line.start_with?(Result::PREFIX)
319
+ end
320
+
321
+ case declare
322
+ when Result::ObjectFile::DECLARE
323
+ object_file = Result::ObjectFile.from_line(line)
324
+ result.add_object_file(object_file)
325
+ when Result::Section::DECLARE
326
+ section = Result::Section.from_line(line)
327
+ result.add_section(section)
328
+ when Result::Symbol::DECLARE
329
+ symbol = Result::Symbol.from_line(line)
330
+ result.add_symbol(symbol)
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ def initialize(path)
337
+ @path = path
338
+ end
339
+
340
+ def result
341
+ @result ||= begin
342
+ parser = Parser.new(Pathname.new(@path))
343
+ parser.parse
344
+ parser.result
345
+ end
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ # https://github.com/google/bloaty
4
+ # brew install cmake
5
+ # cmake .
6
+ # make -j6
7
+
8
+ require 'ioscmpr/size_diff'
9
+
10
+ module Ioscmpr
11
+ class Command
12
+ class Macho < Command
13
+ self.summary = '对比 macho'
14
+ self.description = '对比 macho 中各段差异.'
15
+
16
+ self.arguments = [
17
+ CLAide::Argument.new('PATHS', true, true)
18
+ ]
19
+
20
+ def self.options
21
+ [
22
+ ['--visible-same', '显示大小一致的对比'],
23
+ ['--with-thin', '瘦身后进行对比, 以 ARM64 为基准']
24
+ ].concat(super)
25
+ end
26
+
27
+ def initialize(argv)
28
+ @paths = argv.arguments!
29
+ @visible_same = argv.flag?('visible-same')
30
+ @with_thin = argv.flag?('with-thin')
31
+ super
32
+ end
33
+
34
+ def validate!
35
+ files = @paths.reject { |path| File.file?(path) }
36
+ help! '必须传入至少一个 macho 文件路径' unless @paths.any?
37
+ help! "#{files} 需要为 macho 文件" unless files.empty?
38
+ end
39
+
40
+ def run
41
+ prepare
42
+
43
+ @info_pairs = @paths.map { |path| MachoEntity.new(path).info_pair }
44
+ names = @info_pairs.map(&:items).flatten.sort_by(&:size).map(&:name).uniq
45
+
46
+ clean
47
+
48
+ options = {
49
+ filter_zero_gap_sum: !@visible_same,
50
+ items_accessor: ->(info_pair) { info_pair.items },
51
+ calculate_totals: false
52
+ }
53
+
54
+ diff = SizeDiff.new(@info_pairs, names, options)
55
+ diff.run
56
+ end
57
+
58
+ private
59
+
60
+ def work_dir
61
+ File.join(Dir.pwd, '.ioscmpr.macho')
62
+ end
63
+
64
+ def prepare
65
+ if @with_thin
66
+ Dir.mkdir(work_dir) unless File.directory?(work_dir)
67
+
68
+ not_fat_machos = @paths.select do |path|
69
+ archs = `xcrun lipo -archs '#{path}'`.split(' ')
70
+ archs.count <= 1 || !archs.include?('arm64')
71
+ end
72
+ return if not_fat_machos.any?
73
+
74
+ outputs = []
75
+ @paths.each_with_index do |path, index|
76
+ output = File.join(work_dir, index.to_s)
77
+ `xcrun lipo -thin arm64 '#{path}' -output '#{output}'`
78
+ outputs << output
79
+ end
80
+ @paths = outputs
81
+ end
82
+ end
83
+
84
+ def clean
85
+ FileUtils.rm_rf(work_dir) if @with_thin && File.directory?(work_dir)
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ module Ioscmpr
92
+ class MachoEntity
93
+ class Item
94
+ attr_reader :name
95
+ attr_reader :file_size
96
+ attr_reader :vm_size
97
+ alias size file_size
98
+
99
+ def initialize(line)
100
+ @line = line
101
+ @file_per, @file_size, @vm_per, @vm_size, @name =
102
+ line.split(' ', 5)
103
+
104
+ @file_size = transform_size(@file_size)
105
+ @vm_size = transform_size(@vm_size)
106
+ end
107
+
108
+ def transform_size(size)
109
+ if size.end_with?('Ki')
110
+ size.sub('Ki', '').to_i * 1024
111
+ elsif size.end_with?('Mi')
112
+ size.sub('Mi', '').to_i * 1024 * 1024
113
+ else
114
+ size.to_i
115
+ end
116
+ end
117
+
118
+ def to_s
119
+ @line
120
+ end
121
+ end
122
+
123
+ class InfoPair
124
+ attr_reader :items
125
+ attr_reader :name
126
+
127
+ def initialize(name)
128
+ @name = name
129
+ @items = []
130
+ end
131
+
132
+ def add_items(items)
133
+ @items += items
134
+ end
135
+
136
+ def to_s
137
+ "#{name}: \n#{items.join("\n")}"
138
+ end
139
+ end
140
+
141
+ def initialize(path)
142
+ @path = path
143
+ end
144
+
145
+ def info_pair
146
+ @info_pair ||= begin
147
+ info_pair = InfoPair.new(@path)
148
+ items = bloaty(@path).map { |line| Item.new(line) }
149
+ info_pair.add_items(items)
150
+ info_pair
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ def bloaty(_path)
157
+ result = `#{__dir__}/bloaty '#{@path}' -n 0 -s file`.split("\n").reject(&:empty?).drop(2)
158
+ result
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'terminal-table'
4
+ require 'colorize'
5
+
6
+ module Ioscmpr
7
+ class SizeDiff
8
+ class Row
9
+ attr_reader :values
10
+ attr_reader :name
11
+ attr_reader :gaps
12
+ attr_accessor :ref_value
13
+
14
+ def initialize(name)
15
+ @name = name
16
+ @values = []
17
+ end
18
+
19
+ def gaps
20
+ @values.drop(1).map { |value| value - ref_value }
21
+ end
22
+
23
+ def gap_abs_sum
24
+ gaps.map(&:abs).reduce(&:+)
25
+ end
26
+
27
+ def add_value(value)
28
+ @ref_value ||= value
29
+ @values << value
30
+ end
31
+
32
+ def color_gap(gap)
33
+ abs_gap = gap.abs
34
+ if gap.positive?
35
+ "↑#{abs_gap}".red
36
+ elsif gap.negative?
37
+ "↓#{abs_gap}".green
38
+ else
39
+ abs_gap.to_s.white
40
+ end
41
+ end
42
+
43
+ def row
44
+ local_gaps = gaps
45
+ [name, values.first, *values.drop(1).map.with_index { |value, index| format('%-10s [%10s]', value, color_gap(local_gaps[index])) }]
46
+ end
47
+ end
48
+
49
+ DEFAULT_OPTIONS = {
50
+ filter_zero_gap_sum: true,
51
+ sort_by_gap_index: 0,
52
+ calculate_totals: true,
53
+ head: 'Name',
54
+ item_finder: nil,
55
+ items_accessor: nil
56
+ }.freeze
57
+
58
+ def initialize(groups, names, options = DEFAULT_OPTIONS)
59
+ @groups = groups
60
+ @names = names
61
+ init_option_values(options)
62
+ end
63
+
64
+ def init_option_values(options)
65
+ DEFAULT_OPTIONS.each do |key, default|
66
+ value = options.key?(key) ? options[key] : default
67
+ instance_variable_set("@#{key}", value)
68
+ end
69
+ end
70
+
71
+ def run
72
+ table = Terminal::Table.new(headings: [@head] + [*1..@groups.count])
73
+
74
+ groups = @groups.map do |group|
75
+ group = @items_accessor ? @items_accessor.call(group) : group
76
+ group
77
+ end
78
+
79
+ rows = @names.map do |name|
80
+ row = Row.new(name)
81
+
82
+ groups.each do |items|
83
+ item = if @item_finder
84
+ @item_finder.call(items, name)
85
+ else
86
+ if items.is_a?(Hash)
87
+ items[name]
88
+ else
89
+ items.find { |i| i.name == name }
90
+ end
91
+ end
92
+
93
+ size = item&.size || 0
94
+
95
+ row.add_value(size)
96
+ end
97
+
98
+ row
99
+ end
100
+
101
+ if groups.count > 1
102
+ rows = rows.sort do |r1, r2|
103
+ r1.gaps[@sort_by_gap_index]&.abs <=> r2.gaps[@sort_by_gap_index]&.abs
104
+ end
105
+ end
106
+
107
+ rows.each do |row|
108
+ next if row.gap_abs_sum&.zero? && @filter_zero_gap_sum
109
+
110
+ table.add_row(row.row)
111
+ end
112
+
113
+ if @calculate_totals
114
+ totals = []
115
+ rows.each do |row|
116
+ row.values.each_with_index do |value, index|
117
+ totals[index] ||= value
118
+ totals[index] += value
119
+ end
120
+ end
121
+
122
+ row = Row.new('TOTALS')
123
+ totals.each do |value|
124
+ row.add_value(value)
125
+ end
126
+ table.add_row(row.row)
127
+ end
128
+
129
+ puts table
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ioscmpr
4
+ VERSION = '0.1.1'
5
+ end
metadata ADDED
@@ -0,0 +1,129 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ioscmpr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - songruiwang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-02-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: claide
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: colorize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: terminal-table
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.17'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.17'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ description: Write a longer description or delete this line.
84
+ email:
85
+ - songruiwang@kuaishou.com
86
+ executables:
87
+ - ioscmpr
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - Gemfile.lock
94
+ - README.md
95
+ - Rakefile
96
+ - bin/ioscmpr
97
+ - ioscmpr.gemspec
98
+ - lib/ioscmpr.rb
99
+ - lib/ioscmpr/command.rb
100
+ - lib/ioscmpr/command/bloaty
101
+ - lib/ioscmpr/command/car.rb
102
+ - lib/ioscmpr/command/ipa.rb
103
+ - lib/ioscmpr/command/linkmap.rb
104
+ - lib/ioscmpr/command/macho.rb
105
+ - lib/ioscmpr/size_diff.rb
106
+ - lib/ioscmpr/version.rb
107
+ homepage: https://github.com/tripleCC/ioscmpr
108
+ licenses: []
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 3.1.2
126
+ signing_key:
127
+ specification_version: 4
128
+ summary: Write a short summary, because RubyGems requires one.
129
+ test_files: []