├── .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 |
--------------------------------------------------------------------------------