emerge 0.7.0 → 0.7.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f526af7bee28fe19586eb432339f8b6d771e98c9127a5126a4be9f47a32d6ab7
4
- data.tar.gz: eb356b6d63d1e66595550516aa78c36d7926b867ac5caf9a35000f0c8b3feeb5
3
+ metadata.gz: 65f13db01f807a85200615394b091f5bfea4cd3bf4cf4f94ec0a742d5b93c597
4
+ data.tar.gz: 322a6582fd941518f080ae8d38af3555a404e295a99658083d5032a0127640d9
5
5
  SHA512:
6
- metadata.gz: 5e3ba42f1367adf1b50e0bb236b79dc58225df5bd29e7e3af419775cac52e2d04415d5c949b0a62f2eb17f0ae5d25bd82cc086f194d34da0f912dd35af0e433a
7
- data.tar.gz: adf35a55cf1e66d9b0de879e8bab9843c2da10d3a00784dc2eebab7f865ac6ca501be7f81a28c660c6de60ff6ffb4f8e0670fbaeaf4057a67c943424759c8855
6
+ metadata.gz: 6e7adb6bafb19cec3408a3e866d9a148ebe99cf80dde147c47f9133fad0a6d8e7e655a2bfdce7ece4be9596235c650d7abf60ef07f899f30a0a700ef40eccb03
7
+ data.tar.gz: 9ee165532fb1443fda2f5dce215bb545b9a9997d6684cc5e36d3b18de416a73496779dad9685fac1a03735d327077f040dcd4b17050027abe1accaf7ec1562f8
@@ -3,6 +3,7 @@ require 'cfpropertylist'
3
3
  require 'zip'
4
4
  require 'rbconfig'
5
5
  require 'tmpdir'
6
+ require 'tty-prompt'
6
7
 
7
8
  module EmergeCLI
8
9
  module Commands
@@ -13,7 +14,7 @@ module EmergeCLI
13
14
 
14
15
  option :api_token, type: :string, required: false,
15
16
  desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
16
- option :build_id, type: :string, required: true, desc: 'Build ID to download'
17
+ option :id, type: :string, required: true, desc: 'Emerge build ID to download'
17
18
  option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
18
19
  option :device_id, type: :string, desc: 'Specific device ID to target'
19
20
  option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
@@ -32,7 +33,7 @@ module EmergeCLI
32
33
  api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
33
34
  raise 'API token is required' unless api_token
34
35
 
35
- raise 'Build ID is required' unless @options[:build_id]
36
+ raise 'Build ID is required' unless @options[:id]
36
37
 
37
38
  output_name = nil
38
39
  app_id = nil
@@ -41,7 +42,7 @@ module EmergeCLI
41
42
  @network ||= EmergeCLI::Network.new(api_token:)
42
43
 
43
44
  Logger.info 'Getting build URL...'
44
- request = get_build_url(@options[:build_id])
45
+ request = get_build_url(@options[:id])
45
46
  response = parse_response(request)
46
47
 
47
48
  platform = response['platform']
@@ -49,10 +50,32 @@ module EmergeCLI
49
50
  app_id = response['appId']
50
51
 
51
52
  extension = platform == 'ios' ? 'ipa' : 'apk'
52
- Logger.info 'Downloading build...'
53
- output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
54
- `curl --progress-bar -L '#{download_url}' -o #{output_name} `
55
- Logger.info "Build downloaded to #{output_name}"
53
+ output_name = @options[:output] || "#{@options[:id]}.#{extension}"
54
+
55
+ if File.exist?(output_name)
56
+ Logger.info "Build file already exists at #{output_name}"
57
+ prompt = TTY::Prompt.new
58
+ choice = prompt.select('What would you like to do?', {
59
+ 'Install existing file' => :install,
60
+ 'Overwrite with new download' => :overwrite,
61
+ 'Cancel' => :cancel
62
+ })
63
+
64
+ case choice
65
+ when :install
66
+ Logger.info 'Proceeding with existing file...'
67
+ when :overwrite
68
+ Logger.info 'Downloading new build...'
69
+ `curl --progress-bar -L '#{download_url}' -o #{output_name}`
70
+ Logger.info "✅ Build downloaded to #{output_name}"
71
+ when :cancel
72
+ raise 'Operation cancelled by user'
73
+ end
74
+ else
75
+ Logger.info 'Downloading build...'
76
+ `curl --progress-bar -L '#{download_url}' -o #{output_name}`
77
+ Logger.info "✅ Build downloaded to #{output_name}"
78
+ end
56
79
  rescue StandardError => e
