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)