emerge 0.6.1 → 0.7.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.
- checksums.yaml +4 -4
- data/lib/commands/build/distribution/install.rb +141 -0
- data/lib/commands/build/distribution/validate.rb +166 -0
- data/lib/commands/{autofixes → fix}/exported_symbols.rb +1 -1
- data/lib/commands/{autofixes → fix}/minify_strings.rb +1 -1
- data/lib/commands/{autofixes → fix}/strip_binary_symbols.rb +1 -1
- data/lib/commands/order_files/download_order_files.rb +51 -49
- data/lib/commands/order_files/validate_linkmaps.rb +34 -32
- data/lib/commands/order_files/validate_xcode_project.rb +46 -44
- data/lib/emerge_cli.rb +37 -36
- data/lib/reaper/ast_parser.rb +24 -5
- data/lib/version.rb +1 -1
- metadata +7 -7
- data/lib/commands/build_distribution/download_and_install.rb +0 -139
- data/lib/commands/build_distribution/validate_app.rb +0 -164
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: f526af7bee28fe19586eb432339f8b6d771e98c9127a5126a4be9f47a32d6ab7
         | 
| 4 | 
            +
              data.tar.gz: eb356b6d63d1e66595550516aa78c36d7926b867ac5caf9a35000f0c8b3feeb5
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 5e3ba42f1367adf1b50e0bb236b79dc58225df5bd29e7e3af419775cac52e2d04415d5c949b0a62f2eb17f0ae5d25bd82cc086f194d34da0f912dd35af0e433a
         | 
| 7 | 
            +
              data.tar.gz: adf35a55cf1e66d9b0de879e8bab9843c2da10d3a00784dc2eebab7f865ac6ca501be7f81a28c660c6de60ff6ffb4f8e0670fbaeaf4057a67c943424759c8855
         | 
| @@ -0,0 +1,141 @@ | |
| 1 | 
            +
            require 'dry/cli'
         | 
| 2 | 
            +
            require 'cfpropertylist'
         | 
| 3 | 
            +
            require 'zip'
         | 
| 4 | 
            +
            require 'rbconfig'
         | 
| 5 | 
            +
            require 'tmpdir'
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            module EmergeCLI
         | 
| 8 | 
            +
              module Commands
         | 
| 9 | 
            +
                module Build
         | 
| 10 | 
            +
                  module Distribution
         | 
| 11 | 
            +
                    class Install < EmergeCLI::Commands::GlobalOptions
         | 
| 12 | 
            +
                      desc 'Download and install a build from Build Distribution'
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                      option :api_token, type: :string, required: false,
         | 
| 15 | 
            +
                                         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 :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
         | 
| 18 | 
            +
                      option :device_id, type: :string, desc: 'Specific device ID to target'
         | 
| 19 | 
            +
                      option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
         | 
| 20 | 
            +
                                           desc: 'Type of device to target (virtual/physical/any)'
         | 
| 21 | 
            +
                      option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      def initialize(network: nil)
         | 
| 24 | 
            +
                        @network = network
         | 
| 25 | 
            +
                      end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      def call(**options)
         | 
| 28 | 
            +
                        @options = options
         | 
| 29 | 
            +
                        before(options)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                        Sync do
         | 
| 32 | 
            +
                          api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
         | 
| 33 | 
            +
                          raise 'API token is required' unless api_token
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                          raise 'Build ID is required' unless @options[:build_id]
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                          output_name = nil
         | 
| 38 | 
            +
                          app_id = nil
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                          begin
         | 
| 41 | 
            +
                            @network ||= EmergeCLI::Network.new(api_token:)
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                            Logger.info 'Getting build URL...'
         | 
| 44 | 
            +
                            request = get_build_url(@options[:build_id])
         | 
| 45 | 
            +
                            response = parse_response(request)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                            platform = response['platform']
         | 
| 48 | 
            +
                            download_url = response['downloadUrl']
         | 
| 49 | 
            +
                            app_id = response['appId']
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                            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}"
         | 
| 56 | 
            +
                          rescue StandardError => e
         | 
| 57 | 
            +
                            Logger.error "❌ Failed to download build: #{e.message}"
         | 
| 58 | 
            +
                            raise e
         | 
| 59 | 
            +
                          ensure
         | 
| 60 | 
            +
                            @network&.close
         | 
| 61 | 
            +
                          end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                          begin
         | 
| 64 | 
            +
                            if @options[:install] && !output_name.nil?
         | 
| 65 | 
            +
                              if platform == 'ios'
         | 
| 66 | 
            +
                                install_ios_build(output_name, app_id)
         | 
| 67 | 
            +
                              elsif platform == 'android'
         | 
| 68 | 
            +
                                install_android_build(output_name)
         | 
| 69 | 
            +
                              end
         | 
| 70 | 
            +
                            end
         | 
| 71 | 
            +
                          rescue StandardError => e
         | 
| 72 | 
            +
                            Logger.error "❌ Failed to install build: #{e.message}"
         | 
| 73 | 
            +
                            raise e
         | 
| 74 | 
            +
                          end
         | 
| 75 | 
            +
                        end
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      private
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                      def get_build_url(build_id)
         | 
| 81 | 
            +
                        @network.get(
         | 
| 82 | 
            +
                          path: '/distribution/downloadUrl',
         | 
| 83 | 
            +
                          max_retries: 3,
         | 
| 84 | 
            +
                          query: {
         | 
| 85 | 
            +
                            buildId: build_id
         | 
| 86 | 
            +
                          }
         | 
| 87 | 
            +
                        )
         | 
| 88 | 
            +
                      end
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                      def parse_response(response)
         | 
| 91 | 
            +
                        case response.status
         | 
| 92 | 
            +
                        when 200
         | 
| 93 | 
            +
                          JSON.parse(response.read)
         | 
| 94 | 
            +
                        when 400
         | 
| 95 | 
            +
                          error_message = JSON.parse(response.read)['errorMessage']
         | 
| 96 | 
            +
                          raise "Invalid parameters: #{error_message}"
         | 
| 97 | 
            +
                        when 401, 403
         | 
| 98 | 
            +
                          raise 'Invalid API token'
         | 
| 99 | 
            +
                        else
         | 
| 100 | 
            +
                          raise "Getting build failed with status #{response.status}"
         | 
| 101 | 
            +
                        end
         | 
| 102 | 
            +
                      end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                      def install_ios_build(build_path, app_id)
         | 
| 105 | 
            +
                        device_type = case @options[:device_type]
         | 
| 106 | 
            +
                                      when 'simulator'
         | 
| 107 | 
            +
                                        XcodeDeviceManager::DeviceType::VIRTUAL
         | 
| 108 | 
            +
                                      when 'physical'
         | 
| 109 | 
            +
                                        XcodeDeviceManager::DeviceType::PHYSICAL
         | 
| 110 | 
            +
                                      else
         | 
| 111 | 
            +
                                        XcodeDeviceManager::DeviceType::ANY
         | 
| 112 | 
            +
                                      end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                        device_manager = XcodeDeviceManager.new
         | 
| 115 | 
            +
                        device = if @options[:device_id]
         | 
| 116 | 
            +
                                   device_manager.find_device_by_id(@options[:device_id])
         | 
| 117 | 
            +
                                 else
         | 
| 118 | 
            +
                                   device_manager.find_device_by_type(device_type, build_path)
         | 
| 119 | 
            +
                                 end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
                        Logger.info "Installing build on #{device.device_id}"
         | 
| 122 | 
            +
                        device.install_app(build_path)
         | 
