danger-app_size_report 0.0.3 → 1.1.0

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.
@@ -2,102 +2,281 @@
2
2
 
3
3
  module Danger
4
4
  require 'json'
5
+ require 'open-uri'
6
+ require 'fileutils'
7
+
5
8
  require_relative '../converter/parser/report_parser'
6
9
  require_relative '../converter/helper/memory_size'
10
+ require_relative '../converter/helper/android_utils'
7
11
 
8
- # A Danger plugin for reporting iOS app size violations.
12
+ # A Danger plugin for reporting iOS and Android app size violations.
13
+ #
14
+ #
9
15
  # A valid App Thinning Size Report must be passed to the plugin
10
- # for accurate functionality.
16
+ # for accurate functionality in case of iOS.
11
17
  #
12
- # @example Report app size violations if one or more App variants
13
- # exceed 4GB.
18
+ # @example Report iOS app size violations if one or more App variants exceed 4GB.
14
19
  #
15
20
  # report_path = "/Path/to/AppSize/Report.txt"
16
- # app_size_report.flag_violations(
21
+ # app_size_report.flag_ios_violations(
17
22
  # report_path,
18
23
  # build_type: 'App',
19
- # size_limit: 4,
24
+ # limit_size: 4,
20
25
  # limit_unit: 'GB',
21
26
  # fail_on_warning: false
22
27
  # )
23
28
  #
24
- # @example Report app size violations if one or more App Clip variants
25
- # exceed 8MB.
29
+ # @example Report iOS app size violations if one or more App Clip variants exceed 8MB.
26
30
  #
27
31
  # report_path = "/Path/to/AppSize/Report.txt"
28
- # app_size_report.flag_violations(
32
+ # app_size_report.flag_ios_violations(
29
33
  # report_path,
30
34
  # build_type: 'Clip',
31
- # size_limit: 8,
35
+ # limit_size: 8,
32
36
  # limit_unit: 'MB',
33
37
  # fail_on_warning: false
34
38
  # )
35
39
  #
36
- # @example Fail PR if one or more App Clip variants exceed 8MB.
40
+ # @example Fail PR if one or more iOS App Clip variants exceed 8MB.
37
41
  #
38
42
  # report_path = "/Path/to/AppSize/Report.txt"
39
- # app_size_report.flag_violations(
43
+ # app_size_report.flag_ios_violations(
40
44
  # report_path,
41
45
  # build_type: 'Clip',
42
- # size_limit: 8,
46
+ # limit_size: 8,
43
47
  # limit_unit: 'MB',
44
48
  # fail_on_warning: true
45
49
  # )
46
50
  #
47
- # @example Get JSON string representation of app thinning size report
51
+ # @example Get JSON string representation of iOS app thinning size report
48
52
  #
49
53
  # report_path = "/Path/to/AppSize/Report.txt"
50
54
  # app_size_json = app_size_report.report_json(report_path)
51
55
  #
