├── .gitignore ├── .project ├── LICENSE ├── README.md ├── brew-rmtree.rb └── cmd └── rmtree.rb /.gitignore: -------------------------------------------------------------------------------- 1 | local_deploy.sh 2 | 3 | # Created by http://www.gitignore.io 4 | 5 | ### OSX ### 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear on external disk 18 | .Spotlight-V100 19 | .Trashes 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | 29 | ### PyCharm ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 31 | 32 | ## Directory-based project format 33 | .idea/ 34 | # if you remove the above rule, at least ignore user-specific stuff: 35 | # .idea/workspace.xml 36 | # .idea/tasks.xml 37 | # and these sensitive or high-churn files: 38 | # .idea/dataSources.ids 39 | # .idea/dataSources.xml 40 | # .idea/sqlDataSources.xml 41 | # .idea/dynamic.xml 42 | 43 | ## File-based project format 44 | *.ipr 45 | *.iml 46 | *.iws 47 | 48 | ## Additional for IntelliJ 49 | out/ 50 | 51 | # generated by mpeltonen/sbt-idea plugin 52 | .idea_modules/ 53 | 54 | # generated by JIRA plugin 55 | atlassian-ide-plugin.xml 56 | 57 | # generated by Crashlytics plugin (for Android Studio and Intellij) 58 | com_crashlytics_export_strings.xml 59 | 60 | 61 | ### Eclipse ### 62 | *.pydevproject 63 | .metadata 64 | .gradle 65 | bin/ 66 | tmp/ 67 | *.tmp 68 | *.bak 69 | *.swp 70 | *~.nib 71 | local.properties 72 | .settings/ 73 | .loadpath 74 | 75 | # External tool builders 76 | .externalToolBuilders/ 77 | 78 | # Locally stored "Eclipse launch configurations" 79 | *.launch 80 | 81 | # CDT-specific 82 | .cproject 83 | 84 | # PDT-specific 85 | .buildpath 86 | 87 | # sbteclipse plugin 88 | .target 89 | 90 | # TeXlipse plugin 91 | .texlipse 92 | 93 | 94 | ### Ruby ### 95 | *.gem 96 | *.rbc 97 | /.config 98 | /coverage/ 99 | /InstalledFiles 100 | /pkg/ 101 | /spec/reports/ 102 | /test/tmp/ 103 | /test/version_tmp/ 104 | /tmp/ 105 | 106 | ## Specific to RubyMotion: 107 | .dat* 108 | .repl_history 109 | build/ 110 | 111 | ## Documentation cache and generated files: 112 | /.yardoc/ 113 | /_yardoc/ 114 | /doc/ 115 | /rdoc/ 116 | 117 | ## Environment normalisation: 118 | /.bundle/ 119 | /lib/bundler/man/ 120 | 121 | # for a library or gem, you might want to ignore these files since the code is 122 | # intended to run in multiple environments; otherwise, check them in: 123 | # Gemfile.lock 124 | .ruby-version 125 | .ruby-gemset 126 | 127 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 128 | .rvmrc 129 | 130 | 131 | ### Windows ### 132 | # Windows image file caches 133 | Thumbs.db 134 | ehthumbs.db 135 | 136 | # Folder config file 137 | Desktop.ini 138 | 139 | # Recycle Bin used on file shares 140 | $RECYCLE.BIN/ 141 | 142 | # Windows Installer files 143 | *.cab 144 | *.msi 145 | *.msm 146 | *.msp 147 | 148 | 149 | ### Linux ### 150 | *~ 151 | 152 | # KDE directory preferences 153 | .directory 154 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | homebrew-rmtree 4 | 5 | 6 | 7 | 8 | 9 | com.aptana.ide.core.unifiedBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.aptana.projects.webnature 16 | com.aptana.ruby.core.rubynature 17 | 18 | 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Casey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | homebrew-rmtree 2 | =============== 3 | 4 | Remove a formula and its unused dependencies 5 | 6 | ## What is it? 7 | 8 | It's an [external command][ec] for [Homebrew][h] that provides a new command, `rmtree`, 9 | that will uninstall that formula, and uninstall any of its dependencies 10 | that have no formula left installed that depend on them. The command will check all dependencies 11 | recursively starting at the one specified on the command line. 12 | 13 | This is tricky business. So this command comes with a warning. 14 | 15 | [ec]: https://github.com/mxcl/homebrew/wiki/External-Commands 16 | [h]: https://github.com/mxcl/homebrew 17 | 18 | ### Warning 19 | 20 | There are formulae that do not specify all of their dependencies. This means that it is possible that 21 | this command will remove something you still need or won't remove something you no longer want. Generally, it is pretty good. 22 | Until someone comes up with a clever way around this, you need to be careful what you uninstall. 23 | A formula could also depend on something you want to keep around, while nothing else actually 24 | depends on it (except you). See Usage to ignore certain formula from being removed. 25 | 26 | ## Installation 27 | 28 | Tap this repository and install via `brew` itself. 29 | 30 | ``` 31 | $ brew tap beeftornado/rmtree 32 | ``` 33 | 34 | Once you've tapped it, you can use the command as described above. 35 | 36 | ## Usage 37 | 38 | Although the script's name is `brew-rmtree.rb`, [Homebrew external 39 | commands][ec] work in such a way that you invoke it as `brew rmtree`. (It 40 | functions exactly like a sub-command built into Homebrew.) 41 | 42 | ### Examples 43 | 44 | Typical use case, will remove `mpv` 45 | 46 | ``` 47 | $ brew rmtree mpv 48 | ==> Examining installed formulae required by mpv... 49 | - 43 / 43 50 | 51 | Can safely be removed 52 | ---------------------- 53 | automake 54 | lua 55 | mpg123 56 | mpv-player/mpv/libass-ct 57 | 58 | Proceed?[y/N]: y 59 | ==> Cleaning up packages safe to remove 60 | 61 | Uninstalling /usr/local/Cellar/mpv/0.9.2... (342 files, 35M) 62 | 63 | Uninstalling /usr/local/Cellar/automake/1.15... (130 files, 3.2M) 64 | 65 | Uninstalling /usr/local/Cellar/libass-ct/HEAD... (9 files, 440K) 66 | 67 | Uninstalling /usr/local/Cellar/lua/5.2.4... (81 files, 1.1M) 68 | 69 | Uninstalling /usr/local/Cellar/mpg123/1.22.2... (16 files, 656K) 70 | ``` 71 | 72 | Trying to remove something required by something else 73 | 74 | ``` 75 | $ brew rmtree python 76 | python can't be removed because other formula depend on it: 77 | mpv-player/mpv/mpv, newt, node, postgresql, sip, yasm 78 | $ brew rmtree --force python 79 | ... (I'm not going to run this but it would remove python) 80 | ``` 81 | 82 | Want to see what will happen without making any changes? 83 | 84 | ``` 85 | $ brew rmtree --dry-run mpv 86 | This is a dry-run, nothing will be deleted 87 | ==> Examining installed formulae required by mpv... 88 | - 43 / 43 89 | 90 | Can safely be removed 91 | ---------------------- 92 | automake 93 | lua 94 | mpg123 95 | mpv-player/mpv/libass-ct 96 | 97 | Won't be removed 98 | ----------------- 99 | autoconf is used by pyenv, homebrew/dupes/rsync 100 | cairo is used by pango 101 | cmake is used by eigen, mysql, homebrew/science/opencv, zbackup 102 | faac is used by ffmpeg 103 | ffmpeg is used by homebrew/science/opencv 104 | fontconfig is used by imagemagick, pango 105 | freetype is used by graphviz, imagemagick 106 | fribidi is used by libass 107 | gettext is used by newt 108 | git is used by homebrew/headonly/arcanist, caskroom/cask/brew-cask, beeftornado/rmtree/brew-rmtree, go, gobject-introspection, mongodb, x264 109 | glib is used by atk, gdk-pixbuf, pango 110 | gobject-introspection is used by atk, gdk-pixbuf, gtk+, pango 111 | harfbuzz is used by pango 112 | icu4c is used by node, sqlite 113 | jpeg is used by gdk-pixbuf, imagemagick, jasper, homebrew/science/opencv, wxmac 114 | lame is used by ffmpeg 115 | libass is used by ffmpeg 116 | libffi is used by glib 117 | libgpg-error is used by libksba 118 | libogg is used by libvorbis 119 | libpng is used by gdk-pixbuf, graphviz, imagemagick, homebrew/science/opencv, pngquant, s-lang, wxmac 120 | libtiff is used by gdk-pixbuf, imagemagick, homebrew/science/opencv, wxmac 121 | libtool is used by imagemagick 122 | libvo-aacenc is used by ffmpeg 123 | libvorbis is used by ffmpeg 124 | libvpx is used by ffmpeg 125 | little-cms2 is used by imagemagick 126 | openssl is used by freetds, libevent, mongodb, mysql, node, postgresql, wget, zbackup 127 | pixman is used by cairo 128 | pkg-config is used by atk, cloog, homebrew/versions/cloog018, freetds, gdk-pixbuf, graphviz, gtk+, imagemagick, libevent, node, homebrew/science/opencv, openexr, pango, pngquant, pyenv, tmux 129 | python is used by newt, node, postgresql, sip, yasm 130 | texi2html is used by ffmpeg 131 | webp is used by imagemagick 132 | x264 is used by ffmpeg 133 | x265 is used by ffmpeg 134 | xvid is used by ffmpeg 135 | xz is used by atk, coreutils, gdk-pixbuf, gtk+, hicolor-icon-theme, imagemagick, isl, mpfr, nasm, pango, watch, wget, zbackup 136 | yasm is used by ffmpeg 137 | ``` 138 | 139 | ## Options 140 | 141 | Option | Description 142 | -------|------------ 143 | `--force` | Overrides the dependency check for just the top-level formula you are trying to remove. If you try to remove 'ruby' for example, you most likely will not be able to do this because other fomulae specify this as a dependency. This option will let you remove 'ruby'. This will NOT bypass dependency checks for the formula's children. If 'ruby' depends on 'git', then 'git' will still not be removed. 144 | `--ignore` | Ignore some dependencies from removal. This option must appear after the formulae to remove. 145 | `--dry-run` | Does a dry-run. Goes through the whole process without actually removing anything. This gives you a chance to observe what packages would be removed and a chance to ignore them when you do it for real. 146 | `--quiet` | No output 147 | 148 | -------------------------------------------------------------------------------- /brew-rmtree.rb: -------------------------------------------------------------------------------- 1 | class BrewRmtree < Formula 2 | homepage "https://github.com/beeftornado/homebrew-rmtree" 3 | url "https://github.com/beeftornado/homebrew-rmtree.git", :tag => "2.2.6" 4 | revision 1 5 | 6 | head "https://github.com/beeftornado/homebrew-rmtree.git" 7 | 8 | def install 9 | bin.install "cmd/brew-rmtree.rb" 10 | end 11 | 12 | def caveats 13 | <<~EOS 14 | You can uninstall this formula, as `brew tap beeftornado/brew-rmtree` is all that's 15 | needed to install Rmtree and keep it up to date. 16 | EOS 17 | end 18 | 19 | test do 20 | system "brew", "rmtree", "--help" 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /cmd/rmtree.rb: -------------------------------------------------------------------------------- 1 | require 'keg' 2 | require 'formula' 3 | require 'formulary' 4 | require 'dependencies' 5 | require 'shellwords' 6 | require 'set' 7 | require 'pathname' 8 | require 'cmd/deps' 9 | require 'cmd/uses' 10 | require 'cli/parser' 11 | require 'abstract_command' 12 | require 'dependencies_helpers' 13 | 14 | # I am not a ruby-ist and so my style may offend some 15 | 16 | module Homebrew 17 | module Cmd 18 | class RmtreeCmd < AbstractCommand 19 | cmd_args do 20 | usage_banner "`rmtree` [] []" 21 | description <<~EOS 22 | Remove a formula entirely, including all of its dependencies, unless of course, they are used by another formula. 23 | 24 | Warning: 25 | Not all formulae declare their dependencies and therefore this command may end up removing something you still need. It should be used with caution. 26 | 27 | `brew rmtree` 28 | Removes and its dependencies. 29 | 30 | `brew rmtree` 31 | Removes and and their dependencies. 32 | 33 | `brew rmtree` --force 34 | Force the removal of even if other formulae depend on it. 35 | 36 | `brew rmtree` --ignore= 37 | Remove , but don't remove its dependency of 38 | EOS 39 | switch "--quiet", 40 | description: "Hide output." 41 | switch "-n", "--dry-run", 42 | description: "See what would be removed without actually removing anything." 43 | switch "--force", 44 | description: "Force the removal of even if other formulae depend on it. " + 45 | "You can override the dependency check for the top-level formula you " + 46 | "are trying to remove. \nFor example, if you try to remove 'ruby', you most likely will " + 47 | "not be able to do this because other fomulae specify this as a dependency. This " + 48 | "option will enable you to remove 'ruby'. This will NOT bypass dependency checks for the " + 49 | "formula's children. If 'ruby' depends on 'git', then 'git' will still not be removed. Sorry." 50 | comma_array "--ignore=", 51 | description: "Ignore some dependencies from being removed. Specify multiple values separated by a comma." 52 | switch "--include-implicit", 53 | hidden: true, 54 | description: "Include `:implicit` dependencies for ." 55 | switch "--include-build", 56 | hidden: true, 57 | description: "Include `:build` dependencies for ." 58 | switch "--include-test", 59 | hidden: true, 60 | description: "Include `:test` dependencies for (non-recursive)." 61 | switch "--include-optional", 62 | hidden: true, 63 | description: "Include `:optional` dependencies for ." 64 | switch "--skip-recommended", 65 | hidden: true, 66 | description: "Skip `:recommended` dependencies for ." 67 | switch "--missing", 68 | hidden: true, 69 | description: "Show only missing dependencies." 70 | end 71 | 72 | def run 73 | BrewRmtree.new.run(args) 74 | end 75 | end 76 | end 77 | end 78 | 79 | class BrewRmtree 80 | include DependenciesHelpers 81 | 82 | @@dry_run = false 83 | @@used_by_table = {} 84 | @@dependency_table = {} 85 | 86 | def bash(command) 87 | escaped_command = Shellwords.escape(command) 88 | return %x! bash -c #{escaped_command} ! 89 | end 90 | 91 | # Find the path to the currently running version of homebrew 92 | # Fixes #46 93 | def brew_path() 94 | brew_home = Pathname.new(ENV["HOMEBREW_PREFIX"]) 95 | if brew_home.directory? 96 | brew_bin = brew_home / "bin/brew" 97 | if brew_bin.executable? 98 | return brew_bin.to_s 99 | end 100 | end 101 | return "brew" 102 | end 103 | 104 | # replaces Kernel#puts w/ do-nothing method 105 | def puts_off 106 | Kernel.module_eval %q{ 107 | def puts(*args) 108 | end 109 | def print(*args) 110 | end 111 | } 112 | end 113 | 114 | # restores Kernel#puts to its original definition 115 | def puts_on 116 | Kernel.module_eval %q{ 117 | def puts(*args) 118 | $stdout.puts(*args) 119 | end 120 | def print(*args) 121 | $stdout.print(*args) 122 | end 123 | } 124 | end 125 | 126 | # Sets the text to output with the spinner 127 | def set_spinner_progress(txt) 128 | @spinner[:progress] = txt 129 | end 130 | 131 | def show_wait_spinner(fps=10) 132 | chars = %w[| / - \\] 133 | delay = 1.0/fps 134 | iter = 0 135 | @spinner = Thread.new do 136 | Thread.current[:progress] = "" 137 | progress_size = 0 138 | while iter do # Keep spinning until told otherwise 139 | print ' ' + chars[(iter+=1) % chars.length] + Thread.current[:progress] 140 | progress_size = Thread.current[:progress].length 141 | sleep delay 142 | print "\b"*(progress_size + 2) 143 | end 144 | end 145 | yield.tap{ # After yielding to the block, save the return value 146 | iter = false # Tell the thread to exit, cleaning up after itself… 147 | @spinner.join # …and wait for it to do so. 148 | } # Use the block's return value as the method's 149 | end 150 | 151 | # Remove a particular keg 152 | def remove_keg(keg_name, dry_run) 153 | if dry_run 154 | puts "Would have removed #{keg_name}" 155 | return 156 | end 157 | 158 | # Remove old versions of keg 159 | puts bash "#{brew_path} cleanup #{keg_name} 2>/dev/null" 160 | 161 | # Remove current keg 162 | puts bash "#{brew_path} uninstall #{keg_name}" 163 | end 164 | 165 | # A list of dependencies of keg_name that are still installed after removal 166 | # of the keg 167 | def orphaned_dependencies(keg_name) 168 | bash("join <(sort <(#{brew_path} leaves)) <(sort <(#{brew_path} deps #{keg_name}))").split("\n") 169 | end 170 | 171 | # A list of kegs that use keg_name, using homebrew code instead of shell cmd 172 | def uses(keg_name, recursive=true, ignores=[], args:) 173 | # https://raw.githubusercontent.com/Homebrew/brew/master/Library/Homebrew/cmd/uses.rb 174 | formulae = [Formulary.factory(keg_name)] 175 | uses = Formula.installed.select do |f| 176 | formulae.all? do |ff| 177 | begin 178 | if recursive 179 | deps = f.recursive_dependencies do |dependent, dep| 180 | if dep.recommended? 181 | Dependency.prune if ignores.include?("recommended?") || dependent.build.without?(dep) 182 | elsif dep.optional? 183 | Dependency.prune if !includes.include?("optional?") && !dependent.build.with?(dep) 184 | elsif dep.build? 185 | Dependency.prune unless includes.include?("build?") 186 | end 187 | 188 | # If a tap isn't installed, we can't find the dependencies of one 189 | # its formulae, and an exception will be thrown if we try. 190 | if dep.is_a?(TapDependency) && !dep.tap.installed? 191 | Dependency.keep_but_prune_recursive_deps 192 | end 193 | end 194 | 195 | dep_formulae = deps.flat_map do |dep| 196 | begin 197 | dep.to_formula 198 | rescue 199 | [] 200 | end 201 | end 202 | 203 | reqs_by_formula = ([f] + dep_formulae).flat_map do |formula| 204 | formula.requirements.map { |req| [formula, req] } 205 | end 206 | 207 | reqs_by_formula.reject! do |dependent, req| 208 | if req.recommended? 209 | ignores.include?("recommended?") || dependent.build.without?(req) 210 | elsif req.optional? 211 | !includes.include?("optional?") && !dependent.build.with?(req) 212 | elsif req.build? 213 | !includes.include?("build?") 214 | end 215 | end 216 | 217 | reqs = reqs_by_formula.map(&:last) 218 | else 219 | includes, ignores = args_includes_ignores(args) 220 | deps = f.deps.reject do |dep| 221 | ignores.any? { |ignore| dep.send(ignore) } && includes.none? { |include| dep.send(include) } 222 | end 223 | # deps.reject! do |dep| 224 | # # Exclude build dependencies or not required and can be built without it 225 | # dep.build? || (!dep.required? && as_formula(dep.name).build.without?(dep)) 226 | # end 227 | reqs = f.requirements.reject do |req| 228 | ignores.any? { |ignore| req.send(ignore) } && includes.none? { |include| req.send(include) } 229 | end 230 | end 231 | next true if deps.any? do |dep| 232 | begin 233 | dep.to_formula.full_name == ff.full_name 234 | rescue 235 | dep.name == ff.name 236 | end 237 | end 238 | 239 | reqs.any? { |req| req.name == ff.name } 240 | rescue FormulaUnavailableError 241 | # Silently ignore this case as we don't care about things used in 242 | # taps that aren't currently tapped. 243 | next 244 | end 245 | end 246 | end 247 | uses.map(&:full_name) 248 | end 249 | 250 | def deps_for_formula(f, args:) 251 | # https://github.com/Homebrew/brew/blob/d1b83819deacd99b55c9d400149dc9b49fa795df/Library/Homebrew/cmd/deps.rb#L137 252 | includes, ignores = args_includes_ignores(args) 253 | 254 | deps = f.runtime_dependencies 255 | reqs = select_includes(f.requirements, ignores, includes) 256 | 257 | deps + reqs.to_a 258 | end 259 | 260 | # Gather complete list of packages used by root package 261 | def dependency_tree(keg_name, recursive=true, args:) 262 | deps_for_formula(as_formula(keg_name), args: args 263 | ).map{ |x| as_formula(x) } 264 | .reject{ |x| x.nil? } 265 | .select(&:any_version_installed? 266 | ) 267 | end 268 | 269 | # Returns a set of dependencies as their keg name 270 | def dependency_tree_as_keg_names(keg_name, recursive=true, args:) 271 | @@dependency_table[keg_name] ||= dependency_tree(keg_name, recursive, args: args).map!(&:name) 272 | end 273 | 274 | # Return a formula for keg_name 275 | def as_formula(keg_name) 276 | if keg_name.is_a? Dependency 277 | return find_active_formula(keg_name.name) 278 | end 279 | if keg_name.is_a? Requirement 280 | begin 281 | return find_active_formula(keg_name.to_dependency.name) 282 | rescue 283 | return nil 284 | end 285 | end 286 | return find_active_formula(keg_name) 287 | end 288 | 289 | # Given a formula name, find the formula for the active version. 290 | # Default formulae are for the latest version which may not be installed causing issue #28. 291 | def find_active_formula(name) 292 | latest_formula = Formulary.factory(name) 293 | active_version = latest_formula.linked_version 294 | active_prefix = latest_formula.installed_prefixes.last 295 | begin 296 | return Formulary.factory("#{active_prefix}/.brew/#{name}.rb") 297 | rescue 298 | return latest_formula 299 | end 300 | end 301 | 302 | def used_by(dep_name, del_formula, args:) 303 | @@used_by_table[dep_name] ||= uses(dep_name, false, args: args).to_set.delete(del_formula.full_name) 304 | end 305 | 306 | # Return list of installed formula that will still use this dependency 307 | # after deletion and thus cannot be removed. 308 | def still_used_by(dep_name, del_formula, full_dep_list, args:) 309 | # List of formulae that use this keg and aren't in the tree 310 | # of dependencies to be removed 311 | return used_by(dep_name, del_formula, args: args).subtract(full_dep_list) 312 | end 313 | 314 | def cant_remove(dep_set) 315 | !dep_set.empty? 316 | end 317 | 318 | def can_remove(dep_set) 319 | dep_set.empty? 320 | end 321 | 322 | def removable_in_tree(tree) 323 | tree.select {|dep,used_by_set| can_remove(used_by_set)} 324 | end 325 | 326 | def unremovable_in_tree(tree) 327 | tree.select {|dep,used_by_set| cant_remove(used_by_set)} 328 | end 329 | 330 | def describe_build_tree_will_remove(tree) 331 | will_remove = removable_in_tree(tree) 332 | 333 | puts "" 334 | puts "Can safely be removed" 335 | puts "----------------------" 336 | puts will_remove.map { |dep,_| dep }.sort.join("\n") 337 | end 338 | 339 | def describe_build_tree_wont_remove(tree) 340 | wont_remove = unremovable_in_tree(tree) 341 | 342 | puts "" 343 | puts "Won't be removed" 344 | puts "-----------------" 345 | puts wont_remove.map { |dep,used_by| "#{dep} is used by #{used_by.to_a.join(', ')}" }.sort.join("\n") 346 | end 347 | 348 | # Print out interpretation of dependency analysis 349 | def describe_build_tree(tree) 350 | describe_build_tree_will_remove(tree) 351 | describe_build_tree_wont_remove(tree) 352 | end 353 | 354 | # Simple prompt helper 355 | def should_proceed(prompt) 356 | input = [(print "#{prompt}[y/N]: "), STDIN.gets.chomp()][1] 357 | if ['y', 'yes'].include?(input.downcase) 358 | return true 359 | end 360 | return false 361 | end 362 | 363 | def should_proceed_or_quit(prompt) 364 | puts "" 365 | unless should_proceed(prompt) 366 | puts "" 367 | onoe "User quit" 368 | exit 0 369 | end 370 | return true 371 | end 372 | 373 | # Will mark any children and parents of dep as unremovable if dep is unremovable 374 | def revisit_neighbors(of_dependency, del_formula, dep_set, wont_remove_because, args:) 375 | # Prevent subsequent related formula from being flagged for removal 376 | dep_set.delete(of_dependency) 377 | 378 | # Update users of the dependency 379 | used_by(of_dependency, del_formula, args: args).each do |user_of_d| 380 | # Only update those we visited and think we can remove 381 | if wont_remove_because.has_key? user_of_d and can_remove(wont_remove_because[user_of_d]) 382 | wont_remove_because[user_of_d] << of_dependency 383 | revisit_neighbors(user_of_d, del_formula, dep_set, wont_remove_because, args: args) 384 | end 385 | end 386 | 387 | # Update dependencies of the dependency 388 | dependency_tree_as_keg_names(of_dependency, false, args: args).each do |d| 389 | # Only update those we visited and think we can remove 390 | if wont_remove_because.has_key? d and can_remove(wont_remove_because[d]) 391 | wont_remove_because[d] << of_dependency 392 | revisit_neighbors(d, del_formula, dep_set, wont_remove_because, args: args) 393 | end 394 | end 395 | end 396 | 397 | # Walk the tree and decide which ones are safe to remove 398 | def build_tree(keg_name, ignored_kegs=[], args: ) 399 | # List used to save the status of all dependency packages 400 | wont_remove_because = {} 401 | 402 | ohai "Examining installed formulae required by #{keg_name}..." 403 | show_wait_spinner{ 404 | 405 | # Convert the keg_name the user provided into homebrew formula 406 | f = as_formula(keg_name) 407 | 408 | # Get the complete list of dependencies and convert it to just keg names 409 | dep_arr = dependency_tree_as_keg_names(keg_name, args: args) 410 | dep_set = dep_arr.to_set 411 | 412 | # For each possible dependency that we want to remove, check if anything 413 | # uses it, which is not also in the list of dependencies. That means it 414 | # isn't safe to remove. 415 | dep_arr.each do |dep| 416 | 417 | # Set the progress text for spinner thread 418 | set_spinner_progress " #{wont_remove_because.size} / #{dep_arr.length} " 419 | 420 | # Save the list of formulae that use this keg and aren't in the tree 421 | # of dependencies to be removed 422 | wont_remove_because[dep] = still_used_by(dep, f, dep_set, args: args) 423 | 424 | # Allow user to keep dependencies that aren't used anymore by saying 425 | # something phony uses it 426 | if ignored_kegs.include?(dep) 427 | if wont_remove_because[dep].empty? 428 | wont_remove_because[dep] << "ignored" 429 | end 430 | end 431 | 432 | # Revisit any formulae already visited and related to this dependency 433 | # because at the time they didn't have this new information 434 | if cant_remove(wont_remove_because[dep]) 435 | # This dependency can't be removed. Users and dependencies need to be reconsidered. 436 | revisit_neighbors(dep, f, dep_set, wont_remove_because, args: args) 437 | end 438 | 439 | set_spinner_progress " #{wont_remove_because.size} / #{dep_arr.length} " 440 | end 441 | } 442 | print "\n" 443 | return wont_remove_because 444 | end 445 | 446 | def order_to_be_removed_v2(start_from, wont_remove_because, args:) 447 | # Maintain stuff we delete 448 | deleted_formulae = [start_from] 449 | 450 | # Stuff we *should* be able to delete, albeit using faulty logic from before 451 | maybe_dependencies_to_delete = removable_in_tree(wont_remove_because).map { |d,_| d } 452 | 453 | # Keep deleting things that we *think* we can delete. As we go through the list, 454 | # more things should become deletable. But when no new things become deletable, 455 | # then we are done. This is hacky logic v2 456 | last_size = 0 457 | while maybe_dependencies_to_delete.size != last_size 458 | last_size = maybe_dependencies_to_delete.size 459 | maybe_dependencies_to_delete.each do |dep| 460 | _used_by = uses(dep, false, args: args).to_set.subtract(deleted_formulae.to_set) 461 | # puts "Deleted formulae are #{deleted_formulae.inspect()}" 462 | # puts "#{dep} is used by #{_used_by.inspect()}" 463 | if _used_by.size == 0 464 | deleted_formulae << dep 465 | maybe_dependencies_to_delete.delete dep 466 | end 467 | end 468 | end 469 | 470 | return deleted_formulae, maybe_dependencies_to_delete 471 | end 472 | 473 | def rmtree(keg_name, force=false, ignored_kegs=[], args:) 474 | # Does anything use keg such that we can't remove it? 475 | if !force 476 | keg_used_by = uses(keg_name, false, args: args) 477 | if !keg_used_by.empty? 478 | puts "#{keg_name} can't be removed because other formula depend on it:" 479 | puts keg_used_by.join(", ") 480 | return 481 | end 482 | end 483 | 484 | # Check if the formula is installed (outdated implies installed) 485 | unless as_formula(keg_name).any_version_installed? || as_formula(keg_name).outdated? 486 | onoe "#{keg_name} is not currently installed" 487 | return 488 | end 489 | 490 | # Dependency list of what can be removed, and what can't, and why 491 | wont_remove_because = build_tree(keg_name, ignored_kegs, args: args) 492 | 493 | kegs_to_delete_in_order, maybe_dependencies_to_delete = order_to_be_removed_v2(keg_name, wont_remove_because, 494 | args: args) 495 | 496 | # Dry run print out more information on what will happen 497 | if @@dry_run 498 | # describe_build_tree(wont_remove_because) 499 | 500 | puts "" 501 | puts "Can safely be removed" 502 | puts "----------------------" 503 | kegs_to_delete_in_order.each do |k| 504 | puts k 505 | end 506 | 507 | describe_build_tree_wont_remove(wont_remove_because) 508 | if @@dry_run 509 | maybe_dependencies_to_delete.each do |dep| 510 | _used_by = uses(dep, false, args: args).to_set.subtract(kegs_to_delete_in_order) 511 | puts "#{dep} is used by #{_used_by.to_a.join(', ')}" 512 | end 513 | end 514 | 515 | puts "" 516 | puts "Order of operations" 517 | puts "-------------------" 518 | puts kegs_to_delete_in_order 519 | else 520 | # Confirm with user packages that can and will be removed 521 | # describe_build_tree_will_remove(wont_remove_because) 522 | 523 | puts "" 524 | puts "Can safely be removed" 525 | puts "----------------------" 526 | kegs_to_delete_in_order.each do |k| 527 | puts k 528 | end 529 | 530 | should_proceed_or_quit("Proceed?") 531 | 532 | ohai "Cleaning up packages safe to remove" 533 | end 534 | 535 | # Remove packages 536 | # remove_keg(keg_name, @@dry_run) 537 | #removable_in_tree(wont_remove_because).map { |d,_| remove_keg(d, @@dry_run) } 538 | kegs_to_delete_in_order.each { |d| remove_keg(d, @@dry_run) } 539 | end 540 | 541 | def run(args) 542 | force = args.force? 543 | ignored_kegs = [] 544 | ignored_kegs.push(*args.ignore) 545 | rm_kegs = args.named 546 | quiet = args.quiet? 547 | @@dry_run = args.dry_run? 548 | 549 | raise KegUnspecifiedError if args.no_named? 550 | 551 | # Turn off output if 'quiet' is specified 552 | if quiet 553 | puts_off 554 | end 555 | 556 | if @@dry_run 557 | puts "This is a dry-run, nothing will be deleted" 558 | end 559 | 560 | # Convert ignored kegs into full names 561 | ignored_kegs.map! { |k| as_formula(k).full_name } 562 | 563 | rm_kegs.each { |keg_name| rmtree keg_name, force, ignored_kegs, args: args } 564 | end 565 | end 566 | --------------------------------------------------------------------------------