| 123 | 
            +
                        Logger.info '✅ Build installed'
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                        Logger.info "Launching app #{app_id}..."
         | 
| 126 | 
            +
                        device.launch_app(app_id)
         | 
| 127 | 
            +
                        Logger.info '✅ Build launched'
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      def install_android_build(build_path)
         | 
| 131 | 
            +
                        command = "adb -s #{@options[:device_id]} install #{build_path}"
         | 
| 132 | 
            +
                        Logger.debug "Running command: #{command}"
         | 
| 133 | 
            +
                        `#{command}`
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                        Logger.info '✅ Build installed'
         | 
| 136 | 
            +
                      end
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
              end
         | 
| 141 | 
            +
            end
         | 
| @@ -0,0 +1,166 @@ | |
| 1 | 
            +
            require 'dry/cli'
         | 
| 2 | 
            +
            require 'cfpropertylist'
         | 
| 3 | 
            +
            require 'zip'
         | 
| 4 | 
            +
            require 'rbconfig'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module EmergeCLI
         | 
| 7 | 
            +
              module Commands
         | 
| 8 | 
            +
                module Build
         | 
| 9 | 
            +
                  module Distribution
         | 
| 10 | 
            +
                    class ValidateApp < EmergeCLI::Commands::GlobalOptions
         | 
| 11 | 
            +
                      desc 'Validate app for build distribution'
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                      option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                      # Constants
         | 
| 16 | 
            +
                      PLIST_START = '<plist'.freeze
         | 
| 17 | 
            +
                      PLIST_STOP = '</plist>'.freeze
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                      UTF8_ENCODING = 'UTF-8'.freeze
         | 
| 20 | 
            +
                      STRING_FORMAT = 'binary'.freeze
         | 
| 21 | 
            +
                      EMPTY_STRING = ''.freeze
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                      EXPECTED_ABI = 'arm64-v8a'.freeze
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      def call(**options)
         | 
| 26 | 
            +
                        @options = options
         | 
| 27 | 
            +
                        before(options)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                        Sync do
         | 
| 30 | 
            +
                          file_extension = File.extname(@options[:path])
         | 
| 31 | 
            +
                          case file_extension
         | 
| 32 | 
            +
                          when '.xcarchive'
         | 
| 33 | 
            +
                            handle_xcarchive
         | 
| 34 | 
            +
                          when '.ipa'
         | 
| 35 | 
            +
                            handle_ipa
         | 
| 36 | 
            +
                          when '.app'
         | 
| 37 | 
            +
                            handle_app
         | 
| 38 | 
            +
                          when '.apk'
         | 
| 39 | 
            +
                            handle_apk
         | 
| 40 | 
            +
                          else
         | 
| 41 | 
            +
                            raise "Unknown file extension: #{file_extension}"
         | 
| 42 | 
            +
                          end
         | 
| 43 | 
            +
                        end
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                      private
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                      def handle_xcarchive
         | 
| 49 | 
            +
                        raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                        app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
         | 
| 52 | 
            +
                        run_codesign_check(app_path)
         | 
| 53 | 
            +
                        read_provisioning_profile(app_path)
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                      def handle_ipa
         | 
| 57 | 
            +
                        raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        Dir.mktmpdir do |tmp_dir|
         | 
| 60 | 
            +
                          Zip::File.open(@options[:path]) do |zip_file|
         | 
| 61 | 
            +
                            zip_file.each do |entry|
         | 
| 62 | 
            +
                              entry.extract(File.join(tmp_dir, entry.name))
         | 
| 63 | 
            +
                            end
         | 
| 64 | 
            +
                          end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                          app_path = File.join(tmp_dir, 'Payload/*.app')
         | 
| 67 | 
            +
                          app_path = Dir.glob(app_path).first
         | 
| 68 | 
            +
                          run_codesign_check(app_path)
         | 
| 69 | 
            +
                          read_provisioning_profile(app_path)
         | 
| 70 | 
            +
                        end
         | 
| 71 | 
            +
                      end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                      def handle_app
         | 
| 74 | 
            +
                        raise 'Path must be an app' unless @options[:path].end_with?('.app')
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                        app_path = @options[:path]
         | 
| 77 | 
            +
                        run_codesign_check(app_path)
         | 
| 78 | 
            +
                        read_provisioning_profile(app_path)
         | 
| 79 | 
            +
                      end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      def handle_apk
         | 
| 82 | 
            +
                        raise 'Path must be an APK' unless @options[:path].end_with?('.apk')
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                        apk_path = @options[:path]
         | 
| 85 | 
            +
                        check_supported_abis(apk_path)
         | 
| 86 | 
            +
                      end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                      def run_codesign_check(app_path)
         | 
| 89 | 
            +
                        unless RbConfig::CONFIG['host_os'] =~ /darwin/i
         | 
| 90 | 
            +
                          Logger.info 'Skipping codesign check on non-macOS platform'
         | 
| 91 | 
            +
                          return
         | 
| 92 | 
            +
                        end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                        command = "codesign -dvvv '#{app_path}'"
         | 
| 95 | 
            +
                        Logger.debug command
         | 
| 96 | 
            +
                        stdout, _, status = Open3.capture3(command)
         | 
| 97 | 
            +
                        Logger.debug stdout
         | 
| 98 | 
            +
                        raise '❌ Codesign check failed' unless status.success?
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                        Logger.info '✅ Codesign check passed'
         | 
| 101 | 
            +
                      end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                      def read_provisioning_profile(app_path)
         | 
| 104 | 
            +
                        entitlements_path = File.join(app_path, 'embedded.mobileprovision')
         | 
| 105 | 
            +
                        raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                        content = File.read(entitlements_path)
         | 
| 108 | 
            +
                        lines = content.lines
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                        buffer = ''
         | 
| 111 | 
            +
                        inside_plist = false
         | 
| 112 | 
            +
                        lines.each do |line|
         | 
| 113 | 
            +
                          inside_plist = true if line.include? PLIST_START
         | 
| 114 | 
            +
                          if inside_plist
         | 
| 115 | 
            +
                            buffer << line
         | 
| 116 | 
            +
                            break if line.include? PLIST_STOP
         | 
| 117 | 
            +
                          end
         | 
| 118 | 
            +
                        end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                        encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
         | 
| 121 | 
            +
                                                                                    replace: EMPTY_STRING)
         | 
| 122 | 
            +
                        encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                        plist = CFPropertyList::List.new(data: encoded_plist)
         | 
| 125 | 
            +
                        parsed_data = CFPropertyList.native_types(plist.value)
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                        expiration_date = parsed_data['ExpirationDate']
         | 
| 128 | 
            +
                        if expiration_date > Time.now
         | 
| 129 | 
            +
                          Logger.info '✅ Provisioning profile hasn\'t expired'
         | 
| 130 | 
            +
                        else
         | 
| 131 | 
            +
                          Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
         | 
| 132 | 
            +
                        end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                        provisions_all_devices = parsed_data['ProvisionsAllDevices']
         | 
| 135 | 
            +
                        if provisions_all_devices
         | 
| 136 | 
            +
                          Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
         | 
| 137 | 
            +
                        else
         | 
| 138 | 
            +
                          devices = parsed_data['ProvisionedDevices']
         | 
| 139 | 
            +
                          Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
         | 
| 140 | 
            +
                          Logger.info "Devices: #{devices.inspect}"
         | 
| 141 | 
            +
                        end
         | 
| 142 | 
            +
                      end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                      def check_supported_abis(apk_path)
         | 
| 145 | 
            +
                        abis = []
         | 
| 146 | 
            +
             | 