56
+ # @example Report Android app size violations if one or more App variants
57
+ #
58
+ # aab_path = "/Path/to/app.aab"
59
+ # ks_path = "/Path/to/keyStore"
60
+ # ks_alias = "KeyAlias"
61
+ # ks_password = "Key Password"
62
+ # ks_alias_password = "Key Alias Password"
63
+ # app_size_report.flag_android_violations(
64
+ # aab_path,
65
+ # ks_path,
66
+ # ks_alias,
67
+ # ks_password,
68
+ # ks_alias_password,
69
+ # screen_densities: ["MDPI", "HDPI", "XHDPI", "XXHDPI", "XXXHDPI"],
70
+ # languages: ["en", "de", "da", "es", "fr", "it", "nb", "nl", "sv"],
71
+ # build_type: 'App',
72
+ # limit_size: 14,
73
+ # limit_unit: 'MB',
74
+ # fail_on_warning: false
75
+ # )
76
+ #
77
+ # @example Report Android Instant app size violations if one or more App variants
78
+ #
79
+ # aab_path = "/Path/to/app.aab"
80
+ # ks_path = "/Path/to/keyStore"
81
+ # ks_alias = "KeyAlias"
82
+ # ks_password = "Key Password"
83
+ # ks_alias_password = "Key Alias Password"
84
+ # app_size_report.flag_android_violations(
85
+ # aab_path,
86
+ # ks_path,
87
+ # ks_alias,
88
+ # ks_password,
89
+ # ks_alias_password,
90
+ # screen_densities: ["MDPI", "HDPI", "XHDPI", "XXHDPI", "XXXHDPI"],
91
+ # languages: ["en", "de", "da", "es", "fr", "it", "nb", "nl", "sv"],
92
+ # build_type: 'Instant',
93
+ # limit_size: 4,
94
+ # limit_unit: 'MB',
95
+ # fail_on_warning: false
96
+ # )
97
+ #
98
+ # @example Fail PR if one or more Android Instant App variants exceed 4MB.
99
+ #
100
+ # aab_path = "/Path/to/app.aab"
101
+ # ks_path = "/Path/to/keyStore"
102
+ # ks_alias = "KeyAlias"
103
+ # ks_password = "Key Password"
104
+ # ks_alias_password = "Key Alias Password"
105
+ # app_size_report.flag_android_violations(
106
+ # aab_path,
107
+ # ks_path,
108
+ # ks_alias,
109
+ # ks_password,
110
+ # ks_alias_password,
111
+ # screen_densities: ["MDPI", "HDPI", "XHDPI", "XXHDPI", "XXXHDPI"],
112
+ # languages: ["en", "de", "da", "es", "fr", "it", "nb", "nl", "sv"],
113
+ # build_type: 'Instant',
114
+ # limit_size: 4,
115
+ # limit_unit: 'MB',
116
+ # fail_on_warning: true
117
+ # )
118
+ #
52
119
  # @see ChargePoint/danger-app_size_report
53
120
  # @tags ios, xcode, appclip, thinning, size
54
121
  #
55
122
  class DangerAppSizeReport < Plugin
56
- # Reports app size violations given a valid App Thinning Size Report.
57
- # @param [String, required] report_path
58
- # Path to valid App Thinning Size Report text file.
59
- # @param [String, optional] build_type
60
- # Specify whether the report corresponds to an App or an App Clip.
61
- # Default: 'App'
62
- # Supported values: 'App', 'Clip'
63
- # @param [Numeric, optional] size_limit
64
- # Specify the app size limit.
65
- # Default: 4
66
- # @param [String, optional] limit_unit
67
- # Specific the unit for the given size limit.
68
- # Default: 'GB'
69
- # Supported values: 'KB', 'MB', 'GB'
70
- # @param [Boolean, optional] fail_on_warning
71
- # Specify whether the PR should fail if one or more app variants
72
- # exceed the given size limit. By default, the plugin issues
73
- # a warning in this case.
74
- # Default: 'false'
123
+ # Reports IOS app size violations given a valid App Thinning Size Report.
124
+ # The plugin also warns users about variants that exceed the optimal size
125
+ # limit for cellular downloads (200 MB).
126
+ # @overload flag_ios_violations(report_path, build_type, limit_size, limit_unit, fail_on_warning)
127
+ # @param [String, required] report_path
128
+ # Path to valid App Thinning Size Report text file.
129
+ # @param [String, optional] build_type
130
+ # Specify whether the report corresponds to an App or an App Clip.
131
+ # Default: 'App'
132
+ # Supported values: 'App', 'Clip'
133
+ # @param [Numeric, optional] limit_size
134
+ # Specify the app size limit. If the build type is set to 'Clip' and the
135
+ # specified app size limit exceeds 10 MB, the 10 MB limit will be enforced
136
+ # to meet Apple's App Clip size requirements.
137
+ # Default: 4
138
+ # @param [String, optional] limit_unit
139
+ # Specific the unit for the given size limit.
140
+ # Default: 'GB'
141
+ # Supported values: 'KB', 'MB', 'GB'
142
+ # @param [Boolean, optional] fail_on_warning
143
+ # Specify whether the PR should fail if one or more app variants
144
+ # exceed the given size limit. By default, the plugin issues
145
+ # a warning in this case.
146
+ # Default: 'false'
75
147
  # @return [void]
76
148
  #
