module Xccleanup

Constants

VERSION

Public Class Methods

center(text) click to toggle source
# File lib/xccleanup.rb, line 17
def self.center(text)
        if text.length > 0
                puts (' ' * (($line_length - text.length) / 2)) + text
        end
end
get_byte_size(file_or_dir) click to toggle source
# File lib/xccleanup.rb, line 49
def self.get_byte_size(file_or_dir)
        file_or_dir = file_or_dir
        if File.file? file_or_dir
                return File.stat(file_or_dir).size
        else
                cmd = "du -ks '#{file_or_dir}'"
                return (`#{cmd}`).split("\t").first.to_i * 1024
        end
end
get_files_in_dir(dir) click to toggle source
# File lib/xccleanup.rb, line 42
def self.get_files_in_dir(dir)
        if File.directory?(dir)
                return Dir.entries(dir).select {|entry| File.file? File.join(dir,entry)}.map {|entry| File.join(dir,entry)}
        end
        return []
end
get_folders_in_dir(dir) click to toggle source
# File lib/xccleanup.rb, line 35
def self.get_folders_in_dir(dir)
        if File.directory?(dir)
                return Dir.entries(dir).select {|entry| File.directory? File.join(dir,entry) and !(entry[0] =='.')}.map {|entry| File.join(dir,entry)}
        end
        return []
end
line(char) click to toggle source
# File lib/xccleanup.rb, line 13
def self.line(char)
        puts char * $line_length
end
menu() click to toggle source
pbs(bytes) click to toggle source
# File lib/xccleanup.rb, line 59
def self.pbs(bytes)    # pretty byte size
        {
          'B'  => 1024,
          'KB' => 1024 * 1024,
          'MB' => 1024 * 1024 * 1024,
          'GB' => 1024 * 1024 * 1024 * 1024,
          'TB' => 1024 * 1024 * 1024 * 1024 * 1024
        }.each do |e, s| 
                return "#{(bytes.to_f / (s / 1024)).round(1)} #{e}" if bytes < s
        end
end
prompt(*args) click to toggle source
# File lib/xccleanup.rb, line 23
def self.prompt(*args)
    print(*args)
    gets
end
prompt_bool(message) click to toggle source
# File lib/xccleanup.rb, line 28
def self.prompt_bool(message)
        message = message + " (y/n) "
        input = prompt message
        input = input.strip!
        return input.casecmp("y") == 0 || input.casecmp("yes") == 0
end
remove_derived_data(manually) click to toggle source

Cleanup functions

# File lib/xccleanup.rb, line 74
def self.remove_derived_data(manually)
        saved_bytes = 0

        dd_dir = File.expand_path('~/Library/Developer/Xcode/DerivedData')
        dd_folders = get_folders_in_dir(dd_dir).sort_by{ |d| File.mtime(d) }.reverse
        if dd_folders.length > 1
                
                recent_projects = 0
                if manually
                        recent_projects = prompt "> KEEP how many most recent projects? "
                        recent_projects = recent_projects.to_i
                end

                kept = 0
                dd_folders.each do |folder_path|
                        folder_name = folder_path.split('/').last
                        if folder_name != 'ModuleCache'
                                project_name = folder_name.rindex('-').nil? ? folder_name : folder_name[0,folder_name.rindex('-')]
                                project_size = get_byte_size(folder_path)
                                if kept < recent_projects
                                        puts "- Keeping #{project_name} (#{pbs(project_size)})"
                                else
                                        puts "- Removing #{project_name} (#{pbs(project_size)})"
                                        FileUtils.rm_rf(folder_path)
                                        saved_bytes += project_size
                                end
                                kept += 1
                        end
                end
        else
                puts "Skipping, no cleanup needed."
        end

        return saved_bytes
end
remove_device_support(manually) click to toggle source
# File lib/xccleanup.rb, line 127
def self.remove_device_support(manually)
        saved_bytes = 0

        ds_folder = File.expand_path('~/Library/Developer/Xcode/iOS DeviceSupport/')
        ds_versions = get_folders_in_dir(ds_folder).select { |folder| Gem::Version.correct?(folder.split('/').last.split(' ').first) }

        if ds_versions.length > 0
                puts "Found versions:"
                ds_versions.each do |version_folder|
                        version_size = get_byte_size(version_folder)
                        version_name = version_folder.split('/').last
                        puts "• #{version_name} (#{pbs(version_size)})"
                end

                min_version = Gem::Version.new('9999.9.9')
                if manually
                        min_version = prompt "> Miminum version to KEEP? "
                        min_version = Gem::Version.new(min_version)
                end

                unless min_version.nil?
                        ds_versions.each do |version_folder|
                                version_name = version_folder.split('/').last
                                version_number = version_name.split(' ').first
                                version_number = Gem::Version.new(version_number)
                                unless version_number.nil?
                                        if version_number < min_version
                                                puts "- Removing #{version_name}"
                                                saved_bytes += get_byte_size(version_folder)
                                                FileUtils.rm_rf(version_folder)
                                        else
                                                puts "- Keeping #{version_name}"
                                        end
                                end
                        end
                end
        end

        return saved_bytes