| 147 | 
            +
                        Zip::File.open(apk_path) do |zip_file|
         | 
| 148 | 
            +
                          zip_file.each do |entry|
         | 
| 149 | 
            +
                            if entry.name.start_with?('lib/') && entry.name.count('/') == 2
         | 
| 150 | 
            +
                              abi = entry.name.split('/')[1]
         | 
| 151 | 
            +
                              abis << abi unless abis.include?(abi)
         | 
| 152 | 
            +
                            end
         | 
| 153 | 
            +
                          end
         | 
| 154 | 
            +
                        end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                        unless abis.include?(EXPECTED_ABI)
         | 
| 157 | 
            +
                          raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
         | 
| 158 | 
            +
                        end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                        Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
         | 
| 161 | 
            +
                      end
         | 
| 162 | 
            +
                    end
         | 
| 163 | 
            +
                  end
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
              end
         | 
| 166 | 
            +
            end
         | 
| @@ -2,75 +2,77 @@ require 'dry/cli' | |
| 2 2 |  | 
| 3 3 | 
             
            module EmergeCLI
         | 
| 4 4 | 
             
              module Commands
         | 
| 5 | 
            -
                 | 
| 6 | 
            -
                   | 
| 5 | 
            +
                module OrderFiles
         | 
| 6 | 
            +
                  class Download < EmergeCLI::Commands::GlobalOptions
         | 
| 7 | 
            +
                    desc 'Download order files from Emerge'
         | 
| 7 8 |  | 
| 8 | 
            -
             | 
| 9 | 
            +
                    option :bundle_id, type: :string, required: true, desc: 'Bundle identifier to download order files for'
         | 
| 9 10 |  | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 11 | 
            +
                    option :api_token, type: :string, required: false,
         | 
| 12 | 
            +
                                       desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
         | 
| 12 13 |  | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 14 | 
            +
                    option :app_version, type: :string, required: true,
         | 
| 15 | 
            +
                                         desc: 'App version to download order files for'
         | 
| 15 16 |  | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 17 | 
            +
                    option :unzip, type: :boolean, required: false,
         | 
| 18 | 
            +
                                   desc: 'Unzip the order file after downloading'
         | 
| 18 19 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 20 | 
            +
                    option :output, type: :string, required: false,
         | 
| 21 | 
            +
                                    desc: 'Output name for the order file, defaults to bundle_id-app_version.gz'
         | 
| 21 22 |  | 
| 22 | 
            -
             | 
| 23 | 
            +
                    EMERGE_ORDER_FILE_URL = 'order-files-prod.emergetools.com'.freeze
         | 
| 23 24 |  | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 25 | 
            +
                    def initialize(network: nil)
         | 
| 26 | 
            +
                      @network = network
         | 
| 27 | 
            +
                    end
         | 
| 27 28 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 29 | 
            +
                    def call(**options)
         | 
| 30 | 
            +
                      @options = options
         | 
| 31 | 
            +
                      before(options)
         | 
| 31 32 |  | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 33 | 
            +
                      begin
         | 
| 34 | 
            +
                        api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
         | 
| 35 | 
            +
                        raise 'API token is required' unless api_token
         | 
| 35 36 |  | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 37 | 
            +
                        raise 'Bundle ID is required' unless @options[:bundle_id]
         | 
| 38 | 
            +
                        raise 'App version is required' unless @options[:app_version]
         | 
| 38 39 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 41 | 
            -
             | 
| 40 | 
            +
                        @network ||= EmergeCLI::Network.new(api_token:, base_url: EMERGE_ORDER_FILE_URL)
         | 
| 41 | 
            +
                        output_name = @options[:output] || "#{@options[:bundle_id]}-#{@options[:app_version]}.gz"
         | 
| 42 | 
            +
                        output_name = "#{output_name}.gz" unless output_name.end_with?('.gz')
         | 
| 42 43 |  | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 45 | 
            -
             | 
| 44 | 
            +
                        Sync do
         | 
| 45 | 
            +
                          request = get_order_file(options[:bundle_id], options[:app_version])
         | 
| 46 | 
            +
                          response = request.read
         | 
| 46 47 |  | 
| 47 | 
            -
             | 
| 48 | 
            +
                          File.write(output_name, response)
         | 
| 48 49 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 50 | 
            +
                          if @options[:unzip]
         | 
| 51 | 
            +
                            Logger.info 'Unzipping order file...'
         | 
| 52 | 
            +
                            Zlib::GzipReader.open(output_name) do |gz|
         | 
| 53 | 
            +
                              File.write(output_name.gsub('.gz', ''), gz.read)
         | 
| 54 | 
            +
                            end
         | 
| 53 55 | 
             
                          end
         | 
| 54 | 
            -
                        end
         | 
| 55 56 |  | 
| 56 | 
            -
             | 
| 57 | 
            +
                          Logger.info 'Order file downloaded successfully'
         | 
| 58 | 
            +
                        end
         | 
| 59 | 
            +
                      rescue StandardError => e
         | 
| 60 | 
            +
                        Logger.error "Failed to download order file: #{e.message}"
         | 
| 61 | 
            +
                        Logger.error 'Check your parameters and try again'
         | 
| 62 | 
            +
                        raise e
         | 
| 63 | 
            +
                      ensure
         | 
| 64 | 
            +
                        @network&.close
         | 
| 57 65 | 
             
                      end
         | 
| 58 | 
            -
                    rescue StandardError => e
         | 
| 59 | 
            -
                      Logger.error "Failed to download order file: #{e.message}"
         | 
| 60 | 
            -
                      Logger.error 'Check your parameters and try again'
         | 
| 61 | 
            -
                      raise e
         | 
| 62 | 
            -
                    ensure
         | 
| 63 | 
            -
                      @network&.close
         | 
| 64 66 | 
             
                    end
         | 
| 65 | 
            -
                  end
         | 
| 66 67 |  | 
| 67 | 
            -
             | 
| 68 | 
            +
                    private
         | 
| 68 69 |  | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 70 | 
            +
                    def get_order_file(bundle_id, app_version)
         | 
| 71 | 
            +
                      @network.get(
         | 
| 72 | 
            +
                        path: "/#{bundle_id}/#{app_version}",
         | 
| 73 | 
            +
                        max_retries: 0
         | 
| 74 | 
            +
                      )
         | 
| 75 | 
            +
                    end
         | 
| 74 76 | 
             
                  end
         | 
| 75 77 | 
             
                end
         | 
| 76 78 | 
             
              end
         | 
| @@ -3,52 +3,54 @@ require 'cfpropertylist' | |
| 3 3 |  | 
| 4 4 | 
             
            module EmergeCLI
         | 
| 5 5 | 
             
              module Commands
         | 
| 6 | 
            -
                 | 
| 7 | 
            -
                   | 
| 6 | 
            +
                module OrderFiles
         | 
| 7 | 
            +
                  class ValidateLinkmaps < EmergeCLI::Commands::GlobalOptions
         | 
| 8 | 
            +
                    desc 'Validate linkmaps in xcarchive'
         | 
| 8 9 |  | 
| 9 | 
            -
             | 
| 10 | 
            +
                    option :path, type: :string, required: true, desc: 'Path to the xcarchive to validate'
         | 
| 10 11 |  | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 12 | 
            +
                    def initialize(network: nil)
         | 
| 13 | 
            +
                      @network = network
         | 
| 14 | 
            +
                    end
         | 
| 14 15 |  | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 16 | 
            +
                    def call(**options)
         | 
| 17 | 
            +
                      @options = options
         | 
| 18 | 
            +
                      before(options)
         | 