77
- def flag_violations(report_path, build_type: 'App', size_limit: 4, limit_unit: 'GB', fail_on_warning: false)
149
+ def flag_ios_violations(report_path, **kargs)
150
+ supported_kargs = %i[build_type limit_size limit_unit size_limit fail_on_warning]
151
+
152
+ # Identify any unsupported arguments passed to method
153
+ unsupported_kargs = kargs.keys - supported_kargs
154
+
155
+ raise ArgumentError, "The argument '#{unsupported_kargs[0]}' is not supported by flag_ios_violations" if unsupported_kargs.count == 1
156
+
157
+ raise ArgumentError, "The arguments #{unsupported_kargs} are not supported by flag_ios_violations" if unsupported_kargs.count > 1
158
+
159
+ # Set up optional arguments with default values if needed
160
+ build_type = kargs[:build_type] || 'App'
161
+ limit_size = kargs[:limit_size] || kargs[:size_limit] || 4
162
+ limit_unit = kargs[:limit_unit] || 'GB'
163
+ fail_on_warning = kargs[:fail_on_warning] || false
164
+
78
165
  report_text = File.read(report_path)
79
166
  variants = ReportParser.parse(report_text)
80
167
 
81
- unless %w[App Clip].include? build_type
82
- raise ArgumentError, "The 'build_type' argument only accepts the values \"App\" and \"Clip\""
83
- end
168
+ raise ArgumentError, "The 'build_type' argument only accepts the values \"App\" and \"Clip\"" unless %w[App Clip].include? build_type
84
169
 
85
- raise ArgumentError, "The 'size_limit' argument only accepts numeric values" unless size_limit.is_a? Numeric
170
+ if kargs[:limit_size]
171
+ raise ArgumentError, "The 'limit_size' argument only accepts numeric values" unless limit_size.is_a? Numeric
172
+ elsif kargs[:size_limit]
173
+ raise ArgumentError, "The 'size_limit' argument only accepts numeric values" unless limit_size.is_a? Numeric
174
+ end
86
175
 
87
176
  limit_unit.upcase!
88
- unless %w[KB MB GB].include? limit_unit
89
- raise ArgumentError, "The 'build_type' argument only accepts the values \"KB\", \"MB\" and \"GB\""
90
- end
177
+ raise ArgumentError, "The 'build_type' argument only accepts the values \"KB\", \"MB\" and \"GB\"" unless %w[KB MB GB].include? limit_unit
91
178
 
92
- unless [true, false].include? fail_on_warning
93
- raise ArgumentError, "The 'fail_on_warning' argument only accepts the values 'true' and 'false'"
94
- end
179
+ raise ArgumentError, "The 'fail_on_warning' argument only accepts the values 'true' and 'false'" unless [true, false].include? fail_on_warning
95
180
 
96
- generate_size_report_markdown(variants, build_type, size_limit, limit_unit, fail_on_warning)
181
+ generate_size_report_markdown(variants, build_type, limit_size, limit_unit, fail_on_warning)
97
182
  generate_variant_descriptors_markdown(variants)
98
183
  generate_ads_label_markdown
99
184
  end
100
185
 