end
remove_doc_sets(manually) click to toggle source
# File lib/xccleanup.rb, line 357
def self.remove_doc_sets(manually)
        saved_bytes = 0

        ds_dir = File.expand_path('~/Library/Developer/Shared/Documentation/DocSets')
        ds_folders = get_folders_in_dir(ds_dir)

        ds_folders.each do |ds_folder|
                docset_size = get_byte_size(ds_folder)
                docset_name = ds_folder.split('/').last
                if docset_name.split('.').last == 'docset'
                        if !manually || prompt_bool("> REMOVE \"#{docset_name}\" (#{pbs(docset_size)})?")
                                FileUtils.rm_rf(ds_folder)
                                saved_bytes += docset_size
                        end
                end
        end

        return saved_bytes
end
remove_expired_provisioning_profiles(manually) click to toggle source
# File lib/xccleanup.rb, line 270
def self.remove_expired_provisioning_profiles(manually)
        saved_bytes = 0

        pp_folder = File.expand_path('~/Library/MobileDevice/Provisioning Profiles')
        profiles = get_files_in_dir(pp_folder)

        today = Date.today

        profiles.each do |profile|
                escaped_profile = profile.gsub(/ /, '\ ')
                filename = profile.split('/').last
                ext = filename.split('.').last
                if ext == 'mobileprovision'
                        cmd = "security cms -D -i #{escaped_profile}"
                        plist = `#{cmd}`

                        exp_date_pos = plist.index('<key>ExpirationDate</key>')
                        unless exp_date_pos.nil?
                                exp_date_pos = plist.index('<date>', exp_date_pos)
                                unless exp_date_pos.nil?
                                        exp_date_pos = exp_date_pos + 6 # <date>
                                        exp_date_end = plist.index('</date>', exp_date_pos)
                                        unless exp_date_end.nil?
                                                exp_date_str = plist[exp_date_pos, exp_date_end - exp_date_pos]
                                                exp_date = Date.parse(exp_date_str)
                                                if today > exp_date
                                                        puts "- Removing #{filename} (#{exp_date})"
                                                        saved_bytes += get_byte_size(profile)
                                                        FileUtils.rm(profile)
                                                else
                                                        puts "- Skipping #{filename} (#{exp_date})"
                                                end
                                        end
                                end
                        end
                end
        end

        return saved_bytes
end
remove_module_cache(manually) click to toggle source
# File lib/xccleanup.rb, line 110
def self.remove_module_cache(manually)
        saved_bytes = 0

        puts "Removing Module Cache..."
        
        dd_dir = File.expand_path('~/Library/Developer/Xcode/DerivedData')
        path = File.join(dd_dir,'ModuleCache')
        saved_bytes = get_byte_size(path)

        get_folders_in_dir(path).each do |folder|
                FileUtils.rm_rf(folder)
        end

        return saved_bytes