| 18 19 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 20 | 
            +
                      Sync do
         | 
| 21 | 
            +
                        executable_name = get_executable_name
         | 
| 22 | 
            +
                        raise 'Executable not found' if executable_name.nil?
         | 
| 22 23 |  | 
| 23 | 
            -
             | 
| 24 | 
            +
                        Logger.info "Using executable: #{executable_name}"
         | 
| 24 25 |  | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 26 | 
            +
                        linkmaps_path = File.join(@options[:path], 'Linkmaps')
         | 
| 27 | 
            +
                        raise 'Linkmaps folder not found' unless File.directory?(linkmaps_path)
         | 
| 27 28 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 29 | 
            +
                        linkmaps = Dir.glob("#{linkmaps_path}/*.txt")
         | 
| 30 | 
            +
                        raise 'No linkmaps found' if linkmaps.empty?
         | 
| 30 31 |  | 
| 31 | 
            -
             | 
| 32 | 
            -
             | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 32 | 
            +
                        executable_linkmaps = linkmaps.select do |linkmap|
         | 
| 33 | 
            +
                          File.basename(linkmap).start_with?(executable_name)
         | 
| 34 | 
            +
                        end
         | 
| 35 | 
            +
                        raise 'No linkmaps found for executable' if executable_linkmaps.empty?
         | 
| 35 36 |  | 
| 36 | 
            -
             | 
| 37 | 
            +
                        Logger.info "✅ Found linkmaps for #{executable_name}"
         | 
| 38 | 
            +
                      end
         | 
| 37 39 | 
             
                    end
         | 
| 38 | 
            -
                  end
         | 
| 39 40 |  | 
| 40 | 
            -
             | 
| 41 | 
            +
                    private
         | 
| 41 42 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 43 | 
            +
                    def get_executable_name
         | 
| 44 | 
            +
                      raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
         | 
| 44 45 |  | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 46 | 
            +
                      app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
         | 
| 47 | 
            +
                      info_path = File.join(app_path, 'Info.plist')
         | 
| 48 | 
            +
                      plist_data = File.read(info_path)
         | 
| 49 | 
            +
                      plist = CFPropertyList::List.new(data: plist_data)
         | 
| 50 | 
            +
                      parsed_data = CFPropertyList.native_types(plist.value)
         | 
| 50 51 |  | 
| 51 | 
            -
             | 
| 52 | 
            +
                      parsed_data['CFBundleExecutable']
         | 
| 53 | 
            +
                    end
         | 
| 52 54 | 
             
                  end
         | 
| 53 55 | 
             
                end
         | 
| 54 56 | 
             
              end
         | 
| @@ -3,67 +3,69 @@ require 'xcodeproj' | |
| 3 3 |  | 
| 4 4 | 
             
            module EmergeCLI
         | 
| 5 5 | 
             
              module Commands
         | 
| 6 | 
            -
                 | 
| 7 | 
            -
                   | 
| 6 | 
            +
                module OrderFiles
         | 
| 7 | 
            +
                  class ValidateXcodeProject < EmergeCLI::Commands::GlobalOptions
         | 
| 8 | 
            +
                    desc 'Validate xcodeproject for order files'
         | 
| 8 9 |  | 
| 9 | 
            -
             | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 10 | 
            +
                    option :path, type: :string, required: true, desc: 'Path to the xcodeproject to validate'
         | 
| 11 | 
            +
                    option :target, type: :string, required: false, desc: 'Target to validate'
         | 
| 12 | 
            +
                    option :build_configuration, type: :string, required: false,
         | 
| 13 | 
            +
                                                 desc: 'Build configuration to validate (Release by default)'
         | 
| 13 14 |  | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
             | 
| 17 | 
            -
             | 
| 15 | 
            +
                    # Constants
         | 
| 16 | 
            +
                    LINK_MAPS_CONFIG = 'LD_GENERATE_MAP_FILE'.freeze
         | 
| 17 | 
            +
                    LINK_MAPS_PATH = 'LD_MAP_FILE_PATH'.freeze
         | 
| 18 | 
            +
                    PATH_TO_LINKMAP = '$(TARGET_TEMP_DIR)/$(PRODUCT_NAME)-LinkMap-$(CURRENT_VARIANT)-$(CURRENT_ARCH).txt'.freeze
         | 
| 18 19 |  | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 20 | 
            +
                    def call(**options)
         | 
| 21 | 
            +
                      @options = options
         | 
| 22 | 
            +
                      before(options)
         | 
| 22 23 |  | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 24 | 
            +
                      raise 'Path must be an xcodeproject' unless @options[:path].end_with?('.xcodeproj')
         | 
| 25 | 
            +
                      raise 'Path does not exist' unless File.exist?(@options[:path])
         | 
| 25 26 |  | 
| 26 | 
            -
             | 
| 27 | 
            +
                      @options[:build_configuration] ||= 'Release'
         | 
| 27 28 |  | 
| 28 | 
            -
             | 
| 29 | 
            -
             | 
| 29 | 
            +
                      Sync do
         | 
| 30 | 
            +
                        project = Xcodeproj::Project.open(@options[:path])
         | 
| 30 31 |  | 
| 31 | 
            -
             | 
| 32 | 
            +
                        validate_xcproj(project)
         | 
| 33 | 
            +
                      end
         | 
| 32 34 | 
             
                    end
         | 
| 33 | 
            -
                  end
         | 
| 34 35 |  | 
| 35 | 
            -
             | 
| 36 | 
            +
                    private
         | 
| 36 37 |  | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 40 | 
            -
             | 
| 38 | 
            +
                    def validate_xcproj(project)
         | 
| 39 | 
            +
                      project.targets.each do |target|
         | 
| 40 | 
            +
                        next if @options[:target] && target.name != @options[:target]
         | 
| 41 | 
            +
                        next unless target.product_type == 'com.apple.product-type.application'
         | 
| 41 42 |  | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
             | 
| 43 | 
            +
                        target.build_configurations.each do |config|
         | 
| 44 | 
            +
                          next if config.name != @options[:build_configuration]
         | 
| 45 | 
            +
                          validate_target_config(target, config)
         | 
| 46 | 
            +
                        end
         | 
| 45 47 | 
             
                      end
         | 
| 46 48 | 
             
                    end
         | 
| 47 | 
            -
                  end
         | 
| 48 49 |  | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
             | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 50 | 
            +
                    def validate_target_config(target, config)
         | 
| 51 | 
            +
                      has_error = false
         | 
| 52 | 
            +
                      if config.build_settings[LINK_MAPS_CONFIG] != 'YES'
         | 
| 53 | 
            +
                        has_error = true
         | 
| 54 | 
            +
                        Logger.error "❌ Write Link Map File (#{LINK_MAPS_CONFIG}) is not set to YES"
         | 
| 55 | 
            +
                      end
         | 
| 56 | 
            +
                      if config.build_settings[LINK_MAPS_PATH] != ''
         | 
| 57 | 
            +
                        has_error = true
         | 
| 58 | 
            +
                        Logger.error "❌ Path to Link Map File (#{LINK_MAPS_PATH}) is not set, we recommend \
         | 
| 58 59 | 
             
            setting it to '#{PATH_TO_LINKMAP}'"
         | 
| 59 | 
            -
             | 
| 60 | 
            +
                      end
         | 
| 60 61 |  | 
| 61 | 
            -
             | 
| 62 | 
            -
             | 
| 62 | 
            +
                      if has_error
         | 
| 63 | 
            +
                        Logger.error "❌ Target '#{target.name}' has errors, this means \
         | 