186
+ # Reports Android app size violations given a valid AAB.
187
+ # @overload flag_android_violations(aab_path, ks_path, ks_alias, ks_password, ks_alias_password, screen_densities, languages, build_type, limit_size, size_limit, limit_unit, fail_on_warning)
188
+ # @param [String, required] aab_path
189
+ # Path to valid AAB file.
190
+ # @param [String, required] ks_path
191
+ # Path to valid signing key file.
192
+ # @param [String, required] ks_alias
193
+ # Alias of signing key.
194
+ # @param [String, required] ks_password
195
+ # Password of signing key.
196
+ # @param [String, required] ks_alias_password
197
+ # Alias Password of signing key.
198
+ # @param [Array, optional] screen_densities
199
+ # Array of screen densities to check APK size.
200
+ # Default: ["MDPI", "HDPI", "XHDPI", "XXHDPI", "XXXHDPI"]
201
+ # @param [Array, optional] languages
202
+ # Array of languages to check APK size.
203
+ # Default: ["en"]
204
+ # @param [String, optional] build_type
205
+ # Specify whether the report corresponds to an App, Instant.
206
+ # Default: 'App'
207
+ # Supported values: 'App', 'Instant'
208
+ # @param [Numeric, optional] limit_size
209
+ # Specify the app size limit. If the build type is set to 'Instant' and the
210
+ # specified app size limit exceeds 4 MB, the 4 MB limit will be enforced to
211
+ # meet Android Instant App size requirements.
212
+ # Default: 150
213
+ # @param [String, optional] limit_unit
214
+ # Specific the unit for the given size limit.
215
+ # Default: 'MB'
216
+ # Supported values: 'KB', 'MB', 'GB'
217
+ # @param [Boolean, optional] fail_on_warning
218
+ # Specify whether the PR should fail if one or more app variants
219
+ # exceed the given size limit. By default, the plugin issues
220
+ # a warning in this case.
221
+ # Default: 'false'
222
+ # @return [void]
223
+ #
224
+ def flag_android_violations(aab_path, ks_path, ks_alias, ks_password, ks_alias_password, **kargs)
225
+ project_root = Dir.pwd
226
+ temp_path = "#{project_root}/temp"
227
+ apks_path = "#{temp_path}/output.apks"
228
+ size_csv_path = "#{temp_path}/output.csv"
229
+ bundletool_path = "#{temp_path}/bundletool.jar"
230
+ bundletool_version = '1.8.2'
231
+ variants_limit = 25
232
+
233
+ supported_kargs = %i[screen_densities languages build_type limit_size size_limit limit_unit fail_on_warning]
234
+ unsupported_kargs = kargs.keys - supported_kargs
235
+
236
+ raise ArgumentError, "The argument #{unsupported_kargs[0]} is not supported by flag_android_violations" if unsupported_kargs.count == 1
237
+
238
+ raise ArgumentError, "The arguments #{unsupported_kargs} are not supported by flag_android_violations" if unsupported_kargs.count > 1
239
+
240
+ # Set up optional arguments with default values if needed
241
+ screen_densities = kargs[:screen_densities] || %w[MDPI HDPI XHDPI XXHDPI XXXHDPI]
242
+ languages = kargs[:languages] || ['en']
243
+ build_type = kargs[:build_type] || 'App'
244
+ limit_size = kargs[:limit_size] || kargs[:size_limit] || 150
245
+ limit_unit = kargs[:limit_unit] || 'MB'
246
+ fail_on_warning = kargs[:fail_on_warning] || false
247
+
248
+ raise ArgumentError, "The 'build_type' argument only accepts the values \"App\" and \"Instant\"" unless %w[App Instant].include? build_type
249
+
250
+ if kargs[:limit_size]
251
+ raise ArgumentError, "The 'limit_size' arguments only accepts numeric values" unless limit_size.is_a? Numeric
252
+ elsif kargs[:size_limit]
253
+ raise ArgumentError, "The 'size_limit' argument only accepts numeric values" unless limit_size.is_a? Numeric
254
+ end
255
+
256
+ limit_unit.upcase!
257
+ raise ArgumentError, "The 'limit_unit' argument only accepts the values \"KB\", \"MB\" and \"GB\"" unless %w[KB MB GB].include? limit_unit
258
+
259
+ raise ArgumentError, "The 'fail_on_warning' argument only accepts the values 'true' and 'false'" unless [true, false].include? fail_on_warning
260
+
261
+ create_temp_dir(temp_path)
262
+
263
+ unless AndroidUtils.download_bundletool(bundletool_version, bundletool_path)
264
+ clean_temp!
265
+ return
266
+ end
267
+
268
+ AndroidUtils.generate_apks(aab_path, ks_path, ks_alias, ks_password, ks_alias_password, apks_path,
269
+ bundletool_path)
270
+ AndroidUtils.generate_estimated_sizes(apks_path, size_csv_path, bundletool_path, build_type)
271
+ filtered_sizes = AndroidUtils.filter_estimated_sizes(size_csv_path, screen_densities, languages)
272
+ sorted_sizes = AndroidUtils.sort_estimated_sizes(filtered_sizes)
273
+
274
+ clean_temp!(temp_path)
275
+
276
+ generate_android_size_report_markdown(sorted_sizes, build_type, limit_size, limit_unit, fail_on_warning,
277
+ variants_limit)
278
+ end
279
+
101
280
  # Returns a JSON string representation of the given App Thinning Size Report.
