ioscmpr 0.1.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.
@@ -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: []