| 63 64 | 
             
            that the linkmaps will not be generated as expected"
         | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 65 | 
            +
                        Logger.error "Use `emerge configure order-files-ios --project-path '#{@options[:path]}'` to fix this"
         | 
| 66 | 
            +
                      else
         | 
| 67 | 
            +
                        Logger.info "✅ Target '#{target.name}' is valid"
         | 
| 68 | 
            +
                      end
         | 
| 67 69 | 
             
                    end
         | 
| 68 70 | 
             
                  end
         | 
| 69 71 | 
             
                end
         | 
    
        data/lib/emerge_cli.rb
    CHANGED
    
    | @@ -1,25 +1,25 @@ | |
| 1 1 | 
             
            require_relative 'version'
         | 
| 2 2 |  | 
| 3 3 | 
             
            require_relative 'commands/global_options'
         | 
| 4 | 
            -
            require_relative 'commands/ | 
| 5 | 
            -
            require_relative 'commands/ | 
| 6 | 
            -
            require_relative 'commands/upload/snapshots/client_libraries/paparazzi'
         | 
| 7 | 
            -
            require_relative 'commands/upload/snapshots/client_libraries/roborazzi'
         | 
| 8 | 
            -
            require_relative 'commands/upload/snapshots/client_libraries/default'
         | 
| 9 | 
            -
            require_relative 'commands/integrate/fastlane'
         | 
| 4 | 
            +
            require_relative 'commands/build/distribution/validate'
         | 
| 5 | 
            +
            require_relative 'commands/build/distribution/install'
         | 
| 10 6 | 
             
            require_relative 'commands/config/snapshots/snapshots_ios'
         | 
| 11 7 | 
             
            require_relative 'commands/config/orderfiles/orderfiles_ios'
         | 
| 12 | 
            -
            require_relative 'commands/ | 
| 13 | 
            -
            require_relative 'commands/ | 
| 8 | 
            +
            require_relative 'commands/integrate/fastlane'
         | 
| 9 | 
            +
            require_relative 'commands/fix/minify_strings'
         | 
| 10 | 
            +
            require_relative 'commands/fix/strip_binary_symbols'
         | 
| 11 | 
            +
            require_relative 'commands/fix/exported_symbols'
         | 
| 14 12 | 
             
            require_relative 'commands/order_files/download_order_files'
         | 
| 15 13 | 
             
            require_relative 'commands/order_files/validate_linkmaps'
         | 
| 16 14 | 
             
            require_relative 'commands/order_files/validate_xcode_project'
         | 
| 15 | 
            +
            require_relative 'commands/reaper/reaper'
         | 
| 16 | 
            +
            require_relative 'commands/snapshots/validate_app'
         | 
| 17 17 | 
             
            require_relative 'commands/upload/build'
         | 
| 18 | 
            -
            require_relative 'commands/ | 
| 19 | 
            -
            require_relative 'commands/ | 
| 20 | 
            -
            require_relative 'commands/ | 
| 21 | 
            -
            require_relative 'commands/ | 
| 22 | 
            -
            require_relative 'commands/ | 
| 18 | 
            +
            require_relative 'commands/upload/snapshots/snapshots'
         | 
| 19 | 
            +
            require_relative 'commands/upload/snapshots/client_libraries/swift_snapshot_testing'
         | 
| 20 | 
            +
            require_relative 'commands/upload/snapshots/client_libraries/paparazzi'
         | 
| 21 | 
            +
            require_relative 'commands/upload/snapshots/client_libraries/roborazzi'
         | 
| 22 | 
            +
            require_relative 'commands/upload/snapshots/client_libraries/default'
         | 
| 23 23 |  | 
| 24 24 | 
             
            require_relative 'reaper/ast_parser'
         | 
| 25 25 | 
             
            require_relative 'reaper/code_deleter'
         | 
| @@ -44,41 +44,42 @@ require 'dry/cli' | |
| 44 44 | 
             
            module EmergeCLI
         | 
| 45 45 | 
             
              extend Dry::CLI::Registry
         | 
| 46 46 |  | 
| 47 | 
            -
              register 'upload', aliases: ['u'] do |prefix|
         | 
| 48 | 
            -
                prefix.register 'build', Commands::Upload::Build
         | 
| 49 | 
            -
                prefix.register 'snapshots', Commands::Upload::Snapshots
         | 
| 50 | 
            -
              end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
              register 'integrate' do |prefix|
         | 
| 53 | 
            -
                prefix.register 'fastlane-ios', Commands::Integrate::Fastlane, aliases: ['i']
         | 
| 54 | 
            -
              end
         | 
| 55 | 
            -
             | 
| 56 47 | 
             
              register 'configure' do |prefix|
         | 
| 57 48 | 
             
                prefix.register 'snapshots-ios', Commands::Config::SnapshotsIOS
         | 
| 58 49 | 
             
                prefix.register 'order-files-ios', Commands::Config::OrderFilesIOS
         | 
| 59 50 | 
             
              end
         | 
| 60 51 |  | 
| 61 | 
            -
              register ' | 
| 52 | 
            +
              register 'download' do |prefix|
         | 
| 53 | 
            +
                prefix.register 'order-files', Commands::OrderFiles::Download
         | 
| 54 | 
            +
              end
         | 
| 62 55 |  | 
| 63 | 
            -
              register ' | 
| 64 | 
            -
                prefix.register ' | 
| 56 | 
            +
              register 'fix' do |prefix|
         | 
| 57 | 
            +
                prefix.register 'minify-strings', Commands::Fix::MinifyStrings
         | 
| 58 | 
            +
                prefix.register 'strip-binary-symbols', Commands::Fix::StripBinarySymbols
         | 
| 59 | 
            +
                prefix.register 'exported-symbols', Commands::Fix::ExportedSymbols
         | 
| 65 60 | 
             
              end
         | 
| 66 61 |  | 
| 67 | 
            -
              register ' | 
| 68 | 
            -
                prefix.register ' | 
| 69 | 
            -
                prefix.register 'validate-linkmaps', Commands::ValidateLinkmaps
         | 
| 70 | 
            -
                prefix.register 'validate-xcode-project', Commands::ValidateXcodeProject
         | 
| 62 | 
            +
              register 'integrate' do |prefix|
         | 
| 63 | 
            +
                prefix.register 'fastlane-ios', Commands::Integrate::Fastlane, aliases: ['i']
         | 
| 71 64 | 
             
              end
         | 
| 72 65 |  | 
| 73 | 
            -
              register ' | 
| 74 | 
            -
                prefix.register ' | 
| 75 | 
            -
             | 
| 66 | 
            +
              register 'install' do |prefix|
         | 
| 67 | 
            +
                prefix.register 'build', Commands::Build::Distribution::Install
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              # TODO: make this command action oriented
         | 
| 71 | 
            +
              register 'reaper', Commands::Reaper
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              register 'upload', aliases: ['u'] do |prefix|
         | 
| 74 | 
            +
                prefix.register 'build', Commands::Upload::Build
         | 
| 75 | 
            +
                prefix.register 'snapshots', Commands::Upload::Snapshots
         | 
| 76 76 | 
             
              end
         | 
| 77 77 |  | 
| 78 | 
            -
              register ' | 
| 79 | 
            -
                prefix.register ' | 
| 80 | 
            -
                prefix.register ' | 
| 81 | 
            -
                prefix.register ' | 
| 78 | 
            +
              register 'validate' do |prefix|
         | 
| 79 | 
            +
                prefix.register 'build-distribution', Commands::Build::Distribution::ValidateApp
         | 