102
281
  # @param [String, required] report_path
103
282
  # Path to valid App Thinning Size Report text file.
@@ -111,14 +290,105 @@ module Danger
111
290
 
112
291
  private
113
292
 
293
+ def create_temp_dir(temp_path)
294
+ Dir.mkdir temp_path
295
+ end
296
+
297
+ def clean_temp!(temp_path)
298
+ FileUtils.rm_rf(temp_path)
299
+ end
300
+
301
+ def generate_android_size_report_markdown(sorted_sizes, build_type, size_limit, limit_unit, fail_on_warning, variants_limit)
302
+ limit_size = MemorySize.new("#{size_limit}#{limit_unit}")
303
+
304
+ if build_type == 'Instant' && limit_size.megabytes > 4
305
+ message "The size limit was set to 4 MB as the given limit of #{size_limit} #{limit_unit} exceeds Android Instant App size restrictions"
306
+ size_limit = 4
307
+ limit_unit = 'MB'
308
+ limit_size.kilobytes = 4 * 1024
309
+ elsif build_type == 'App' && limit_size.megabytes > 150
310
+ message "The size limit was set to 150 MB as the given limit of #{size_limit} #{limit_unit} exceeds Android App size restrictions"
311
+ size_limit = 150
312
+ limit_unit = 'MB'
313
+ limit_size.kilobytes = 150 * 1024
314
+ end
315
+
316
+ violation_count = AndroidUtils.violations_count(sorted_sizes, limit_size.bytes)
317
+
318
+ if violation_count.positive?
319
+ if fail_on_warning
320
+ failure "The size limit of #{size_limit} #{limit_unit.upcase} has been exceeded by #{violation_count} variants"
321
+ else
322
+ warn "The size limit of #{size_limit} #{limit_unit.upcase} has been exceeded by #{violation_count} variants"
323
+ end
324
+ end
325
+
326
+ exceed_size_report = "| Under Limit | SDK | ABI | Screen Density | Language | Size (Bytes) |\n"
327
+ exceed_size_report << "| :-: | :-: | :-: | :-: | :-: | :-: |\n"
328
+
329
+ more_exceed_size_report = "| Under Limit | SDK | ABI | Screen Density | Language | Size (Bytes) |\n"
330
+ more_exceed_size_report << "| :-: | :-: | :-: | :-: | :-: | :-: |\n"
331
+
332
+ under_size_report = "| Under Limit | SDK | ABI | Screen Density | Language | Size (Bytes) |\n"
333
+ under_size_report << "| :-: | :-: | :-: | :-: | :-: | :-: |\n"
334
+
335
+ counter = sorted_sizes.length - 1
336
+ exceed_counter = 1
337
+ under_counter = 1
338
+
339
+ while counter >= 0
340
+ variant = sorted_sizes[counter]
341
+ is_violating = variant.max > limit_size.bytes ? '❌' : '✅'
342
+ variant_report = "#{is_violating} | #{variant.sdk} | #{variant.abi} | #{variant.screen_density} | #{variant.language} | #{variant.max} |\n"
343
+
344
+ if variant.max > limit_size.bytes
345
+ if exceed_counter <= variants_limit
346
+ exceed_counter += 1
347
+ exceed_size_report << variant_report
348
+ else
349
+ more_exceed_size_report << variant_report
350
+ end
351
+ elsif under_counter <= variants_limit
352
+ under_counter += 1
353
+ under_size_report << variant_report
354
+ end
355
+
356
+ counter -= 1
357
+ end
358
+
359
+ size_report = "# Android #{build_type} Size Report\n"
360
+ size_report << "### Size limit = #{size_limit} #{limit_unit.upcase}\n\n"
361
+
362
+ if violation_count.positive?
363
+ size_report << "## Variants exceeding the size limit\n\n"
364
+ size_report << exceed_size_report
365
+ size_report << "\n"
366
+
367
+ if violation_count > variants_limit
368
+ size_report << "<details>\n<summary>Click to view more violating variants!</summary>\n\n"
369
+ size_report << more_exceed_size_report
370
+ size_report << "</details>\n\n"
371
+ end
372
+ end
373
+
374
+ size_report << "## Variants under or equal to the size limit\n\n"
375
+ size_report << "<details>\n<summary>Click to expand!</summary>\n\n"
376
+
377
+ size_report << under_size_report
378
+ size_report << "</details>\n"
379
+
380
+ markdown size_report
381
+ end
382
+
114
383
  def generate_size_report_markdown(variants, build_type, size_limit, limit_unit, fail_on_warning)