57
80
  Logger.error "❌ Failed to download build: #{e.message}"
58
81
  raise e
@@ -128,12 +151,34 @@ module EmergeCLI
128
151
  end
129
152
 
130
153
  def install_android_build(build_path)
131
- command = "adb -s #{@options[:device_id]} install #{build_path}"
154
+ device_id = @options[:device_id] || select_android_device
155
+ raise 'No Android devices found' unless device_id
156
+
157
+ command = "adb -s #{device_id} install #{build_path}"
132
158
  Logger.debug "Running command: #{command}"
133
159
  `#{command}`
134
160
 
135
161
  Logger.info '✅ Build installed'
136
162
  end
163
+
164
+ def select_android_device
165
+ devices = get_android_devices
166
+ return nil if devices.empty?
167
+ return devices.first if devices.length == 1
168
+
169
+ prompt = TTY::Prompt.new
170
+ Logger.info 'Multiple Android devices found.'
171
+ prompt.select('Choose a device:', devices)
172
+ end
173
+
174
+ def get_android_devices
175
+ output = `adb devices`
176
+ # Split output into lines, remove first line (header), and extract device IDs
177
+ output.split("\n")[1..]
178
+ .map(&:strip)
179
+ .reject(&:empty?)
180
+ .map { |line| line.split("\t").first }
181
+ end
137
182
  end
138
183
  end
139
184
  end
@@ -7,7 +7,7 @@ module EmergeCLI
7
7
  class Reaper < EmergeCLI::Commands::GlobalOptions
8
8
  desc 'Analyze dead code from an Emerge upload'
9
9
 
10
- option :upload_id, type: :string, required: true, desc: 'Upload ID to analyze'
10
+ option :id, type: :string, required: true, desc: 'Emerge build ID to analyze'
11
11
  option :project_root, type: :string, required: true,
12
12
  desc: 'Root directory of the project, defaults to current directory'
13
13
 
@@ -38,7 +38,7 @@ module EmergeCLI
38
38
  project_root = @options[:project_root] || Dir.pwd
39
39
 
40
40
  Sync do
41
- all_data = @profiler.measure('fetch_dead_code') { fetch_all_dead_code(@options[:upload_id]) }
41
+ all_data = @profiler.measure('fetch_dead_code') { fetch_all_dead_code(@options[:id]) }
42
42
  result = @profiler.measure('parse_dead_code') { DeadCodeResult.new(all_data) }
43
43
 
44
44
  Logger.info result.to_s
@@ -6,6 +6,8 @@ require 'async'
6
6
  require 'async/barrier'
7
7
  require 'async/semaphore'
8
8
  require 'async/http/internet/instance'
9
+ require 'zip'
10
+ require 'tempfile'
9
11
 
10
12
  module EmergeCLI
11
13
  module Commands
@@ -33,6 +35,8 @@ module EmergeCLI
33
35
 
34
36
  option :profile, type: :boolean, default: false, desc: 'Enable performance profiling metrics'
35
37
 
38
+ option :batch, type: :boolean, default: false, desc: 'Upload images in batch using zip file'
39
+
36
40
  argument :image_paths, type: :array, required: false, desc: 'Paths to folders containing images'
37
41
 
38
42
  def initialize(network: nil, git_info_provider: nil)
@@ -64,7 +68,7 @@ module EmergeCLI
64
68
 
65
69
  image_files = @profiler.measure('find_image_files') { find_image_files(client) }
66
70
 
67
- @profiler.measure('check_duplicate_files') { check_duplicate_files(image_files, client) }
71
+ check_duplicate_files(image_files, client)
68
72
 
69
73
  run_id = @profiler.measure('create_run') { create_run }
70
74
 
@@ -126,18 +130,26 @@ module EmergeCLI
126
130
  found_images
127
131
  end
128
132
 
129
- def check_duplicate_files(image_files, client)
133
+ def check_duplicate_files(image_files, _client)
130
134
  seen_files = {}
135
+ duplicate_files = {}
136
+
131
137
  image_files.each do |image_path|
132
- file_name = client.parse_file_info(image_path)[:file_name]
138
+ file_name = File.basename(image_path)
133
139
 
134
140
  if seen_files[file_name]
