upd

upd - update all of your package systems on macOS at once.
git clone git://git.alex.balgavy.eu/upd.git
Log | Files | Refs | README | LICENSE

upd (5288B)


      1 #!/usr/bin/env ruby
      2 # frozen_string_literal: true
      3 
      4 def check_deps(deps)
      5   deps.each do |dep|
      6     unless system("command -v #{dep} >/dev/null 2>&1")
      7       warn "error: command #{dep} not found"
      8       exit 1
      9     end
     10   end
     11 end
     12 
     13 # The Homebrew package manager for macOS
     14 class Homebrew
     15   def initialize
     16     check_deps ['brew']
     17     puts '-- HOMEBREW --'
     18     2.times { system 'brew update' }
     19     @outdated_casks = `brew outdated --cask --greedy --verbose`.lines.map(&:chomp)
     20     @cask_names = @outdated_casks.map { |c| c.split.first }
     21     @outdated_formulae = `brew outdated --formula`
     22   end
     23 
     24   def start_sudo_cache
     25     puts 'Please enter the administrator password to install cask upgrades.'
     26     system 'sudo -v'
     27     @sudo_cache_pid = spawn 'while :; do sudo -n true; sleep 60; done 2>/dev/null'
     28   end
     29 
     30   def prompt_user
     31     return if @outdated_casks.empty?
     32 
     33     puts "\nOutdated casks:"
     34     @outdated_casks.each_with_index { |c, i| puts "#{i} #{c}" }
     35     print 'Numbers of casks to upgrade (space-separated): '
     36     begin
     37       @to_upgrade = $stdin.gets.chomp.split.map { |casknum| @cask_names[Integer(casknum)] }
     38     rescue ArgumentError
     39       warn 'Only numbers allowed'
     40       exit 1
     41     end
     42     start_sudo_cache unless @to_upgrade.empty?
     43   end
     44 
     45   def do_upgrade
     46     if @outdated_formulae.empty?
     47       puts "\nNo formulae to upgrade."
     48     else
     49       puts "\nUpgrading all formulae.\nTo upgrade:"
     50       puts @outdated_formulae
     51       system 'brew upgrade --formula'
     52     end
     53 
     54     return if @outdated_casks.empty?
     55 
     56     if @to_upgrade.empty?
     57       puts 'Not upgrading any casks.'
     58     else
     59       puts "\nUpgrading casks:"
     60       puts @to_upgrade
     61 
     62       @to_upgrade.each { |cask| system "brew upgrade --cask --greedy --verbose #{cask}" }
     63       puts 'Upgrade complete.'
     64     end
     65   end
     66 
     67   def cleanup
     68     Process.kill('TERM', @sudo_cache_pid) if @sudo_cache_pid
     69     system 'brew cleanup -s --prune-prefix'
     70     system 'brew cleanup'
     71     system 'brew doctor'
     72     system 'brew missing'
     73 
     74     puts "Homebrew cache disk usage: #{`du -skh "$(brew --cache)" | cut -f1`}"
     75     puts "Remove with:\trm -rf $(brew --cache)"
     76     brewfile = (ENV['DOTFILES'].nil? ? "#{ENV['HOME']}/Brewfile" : "#{ENV['DOTFILES']}/Brewfile")
     77     system "brew bundle dump --force --file=#{brewfile};"
     78   end
     79 end
     80 
     81 # Pipx for Python
     82 class Pipx
     83   def initialize
     84     check_deps ['pipx']
     85     puts '-- PIPX --'
     86   end
     87 
     88   def do_upgrade
     89     system 'pipx upgrade-all'
     90   end
     91 end
     92 
     93 # Gems for Ruby
     94 class RubyGems
     95   def initialize
     96     check_deps ['gem']
     97     puts '-- GEM --'
     98     puts 'To upgrade:'
     99     system 'gem outdated'
    100   end
    101 
    102   def do_upgrade
    103     system 'gem update'
    104   end
    105 
    106   def cleanup
    107     system 'gem cleanup'
    108   end
    109 end
    110 
    111 # NPM for JS
    112 class NPM
    113   def initialize
    114     check_deps ['npm']
    115     puts '-- NPM --'
    116     puts 'To upgrade:'
    117     system 'npm -g outdated'
    118   end
    119 
    120   def do_upgrade
    121     outdated = `npm outdated -g --parseable | sed 's!:.*!!g; s!.*/!!'`.split
    122     outdated.each { |pkg| system "npm -g install #{pkg}@latest" }
    123     system 'npm -g upgrade'
    124   end
    125 end
    126 
    127 # Rust
    128 class Rustup
    129   def initialize
    130     check_deps ['rustup']
    131     puts '-- Rustup --'
    132   end
    133 
    134   def do_upgrade
    135     system 'rustup self update'
    136     system 'rustup update'
    137   end
    138 end
    139 
    140 class Cargo
    141   def initialize
    142     check_deps ['cargo-install-update']
    143     puts '-- Cargo --'
    144   end
    145   def do_upgrade
    146     system 'cargo-install-update install-update --git --all'
    147   end
    148 end
    149 
    150 # Haskell stack
    151 class Stack
    152   def initialize
    153     check_deps ['stack']
    154     puts '-- Stack --'
    155   end
    156 
    157   def do_upgrade
    158     system 'stack upgrade'
    159   end
    160 end
    161 
    162 # Texlive
    163 class Tlmgr
    164   def initialize
    165     check_deps ['tlmgr']
    166     puts '-- Tlmgr --'
    167   end
    168 
    169   def do_upgrade
    170     system 'sudo tlmgr update --self --all'
    171   end
    172 end
    173 
    174 def do_prompts(systems)
    175   system_instances = systems.map(&:new)
    176   system_instances.each(&:prompt_user) if (['-n', '--dry-run'] & ARGV).empty?
    177   system_instances
    178 end
    179 
    180 def split_systems_by_prompting(all_systems)
    181   systems_with_prompt = all_systems.select { |s| s.method_defined? 'prompt_user' }
    182   { prompting: systems_with_prompt,
    183     no_prompting: all_systems - systems_with_prompt }
    184 end
    185 
    186 def upgrade_cleanup(system_instance)
    187   system_instance.do_upgrade if (['-n', '--dry-run'] & ARGV).empty?
    188   system_instance.cleanup if system_instance.class.method_defined? 'cleanup'
    189 end
    190 
    191 def main
    192   package_systems = [Homebrew, Cargo, NPM, Pipx, RubyGems, Rustup, Stack]#, Tlmgr] # TODO: tlmgr needs sudo
    193 
    194   split_systems = split_systems_by_prompting package_systems
    195 
    196   # Do any prompts to get interactivity out of the way, then upgrade those first
    197   do_prompts(split_systems[:prompting]).each { |s| upgrade_cleanup s } unless split_systems[:prompting].empty?
    198 
    199   # Finish up by upgrading systems that don't need user input
    200   split_systems[:no_prompting].each do |nonprompting_sys|
    201     pksys = nonprompting_sys.new
    202     upgrade_cleanup pksys
    203   end
    204 end
    205 
    206 def handle_help
    207   return if (['-h', '--help'] & ARGV).empty?
    208 
    209   puts <<~END_HELPTEXT
    210     Usage:  upd [options]
    211 
    212     Options:
    213     -h, --help    display this helptext
    214     -n, --dry-run   don't upgrade anything, only update & cleanup
    215   END_HELPTEXT
    216   exit 0
    217 end
    218 
    219 handle_help
    220 begin
    221   caffeinate = spawn 'caffeinate -d'
    222   main
    223 rescue SystemExit, Interrupt
    224   puts "\nInterrupted, exiting..."
    225 end
    226 Process.kill('SIGTERM', caffeinate)
    227 Process.detach(caffeinate)