115
384
  limit_size = MemorySize.new("#{size_limit}#{limit_unit}")
385
+ cellular_limit_size = MemorySize.new('200 MB')
116
386
 
117
- if build_type == 'Clip' && limit_size.megabytes > 10
118
- message "The size limit was set to 10 MB as the given limit of #{size_limit} #{limit_unit} exceeds Apple's App Clip size restrictions"
119
- size_limit = 10
387
+ if build_type == 'Clip' && limit_size.megabytes > 15
388
+ message "The size limit was set to 15 MB as the given limit of #{size_limit} #{limit_unit} exceeds Apple's App Clip size restrictions"
389
+ size_limit = 15
120
390
  limit_unit = 'MB'
121
- limit_size.kilobytes = 10 * 1024
391
+ limit_size.kilobytes = 15 * 1024
122
392
  elsif build_type == 'App' && limit_size.gigabytes > 4
123
393
  message "The size limit was set to 4 GB as the given limit of #{size_limit} #{limit_unit} exceeds Apple's App size restrictions"
124
394
  size_limit = 4
@@ -127,10 +397,10 @@ module Danger
127
397
  end
128
398
 
129
399
  flagged_variant_names = []
400
+ flagged_cellular_variant_names = []
130
401
  variants.each do |variant|
131
- if variant.app_size.uncompressed.value > limit_size.megabytes || variant.on_demand_resources_size.uncompressed.value > limit_size.megabytes
132
- flagged_variant_names.append(variant.variant)
133
- end
402
+ flagged_variant_names.append(variant.variant) if variant.app_size.uncompressed.value > limit_size.megabytes || variant.on_demand_resources_size.uncompressed.value > limit_size.megabytes
403
+ flagged_cellular_variant_names.append(variant.variant) if variant.app_size.uncompressed.value > cellular_limit_size.megabytes || variant.on_demand_resources_size.uncompressed.value > cellular_limit_size.megabytes
134
404
  end
135
405
 
136
406
  if flagged_variant_names.length.positive?
@@ -141,21 +411,25 @@ module Danger
141
411
  end
142
412
  end
143
413
 
414
+ warn 'The optimal cellular size limit of 200 MB has been exceeded by one or more variants' if flagged_cellular_variant_names.length.positive?
415
+
144
416
  size_report = "# App Thinning Size Report\n"
145
417
  size_report << "### Size limit = #{size_limit} #{limit_unit.upcase}\n\n"
146
- size_report << "| Under Limit | Variant | App Size - Compressed | App Size - Uncompressed | ODR Size - Compressed | ODR Size - Uncompressed |\n"
147
- size_report << "| :-: | :-: | :-: | :-: | :-: | :-: |\n"
418
+ size_report << "| Under Limit | Variant | Cellular Friendly | App Size - Compressed | App Size - Uncompressed | ODR Size - Compressed | ODR Size - Uncompressed |\n"
419
+ size_report << "| :-: | :-: | :-: | :-: | :-: | :-: | :-:|\n"
148
420
 
149
421
  flagged_variants_set = flagged_variant_names.to_set
422
+ flagged_cellular_variants_set = flagged_cellular_variant_names.to_set
150
423
 
151
424
  variants.each do |variant|
152
425
  is_violating = flagged_variants_set.include?(variant.variant) ? '❌' : '✅'
426
+ is_cellular_friendly = flagged_cellular_variants_set.include?(variant.variant) ? '❌' : '✅'
153
427
  app_size_compressed = "#{variant.app_size.compressed.value} #{variant.app_size.compressed.unit}"
154
428
  app_size_uncompressed = "#{variant.app_size.uncompressed.value} #{variant.app_size.uncompressed.unit}"