| 80 | 
            +
                prefix.register 'order-files-linkmaps', Commands::OrderFiles::ValidateLinkmaps
         | 
| 81 | 
            +
                prefix.register 'order-files-xcode-project', Commands::OrderFiles::ValidateXcodeProject
         | 
| 82 | 
            +
                prefix.register 'snapshots-app-ios', Commands::Snapshots::ValidateApp
         | 
| 82 83 | 
             
              end
         | 
| 83 84 | 
             
            end
         | 
| 84 85 |  | 
    
        data/lib/reaper/ast_parser.rb
    CHANGED
    
    | @@ -29,6 +29,29 @@ module EmergeCLI | |
| 29 29 |  | 
| 30 30 | 
             
                  attr_reader :parser, :language
         | 
| 31 31 |  | 
| 32 | 
            +
                  @parser_paths_cache = {}
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  class << self
         | 
| 35 | 
            +
                    def find_parser_path(language, platform, arch)
         | 
| 36 | 
            +
                      cache_key = "#{language}-#{platform}-#{arch}"
         | 
| 37 | 
            +
                      return @parser_paths_cache[cache_key] if @parser_paths_cache.key?(cache_key)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      extension = platform == 'darwin' ? 'dylib' : 'so'
         | 
| 40 | 
            +
                      parser_file = "libtree-sitter-#{language}-#{platform}-#{arch}.#{extension}"
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      parser_paths = [
         | 
| 43 | 
            +
                        File.join(File.dirname(__FILE__), '..', '..', 'parsers', parser_file), # Relative to this file
         | 
| 44 | 
            +
                        File.join(Gem::Specification.find_by_name('emerge').gem_dir, 'parsers', parser_file) # Installed gem path
         | 
| 45 | 
            +
                      ]
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                      parser_path = parser_paths.find { |path| File.exist?(path) }
         | 
| 48 | 
            +
                      raise "No language grammar found for #{language}. Searched in: #{parser_paths.join(', ')}" unless parser_path
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                      @parser_paths_cache[cache_key] = parser_path
         | 
| 51 | 
            +
                      parser_path
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 32 55 | 
             
                  def initialize(language)
         | 
| 33 56 | 
             
                    @parser = TreeSitter::Parser.new
         | 
| 34 57 | 
             
                    @language = language
         | 
| @@ -52,11 +75,7 @@ module EmergeCLI | |
| 52 75 | 
             
                             raise "Unsupported architecture: #{RUBY_PLATFORM}"
         | 
| 53 76 | 
             
                           end
         | 
| 54 77 |  | 
| 55 | 
            -
                     | 
| 56 | 
            -
                    parser_file = "libtree-sitter-#{language}-#{platform}-#{arch}.#{extension}"
         | 
| 57 | 
            -
                    parser_path = File.join('parsers', parser_file)
         | 
| 58 | 
            -
                    raise "No language grammar found for #{language}" unless File.exist?(parser_path)
         | 
| 59 | 
            -
             | 
| 78 | 
            +
                    parser_path = self.class.find_parser_path(language, platform, arch)
         | 
| 60 79 | 
             
                    @parser.language = TreeSitter::Language.load(language, parser_path)
         | 
| 61 80 | 
             
                  end
         | 
| 62 81 |  | 
    
        data/lib/version.rb
    CHANGED
    
    
    
        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. | 
| 4 | 
            +
              version: 0.7.0
         | 
| 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- | 
| 11 | 
            +
            date: 2025-01-31 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: async-http
         | 
| @@ -209,13 +209,13 @@ files: | |
| 209 209 | 
             
            - CHANGELOG.md
         | 
| 210 210 | 
             
            - README.md
         | 
| 211 211 | 
             
            - exe/emerge
         | 
| 212 | 
            -
            - lib/commands/ | 
| 213 | 
            -
            - lib/commands/ | 
| 214 | 
            -
            - lib/commands/autofixes/strip_binary_symbols.rb
         | 
| 215 | 
            -
            - lib/commands/build_distribution/download_and_install.rb
         | 
| 216 | 
            -
            - lib/commands/build_distribution/validate_app.rb
         | 
| 212 | 
            +
            - lib/commands/build/distribution/install.rb
         | 
| 213 | 
            +
            - lib/commands/build/distribution/validate.rb
         | 
| 217 214 | 
             
            - lib/commands/config/orderfiles/orderfiles_ios.rb
         | 
| 218 215 | 
             
            - lib/commands/config/snapshots/snapshots_ios.rb
         | 
| 216 | 
            +
            - lib/commands/fix/exported_symbols.rb
         | 
| 217 | 
            +
            - lib/commands/fix/minify_strings.rb
         | 
| 218 | 
            +
            - lib/commands/fix/strip_binary_symbols.rb
         | 
| 219 219 | 
             
            - lib/commands/global_options.rb
         | 
| 220 220 | 
             
            - lib/commands/integrate/fastlane.rb
         | 
| 221 221 | 
             
            - lib/commands/order_files/download_order_files.rb
         | 
| @@ -1,139 +0,0 @@ | |
| 1 | 
            -
            require 'dry/cli'
         | 
| 2 | 
            -
            require 'cfpropertylist'
         | 
| 3 | 
            -
            require 'zip'
         | 
| 4 | 
            -
            require 'rbconfig'
         | 
| 5 | 
            -
            require 'tmpdir'
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            module EmergeCLI
         | 
| 8 | 
            -
              module Commands
         | 
| 9 | 
            -
                module BuildDistribution
         | 
| 10 | 
            -
                  class DownloadAndInstall < EmergeCLI::Commands::GlobalOptions
         | 
| 11 | 
            -
                    desc 'Download build from Build Distribution'
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    option :api_token, type: :string, required: false,
         | 
| 14 | 
            -
                                       desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
         | 
| 15 | 
            -
                    option :build_id, type: :string, required: true, desc: 'Build ID to download'
         | 
| 16 | 
            -
                    option :install, type: :boolean, default: true, required: false, desc: 'Install the build on the device'
         | 
| 17 | 
            -
                    option :device_id, type: :string, desc: 'Specific device ID to target'
         | 
| 18 | 
            -
                    option :device_type, type: :string, enum: %w[virtual physical any], default: 'any',
         | 
| 19 | 
            -
                                         desc: 'Type of device to target (virtual/physical/any)'
         | 
| 20 | 
            -
                    option :output, type: :string, required: false, desc: 'Output path for the downloaded build'
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                    def initialize(network: nil)
         | 
| 23 | 
            -
                      @network = network
         | 
| 24 | 
            -
                    end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                    def call(**options)
         | 
| 27 | 
            -
                      @options = options
         | 
| 28 | 
            -
                      before(options)
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                      Sync do
         | 
| 31 | 
            -
                        api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
         | 
| 32 | 
            -
                        raise 'API token is required' unless api_token
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                        raise 'Build ID is required' unless @options[:build_id]
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                        output_name = nil
         | 
| 37 | 
            -
                        app_id = nil
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                        begin
         | 
| 40 | 
            -
                          @network ||= EmergeCLI::Network.new(api_token:)
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                          Logger.info 'Getting build URL...'
         | 
| 43 | 
            -
                          request = get_build_url(@options[:build_id])
         | 
| 44 | 
            -
                          response = parse_response(request)
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                          platform = response['platform']
         | 
| 47 | 
            -
                          download_url = response['downloadUrl']
         | 
| 48 | 
            -
                          app_id = response['appId']
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                          extension = platform == 'ios' ? 'ipa' : 'apk'
         | 
| 51 | 
            -
                          Logger.info 'Downloading build...'
         | 