135
- Logger.warn "Duplicate file name detected: '#{file_name}'. " \
136
- "Previous occurrence: '#{seen_files[file_name]}'. " \
137
- 'This upload will overwrite the previous one.'
141
+ duplicate_files[file_name] ||= []
142
+ duplicate_files[file_name] << image_path
143
+ else
144
+ seen_files[file_name] = image_path
138
145
  end
139
- seen_files[file_name] = image_path
140
146
  end
147
+
148
+ duplicate_files.each do |filename, paths|
149
+ Logger.warn "Found #{paths.length} duplicate(s) of '#{filename}'. Duplicates: #{paths.join(', ')}"
150
+ end
151
+
152
+ [seen_files, duplicate_files]
141
153
  end
142
154
 
143
155
  def create_run
@@ -178,6 +190,89 @@ module EmergeCLI
178
190
  def upload_images(run_id, concurrency, image_files, client)
179
191
  Logger.info 'Uploading images...'
180
192
 
193
+ if @options[:batch]
194
+ batch_upload_images(run_id, image_files, client)
195
+ else
196
+ individual_upload_images(run_id, concurrency, image_files, client)
197
+ end
198
+ end
199
+
200
+ def batch_upload_images(run_id, image_files, client)
201
+ Logger.info 'Preparing batch upload...'
202
+
203
+ metadata_barrier = Async::Barrier.new
204
+ metadata_semaphore = Async::Semaphore.new(10, parent: metadata_barrier)
205
+
206
+ image_metadata = {
207
+ manifestVersion: 1,
208
+ images: {},
209
+ errors: []
210
+ }
211
+
212
+ used_filenames, = check_duplicate_files(image_files, client)
213
+
214
+ @profiler.measure('process_image_metadata') do
215
+ image_files.each do |image_path|
216
+ metadata_semaphore.async do
217
+ file_info = client.parse_file_info(image_path)
218
+
219
+ dimensions = @profiler.measure('chunky_png_processing') do
220
+ datastream = ChunkyPNG::Datastream.from_file(image_path)
221
+ {
222
+ width: datastream.header_chunk.width,
223
+ height: datastream.header_chunk.height
224
+ }
225
+ end
226
+
227
+ metadata = {
228
+ fileName: file_info[:file_name],
229
+ groupName: file_info[:group_name],
230
+ displayName: file_info[:variant_name],
231
+ width: dimensions[:width],
232
+ height: dimensions[:height]
233
+ }
234
+
235
+ image_name = File.basename(image_path, '.*')
236
+ image_metadata[:images][image_name] = metadata
237
+ end
238
+ end
239
+
240
+ metadata_barrier.wait
241
+ end
242
+
243
+ Tempfile.create(['snapshot_batch', '.zip']) do |zip_file|
244
+ @profiler.measure('create_zip_file') do
245
+ Zip::File.open(zip_file.path, Zip::File::CREATE) do |zipfile|
246
+ zipfile.get_output_stream('manifest.json') { |f| f.write(JSON.generate(image_metadata)) }
247
+
248
+ image_files.each do |image_path|
249
+ filename = File.basename(image_path)
250
+ # Only add files we haven't seen before
251
+ zipfile.add(filename, image_path) if used_filenames[filename] == image_path
252
+ end
253
+ end
254
+ end
255
+
256
+ upload_url = @profiler.measure('create_batch_upload_url') do
257
+ response = @network.post(path: '/v1/snapshots/run/batch-image', body: { run_id: run_id })
258
+ JSON.parse(response.read).fetch('zip_url')
259
+ end
260
+
261
+ Logger.info 'Uploading images...'
262
+ Logger.debug "Uploading batch zip file to #{upload_url}"
263
+ @profiler.measure('upload_batch_zip') do
264
+ @network.put(
265
+ path: upload_url,
266
+ headers: { 'Content-Type' => 'application/zip' },
267
+ body: File.read(zip_file.path)
268
+ )
269
+ end
270
+ end
271
+ ensure
272
+ metadata_barrier&.stop
273
+ end
274
+
275
+ def individual_upload_images(run_id, concurrency, image_files, client)
181
276
  post_image_barrier = Async::Barrier.new
182
277
  post_image_semaphore = Async::Semaphore.new(concurrency, parent: post_image_barrier)
183
278
 
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module EmergeCLI
2
- VERSION = '0.7.0'.freeze
2
+ VERSION = '0.7.2'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: emerge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emerge Tools
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-01-31 00:00:00.000000000 Z
11
+ date: 2025-02-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http