end
remove_old_archives(manually) click to toggle source
# File lib/xccleanup.rb, line 169
def self.remove_old_archives(manually)
        saved_bytes = 0

        arch_folder = File.expand_path('~/Library/Developer/Xcode/Archives/')
        arch_date_folders = get_folders_in_dir(arch_folder)

        bundle_ids = {}

        arch_date_folders.each do |arch_date_folder|
                date_name = arch_date_folder.split('/').last

                archives = get_folders_in_dir(arch_date_folder)


                if archives.length == 0
                        puts "- Empty archives subfolder #{date_name}, removing..."
                        FileUtils.rm_rf(arch_date_folder)
                else
                        archives.each do |archive|
                                plist_path = File.join(archive,'Info.plist')
                                if File.file?(plist_path)
                                        plist = File.read(plist_path)

                                        bundle_id_pos = plist.index('<key>CFBundleIdentifier</key>')
                                        unless bundle_id_pos.nil?
                                                bundle_id_pos = plist.index('<string>', bundle_id_pos)
                                                unless bundle_id_pos.nil?
                                                        bundle_id_pos = bundle_id_pos + 8 # <string>
                                                        bundle_id_end = plist.index('</string>', bundle_id_pos)
                                                        unless bundle_id_end.nil?
                                                                bundle_id = plist[bundle_id_pos, bundle_id_end - bundle_id_pos]
                                                                if bundle_ids[bundle_id].nil?
                                                                        bundle_ids[bundle_id] = {date_name => archive }
                                                                else
                                                                        bundle_ids[bundle_id][date_name] = archive
                                                                end
                                                        end
                                                end
                                        end
                                end
                        end
                end
        end

        skip_single_archives = false
        if manually
                skip_single_archives = prompt_bool("Skip all bundle id's for which only a single archive is present?")
        end

        bundle_ids.each do |bundle_id, dates|
                if dates.length > 1
                        dates = dates.sort_by { |date, archive| Date.parse(date) }.reverse
                        puts "• #{dates.length} archives for \"#{bundle_id}\":"
                        arch_index = 1
                        dates.each do |date, archive|
                                archive_size = get_byte_size(archive)
                                puts "  #{arch_index}: #{date} (#{pbs(archive_size)})"
                                arch_index += 1
                        end
                        
                        recent = 0
                        if manually
                                recent = prompt("> KEEP how many most recent? ").to_i
                        end

                        if recent < dates.length
                                kept = 0
                                dates.each do |date, archive|
                                        if kept >= recent
                                                archive_size = get_byte_size(archive)
                                                puts "- Removing #{date}"
                                                FileUtils.rm_rf(archive)
                                                saved_bytes += archive_size
                                        else
                                                puts "- Keeping #{date}"
                                        end
                                        kept += 1
                                end
                        end
                elsif !skip_single_archives
                        date, archive = dates.first
                        archive_size = get_byte_size(archive)

                        remove = !manually
                        if manually
                                remove = prompt_bool("• 1 archive for \"#{bundle_id}\" (#{date}, #{pbs(archive_size)})\n> REMOVE it?")
                        else
                                puts "• Removing 1 archive for \"#{bundle_id}\" (#{date}, #{pbs(archive_size)})"
                        end

                        if remove
                                FileUtils.rm_rf(archive)
                                saved_bytes += archive_size
                        end
                end
        end

        return saved_bytes
end
remove_simulator_devices(manually) click to toggle source
# File lib/xccleanup.rb, line 312
def self.remove_simulator_devices(manually)
        saved_bytes = 0

        sd_dir = File.expand_path('~/Library/Developer/CoreSimulator/Devices')

        devices_output = `xcrun simctl list devices`
        devices = devices_output.scan /\s\s\s\s(.*) \(([^)]+)\) (.*)/
        devices.each do |device|
                device_uuid = nil
                device.each do |device_component|
                        device_uuid = /[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}/.match(device_component.downcase)
                        
                        unless device_uuid.nil?
                                device_uuid = device_uuid[0].upcase
                                break
                        end
                end

                unavailable = false
                device.each do |device_component|
                        if device_component.include? 'unavailable'
                                unavailable = true
                                break
                        end
                end

                path = File.join(sd_dir, device_uuid)
                byte_size = get_byte_size(path)

                if unavailable || !manually || prompt_bool("> REMOVE #{device[0]} (#{pbs(byte_size)})?")
                        if unavailable
                                puts "! Unavailable device found: #{device[0]} (#{pbs(byte_size)})"
                        end

                      puts "- Removing device #{device[0]}"
                      `xcrun simctl delete #{device_uuid}`
                      saved_bytes += byte_size
              end
        end

        return saved_bytes
end
run_steps(steps, manually) click to toggle source

Execution and Menu

# File lib/xccleanup.rb, line 380
def self.run_steps(steps, manually)
        total_saved_bytes = 0

        steps.each do |method|
                puts "\n"
                line('-')
                method_name = "#{method.name}".gsub('_', ' ').capitalize
                center(method_name.upcase)
                line('-')
                saved_bytes = method.call(manually)
                line('.')
                center("#{method_name} completed, #{pbs(saved_bytes)} removed.")
                total_saved_bytes += saved_bytes
        end

        line('–')
        emoji = '😞'
        if total_saved_bytes > 0
                emoji = '👍'
        end
        mb = 1024 * 1024
        if total_saved_bytes > (100 * mb)
                emoji = '💪'
        end
        if total_saved_bytes > (5000 * mb)
                emoji = '🍾'
        end
        if total_saved_bytes > (10000 * mb)
                emoji = '💥'
        end
        center("🎉  XCODE CLEANUP completed, #{pbs(total_saved_bytes)} removed. #{emoji}")
        line('–')
end