| 52 | 
            -
                          output_name = @options[:output] || "#{@options[:build_id]}.#{extension}"
         | 
| 53 | 
            -
                          `curl --progress-bar -L '#{download_url}' -o #{output_name} `
         | 
| 54 | 
            -
                          Logger.info "✅ Build downloaded to #{output_name}"
         | 
| 55 | 
            -
                        rescue StandardError => e
         | 
| 56 | 
            -
                          Logger.error "❌ Failed to download build: #{e.message}"
         | 
| 57 | 
            -
                          raise e
         | 
| 58 | 
            -
                        ensure
         | 
| 59 | 
            -
                          @network&.close
         | 
| 60 | 
            -
                        end
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                        begin
         | 
| 63 | 
            -
                          if @options[:install] && !output_name.nil?
         | 
| 64 | 
            -
                            if platform == 'ios'
         | 
| 65 | 
            -
                              install_ios_build(output_name, app_id)
         | 
| 66 | 
            -
                            elsif platform == 'android'
         | 
| 67 | 
            -
                              install_android_build(output_name)
         | 
| 68 | 
            -
                            end
         | 
| 69 | 
            -
                          end
         | 
| 70 | 
            -
                        rescue StandardError => e
         | 
| 71 | 
            -
                          Logger.error "❌ Failed to install build: #{e.message}"
         | 
| 72 | 
            -
                          raise e
         | 
| 73 | 
            -
                        end
         | 
| 74 | 
            -
                      end
         | 
| 75 | 
            -
                    end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                    private
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                    def get_build_url(build_id)
         | 
| 80 | 
            -
                      @network.get(
         | 
| 81 | 
            -
                        path: '/distribution/downloadUrl',
         | 
| 82 | 
            -
                        max_retries: 3,
         | 
| 83 | 
            -
                        query: {
         | 
| 84 | 
            -
                          buildId: build_id
         | 
| 85 | 
            -
                        }
         | 
| 86 | 
            -
                      )
         | 
| 87 | 
            -
                    end
         | 
| 88 | 
            -
             | 
| 89 | 
            -
                    def parse_response(response)
         | 
| 90 | 
            -
                      case response.status
         | 
| 91 | 
            -
                      when 200
         | 
| 92 | 
            -
                        JSON.parse(response.read)
         | 
| 93 | 
            -
                      when 400
         | 
| 94 | 
            -
                        error_message = JSON.parse(response.read)['errorMessage']
         | 
| 95 | 
            -
                        raise "Invalid parameters: #{error_message}"
         | 
| 96 | 
            -
                      when 401, 403
         | 
| 97 | 
            -
                        raise 'Invalid API token'
         | 
| 98 | 
            -
                      else
         | 
| 99 | 
            -
                        raise "Getting build failed with status #{response.status}"
         | 
| 100 | 
            -
                      end
         | 
| 101 | 
            -
                    end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                    def install_ios_build(build_path, app_id)
         | 
| 104 | 
            -
                      device_type = case @options[:device_type]
         | 
| 105 | 
            -
                                    when 'simulator'
         | 
| 106 | 
            -
                                      XcodeDeviceManager::DeviceType::VIRTUAL
         | 
| 107 | 
            -
                                    when 'physical'
         | 
| 108 | 
            -
                                      XcodeDeviceManager::DeviceType::PHYSICAL
         | 
| 109 | 
            -
                                    else
         | 
| 110 | 
            -
                                      XcodeDeviceManager::DeviceType::ANY
         | 
| 111 | 
            -
                                    end
         | 
| 112 | 
            -
             | 
| 113 | 
            -
                      device_manager = XcodeDeviceManager.new
         | 
| 114 | 
            -
                      device = if @options[:device_id]
         | 
| 115 | 
            -
                                 device_manager.find_device_by_id(@options[:device_id])
         | 
| 116 | 
            -
                               else
         | 
| 117 | 
            -
                                 device_manager.find_device_by_type(device_type, build_path)
         | 
| 118 | 
            -
                               end
         | 
| 119 | 
            -
             | 
| 120 | 
            -
                      Logger.info "Installing build on #{device.device_id}"
         | 
| 121 | 
            -
                      device.install_app(build_path)
         | 
| 122 | 
            -
                      Logger.info '✅ Build installed'
         | 
| 123 | 
            -
             | 
| 124 | 
            -
                      Logger.info "Launching app #{app_id}..."
         | 
| 125 | 
            -
                      device.launch_app(app_id)
         | 
| 126 | 
            -
                      Logger.info '✅ Build launched'
         | 
| 127 | 
            -
                    end
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                    def install_android_build(build_path)
         | 
| 130 | 
            -
                      command = "adb -s #{@options[:device_id]} install #{build_path}"
         | 
| 131 | 
            -
                      Logger.debug "Running command: #{command}"
         | 
| 132 | 
            -
                      `#{command}`
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                      Logger.info '✅ Build installed'
         | 
| 135 | 
            -
                    end
         | 
| 136 | 
            -
                  end
         | 
| 137 | 
            -
                end
         | 
| 138 | 
            -
              end
         | 
| 139 | 
            -
            end
         | 
| @@ -1,164 +0,0 @@ | |
| 1 | 
            -
            require 'dry/cli'
         | 
| 2 | 
            -
            require 'cfpropertylist'
         | 
| 3 | 
            -
            require 'zip'
         | 
| 4 | 
            -
            require 'rbconfig'
         | 
| 5 | 
            -
             | 
| 6 | 
            -
            module EmergeCLI
         | 
| 7 | 
            -
              module Commands
         | 
| 8 | 
            -
                module BuildDistribution
         | 
| 9 | 
            -
                  class ValidateApp < EmergeCLI::Commands::GlobalOptions
         | 
| 10 | 
            -
                    desc 'Validate app for build distribution'
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                    option :path, type: :string, required: true, desc: 'Path to the xcarchive, IPA or APK to validate'
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                    # Constants
         | 
| 15 | 
            -
                    PLIST_START = '<plist'.freeze
         | 
| 16 | 
            -
                    PLIST_STOP = '</plist>'.freeze
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                    UTF8_ENCODING = 'UTF-8'.freeze
         | 
| 19 | 
            -
                    STRING_FORMAT = 'binary'.freeze
         | 
| 20 | 
            -
                    EMPTY_STRING = ''.freeze
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                    EXPECTED_ABI = 'arm64-v8a'.freeze
         | 
| 23 | 
            -
             | 
| 24 | 
            -
                    def call(**options)
         | 
| 25 | 
            -
                      @options = options
         | 
| 26 | 
            -
                      before(options)
         | 
| 27 | 
            -
             | 
| 28 | 
            -
                      Sync do
         | 
| 29 | 
            -
                        file_extension = File.extname(@options[:path])
         | 
| 30 | 
            -
                        case file_extension
         | 
| 31 | 
            -
                        when '.xcarchive'
         | 
| 32 | 
            -
                          handle_xcarchive
         | 
| 33 | 
            -
                        when '.ipa'
         | 
| 34 | 
            -
                          handle_ipa
         | 
| 35 | 
            -
                        when '.app'
         | 
| 36 | 
            -
                          handle_app
         | 
| 37 | 
            -
                        when '.apk'
         | 
| 38 | 
            -
                          handle_apk
         | 
| 39 | 
            -
                        else
         | 
| 40 | 
            -
                          raise "Unknown file extension: #{file_extension}"
         | 
| 41 | 
            -
                        end
         | 
| 42 | 
            -
                      end
         | 