155
429
  odr_size_compressed = "#{variant.on_demand_resources_size.compressed.value} #{variant.on_demand_resources_size.compressed.unit}"
156
430
  odr_size_uncompressed = "#{variant.on_demand_resources_size.uncompressed.value} #{variant.on_demand_resources_size.uncompressed.unit}"
157
431
 
158
- size_report << "#{is_violating} | #{variant.variant} | #{app_size_compressed} | #{app_size_uncompressed} | #{odr_size_compressed} | #{odr_size_uncompressed} |\n"
432
+ size_report << "#{is_violating} | #{variant.variant} | #{is_cellular_friendly} | #{app_size_compressed} | #{app_size_uncompressed} | #{odr_size_compressed} | #{odr_size_uncompressed} |\n"
159
433
  end
160
434
 
161
435
  markdown size_report
@@ -178,7 +452,7 @@ module Danger
178
452
  end
179
453
 
180
454
  def generate_ads_label_markdown
181
- ads_label = 'Powered by [danger-app_size_report](https://github.com/ChargePoint)'
455
+ ads_label = 'Powered by [danger-app_size_report](https://github.com/ChargePoint/danger-app_size_report)'
182
456
  markdown ads_label
183
457
  end
184
458
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require 'open3'
5
+
6
+ require_relative '../models/android_variant_model'
7
+
8
+ # Used to obtain and parse Android app size information
9
+ class AndroidUtils
10
+ def self.filter_estimated_sizes(path, screen_densities, languages)
11
+ filtered_sizes = []
12
+ CSV.foreach(path, headers: true) do |row|
13
+ sdk = row[AndroidVariant::PARSING_KEYS[:sdk]]
14
+ abi = row[AndroidVariant::PARSING_KEYS[:abi]]
15
+ screen_density = row[AndroidVariant::PARSING_KEYS[:screen_density]]
16
+ language = row[AndroidVariant::PARSING_KEYS[:language]]
17
+ texture_compression_format = row[AndroidVariant::PARSING_KEYS[:texture_compression_format]]
18
+ device_tire = row[AndroidVariant::PARSING_KEYS[:device_tire]]
19
+ min = row[AndroidVariant::PARSING_KEYS[:min]]
20
+ max = row[AndroidVariant::PARSING_KEYS[:max]]
21
+
22
+ if screen_densities.include?(screen_density) && languages.include?(language)
23
+ filtered_sizes << AndroidVariant.new(sdk, abi, screen_density, language, texture_compression_format,
24
+ device_tire, min, max)
25
+ end
26
+ end
27
+ filtered_sizes
28
+ end
29
+
30
+ def self.sort_estimated_sizes(size_arr)
31
+ size_arr.sort { |o1, o2| o1.max <=> o2.max }
32
+ end
33
+
34
+ def self.violations_count(sorted_arr, limit)
35
+ lb = -1
36
+ ub = sorted_arr.length
37
+ while ub - lb > 1
38
+ mid = (lb + ub) / 2
39
+ if sorted_arr[mid].max <= limit
40
+ lb = mid
41
+ else
42
+ ub = mid
43
+ end
44
+ end
45
+ sorted_arr.length - ub
46
+ end
47
+
48
+ def self.generate_apks(aab_path, ks_path, ks_alias, ks_password, ks_alias_password, apks_path, bundletool_path)
49
+ keystore_params = "--ks=\"#{ks_path}\" --ks-key-alias=\"#{ks_alias}\" --ks-pass=\"pass:#{ks_password}\" --key-pass=\"pass:#{ks_alias_password}\""
50
+ cmd = "java -jar #{bundletool_path} build-apks --bundle=\"#{aab_path}\" --output=\"#{apks_path}\" #{keystore_params}"
51
+ Open3.popen3(cmd) do |_, _, stderr, wait_thr|
52
+ exit_status = wait_thr.value
53
+ raise stderr.read unless exit_status.success?
54
+ end
55
+ end
56
+
57
+ def self.generate_estimated_sizes(apks_path, size_csv_path, bundletool_path, build_type)
58
+ bundletool_cmd = "java -jar #{bundletool_path} get-size total --apks=\"#{apks_path}\" --dimensions=ALL"
59
+ bundletool_cmd += ' --instant' if build_type == 'Instant'
60
+ cmd = "#{bundletool_cmd} > #{size_csv_path}"
61
+ Open3.popen3(cmd) do |_, _, stderr, wait_thr|
62
+ exit_status = wait_thr.value
63
+ raise stderr.read unless exit_status.success?
64
+ end
65
+ end
66
+
67
+ def self.download_bundletool(version, bundletool_path)
68
+ URI.open("https://github.com/google/bundletool/releases/download/#{version}/bundletool-all-#{version}.jar") do |bundletool|
69
+ File.binwrite(bundletool_path, bundletool.read)
70
+ end
71
+ rescue OpenURI::HTTPError
72
+ false
73
+ end
74
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'json'
4
4
 