| 43 | 
            -
                    end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                    private
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                    def handle_xcarchive
         | 
| 48 | 
            -
                      raise 'Path must be an xcarchive' unless @options[:path].end_with?('.xcarchive')
         | 
| 49 | 
            -
             | 
| 50 | 
            -
                      app_path = Dir.glob("#{@options[:path]}/Products/Applications/*.app").first
         | 
| 51 | 
            -
                      run_codesign_check(app_path)
         | 
| 52 | 
            -
                      read_provisioning_profile(app_path)
         | 
| 53 | 
            -
                    end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                    def handle_ipa
         | 
| 56 | 
            -
                      raise 'Path must be an IPA' unless @options[:path].end_with?('.ipa')
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                      Dir.mktmpdir do |tmp_dir|
         | 
| 59 | 
            -
                        Zip::File.open(@options[:path]) do |zip_file|
         | 
| 60 | 
            -
                          zip_file.each do |entry|
         | 
| 61 | 
            -
                            entry.extract(File.join(tmp_dir, entry.name))
         | 
| 62 | 
            -
                          end
         | 
| 63 | 
            -
                        end
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                        app_path = File.join(tmp_dir, 'Payload/*.app')
         | 
| 66 | 
            -
                        app_path = Dir.glob(app_path).first
         | 
| 67 | 
            -
                        run_codesign_check(app_path)
         | 
| 68 | 
            -
                        read_provisioning_profile(app_path)
         | 
| 69 | 
            -
                      end
         | 
| 70 | 
            -
                    end
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                    def handle_app
         | 
| 73 | 
            -
                      raise 'Path must be an app' unless @options[:path].end_with?('.app')
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                      app_path = @options[:path]
         | 
| 76 | 
            -
                      run_codesign_check(app_path)
         | 
| 77 | 
            -
                      read_provisioning_profile(app_path)
         | 
| 78 | 
            -
                    end
         | 
| 79 | 
            -
             | 
| 80 | 
            -
                    def handle_apk
         | 
| 81 | 
            -
                      raise 'Path must be an APK' unless @options[:path].end_with?('.apk')
         | 
| 82 | 
            -
             | 
| 83 | 
            -
                      apk_path = @options[:path]
         | 
| 84 | 
            -
                      check_supported_abis(apk_path)
         | 
| 85 | 
            -
                    end
         | 
| 86 | 
            -
             | 
| 87 | 
            -
                    def run_codesign_check(app_path)
         | 
| 88 | 
            -
                      unless RbConfig::CONFIG['host_os'] =~ /darwin/i
         | 
| 89 | 
            -
                        Logger.info 'Skipping codesign check on non-macOS platform'
         | 
| 90 | 
            -
                        return
         | 
| 91 | 
            -
                      end
         | 
| 92 | 
            -
             | 
| 93 | 
            -
                      command = "codesign -dvvv '#{app_path}'"
         | 
| 94 | 
            -
                      Logger.debug command
         | 
| 95 | 
            -
                      stdout, _, status = Open3.capture3(command)
         | 
| 96 | 
            -
                      Logger.debug stdout
         | 
| 97 | 
            -
                      raise '❌ Codesign check failed' unless status.success?
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                      Logger.info '✅ Codesign check passed'
         | 
| 100 | 
            -
                    end
         | 
| 101 | 
            -
             | 
| 102 | 
            -
                    def read_provisioning_profile(app_path)
         | 
| 103 | 
            -
                      entitlements_path = File.join(app_path, 'embedded.mobileprovision')
         | 
| 104 | 
            -
                      raise '❌ Entitlements file not found' unless File.exist?(entitlements_path)
         | 
| 105 | 
            -
             | 
| 106 | 
            -
                      content = File.read(entitlements_path)
         | 
| 107 | 
            -
                      lines = content.lines
         | 
| 108 | 
            -
             | 
| 109 | 
            -
                      buffer = ''
         | 
| 110 | 
            -
                      inside_plist = false
         | 
| 111 | 
            -
                      lines.each do |line|
         | 
| 112 | 
            -
                        inside_plist = true if line.include? PLIST_START
         | 
| 113 | 
            -
                        if inside_plist
         | 
| 114 | 
            -
                          buffer << line
         | 
| 115 | 
            -
                          break if line.include? PLIST_STOP
         | 
| 116 | 
            -
                        end
         | 
| 117 | 
            -
                      end
         | 
| 118 | 
            -
             | 
| 119 | 
            -
                      encoded_plist = buffer.encode(UTF8_ENCODING, STRING_FORMAT, invalid: :replace, undef: :replace,
         | 
| 120 | 
            -
                                                                                  replace: EMPTY_STRING)
         | 
| 121 | 
            -
                      encoded_plist = encoded_plist.sub(/#{PLIST_STOP}.+/, PLIST_STOP)
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                      plist = CFPropertyList::List.new(data: encoded_plist)
         | 
| 124 | 
            -
                      parsed_data = CFPropertyList.native_types(plist.value)
         | 
| 125 | 
            -
             | 
| 126 | 
            -
                      expiration_date = parsed_data['ExpirationDate']
         | 
| 127 | 
            -
                      if expiration_date > Time.now
         | 
| 128 | 
            -
                        Logger.info '✅ Provisioning profile hasn\'t expired'
         | 
| 129 | 
            -
                      else
         | 
| 130 | 
            -
                        Logger.info "❌ Provisioning profile is expired. Expiration date: #{expiration_date}"
         | 
| 131 | 
            -
                      end
         | 
| 132 | 
            -
             | 
| 133 | 
            -
                      provisions_all_devices = parsed_data['ProvisionsAllDevices']
         | 
| 134 | 
            -
                      if provisions_all_devices
         | 
| 135 | 
            -
                        Logger.info 'Provisioning profile supports all devices (likely an enterprise profile)'
         | 
| 136 | 
            -
                      else
         | 
| 137 | 
            -
                        devices = parsed_data['ProvisionedDevices']
         | 
| 138 | 
            -
                        Logger.info 'Provisioning profile does not support all devices (likely a development profile).'
         | 
| 139 | 
            -
                        Logger.info "Devices: #{devices.inspect}"
         | 
| 140 | 
            -
                      end
         | 
| 141 | 
            -
                    end
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                    def check_supported_abis(apk_path)
         | 
| 144 | 
            -
                      abis = []
         | 
| 145 | 
            -
             | 
| 146 | 
            -
                      Zip::File.open(apk_path) do |zip_file|
         | 
| 147 | 
            -
                        zip_file.each do |entry|
         | 
| 148 | 
            -
                          if entry.name.start_with?('lib/') && entry.name.count('/') == 2
         | 
| 149 | 
            -
                            abi = entry.name.split('/')[1]
         | 
| 150 | 
            -
                            abis << abi unless abis.include?(abi)
         | 
| 151 | 
            -
                          end
         | 
| 152 | 
            -
                        end
         | 
| 153 | 
            -
                      end
         | 
| 154 | 
            -
             | 
| 155 | 
            -
                      unless abis.include?(EXPECTED_ABI)
         | 
| 156 | 
            -
                        raise "APK does not support #{EXPECTED_ABI} architecture, found: #{abis.join(', ')}"
         | 
| 157 | 
            -
                      end
         | 
| 158 | 
            -
             | 
| 159 | 
            -
                      Logger.info "✅ APK supports #{EXPECTED_ABI} architecture"
         | 
| 160 | 
            -
                    end
         | 
| 161 | 
            -
                  end
         | 
| 162 | 
            -
                end
         | 
| 163 | 
            -
              end
         | 
| 164 | 
            -
            end
         |