5
+ # Generate pretty JSON from list of variants
5
6
  class JSONConverter
6
7
  def to_json(_options = {})
7
8
  hash = {}
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  require_relative '../helper/json_converter'
4
+ # Defines memory size object to be used to parse the App Thinning Size Report
4
5
  class MemorySize < JSONConverter
5
6
  attr_accessor :kilobytes
6
7
 
@@ -4,6 +4,7 @@ require_relative '../parser/variant_parser'
4
4
  require_relative '../parser/variant_descriptor_parser'
5
5
  require_relative '../parser/app_size_parser'
6
6
 
7
+ # Used to parse sections of the App Thinning Size Report using different parsers
7
8
  class ResultFactory
8
9
  def self.parse(from_text: '', parser: nil)
9
10
  result = nil
@@ -16,15 +17,7 @@ class ResultFactory
16
17
  variant_descriptor_parser = VariantDescriptorParser.new(from_text)
17
18
  variant_descriptor_parser.parse
18
19
  result = variant_descriptor_parser.result
19
- when :app_on_demand_resources_size
20
- app_size_parser = AppSizeParser.new(from_text)
21
- app_size_parser.parse
22
- result = app_size_parser.result
23
- when :app_size
24
- app_size_parser = AppSizeParser.new(from_text)
25
- app_size_parser.parse
26
- result = app_size_parser.result
27
- when :on_demand_resources_size
20
+ when :app_on_demand_resources_size, :app_size, :on_demand_resources_size
28
21
  app_size_parser = AppSizeParser.new(from_text)
29
22
  app_size_parser.parse
30
23
  result = app_size_parser.result
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Android variant model
4
+ class AndroidVariant
5
+ attr_reader :sdk, :abi, :screen_density, :language, :texture_compression_format, :device_tire, :min, :max
6
+
7
+ PARSING_KEYS = {
8
+ sdk: 'SDK',
9
+ abi: 'ABI',
10
+ screen_density: 'SCREEN_DENSITY',
11
+ language: 'LANGUAGE',
12
+ texture_compression_format: 'TEXTURE_COMPRESSION_FORMAT',
13
+ device_tire: 'DEVICE_TIER',
14
+ min: 'MIN',
15
+ max: 'MAX'
16
+ }.freeze
17
+
18
+ def initialize(sdk, abi, screen_density, language, texture_compression_format, device_tire, min, max)
19
+ @sdk = sdk
20
+ @abi = abi
21
+ @screen_density = screen_density
22
+ @language = language
23
+ @texture_compression_format = texture_compression_format
24
+ @device_tire = device_tire
25
+ @min = min.to_i
26
+ @max = max.to_i
27
+ end
28
+ end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helper/json_converter'
4
+
5
+ # App Size Model.
6
+ # Example: 'App size: 6.6 MB compressed, 12.9 MB uncompressed'
4
7
  class AppSizeModel < JSONConverter
5
8
  attr_reader :compressed, :uncompressed
6
9
 
@@ -16,6 +19,7 @@ class AppSizeModel < JSONConverter
16
19
  end
17
20
  end
18
21
 
22
+ # Size Model
19
23
  class SizeModel < JSONConverter
20
24
  attr_reader :raw_value, :value, :unit
21
25
 
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helper/json_converter'
4
+
5
+ # Device Model.
6
+ # Example: 'device: iPhone10,3, os-version: 14.0'
4
7
  class DeviceModel < JSONConverter
5
8
  attr_reader :device, :os_version
6
9