Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

(un)linkapps: modernize, prune: remove broken app symlinks #46549

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 14 additions & 1 deletion Library/Contributions/brew_bash_completion.sh
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,18 @@ _brew_uninstall ()
__brew_complete_installed
}

_brew_unlinkapps ()
{
local cur="${COMP_WORDS[COMP_CWORD]}"
case "$cur" in
--*)
__brewcomp "--dry-run --local"
return
;;
esac
__brew_complete_installed
}

_brew_unpack ()
{
local cur="${COMP_WORDS[COMP_CWORD]}"
Expand Down Expand Up @@ -618,7 +630,7 @@ _brew ()
install|instal|reinstall) _brew_install ;;
irb) _brew_irb ;;
link|ln) _brew_link ;;
linkapps|unlinkapps) _brew_linkapps ;;
linkapps) _brew_linkapps ;;
list|ls) _brew_list ;;
log) _brew_log ;;
man) _brew_man ;;
Expand All @@ -638,6 +650,7 @@ _brew ()
tap-unpin) _brew_tap_unpin ;;
tests) _brew_tests ;;
uninstall|remove|rm) _brew_uninstall ;;
unlinkapps) _brew_unlinkapps ;;
unpack) _brew_unpack ;;
unpin) __brew_complete_formulae ;;
untap|tap-info|tap-pin) __brew_complete_tapped ;;
Expand Down
33 changes: 24 additions & 9 deletions Library/Homebrew/cmd/linkapps.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
# Links any Applications (.app) found in installed prefixes to /Applications
require "keg"
require "formula"

module Homebrew
def linkapps
target_dir = ARGV.include?("--local") ? File.expand_path("~/Applications") : "/Applications"
target_dir = linkapps_target(:local => ARGV.include?("--local"))

unless File.exist? target_dir
unless target_dir.directory?
opoo "#{target_dir} does not exist, stopping."
puts "Run `mkdir #{target_dir}` first."
exit 1
Expand All @@ -16,24 +15,40 @@ def linkapps
kegs = Formula.racks.map do |rack|
keg = rack.subdirs.map { |d| Keg.new(d) }
next if keg.empty?
keg.detect(&:linked?) || keg.max { |a, b| a.version <=> b.version }
keg.detect(&:linked?) || keg.max_by(&:version)
end
else
kegs = ARGV.kegs
end

link_count = 0
kegs.each do |keg|
keg.apps.each do |app|
puts "Linking #{app} to #{target_dir}."
target = "#{target_dir}/#{app.basename}"
puts "Linking: #{app}"
target_app = target_dir/app.basename

if File.exist?(target) && !File.symlink?(target)
onoe "#{target} already exists, skipping."
if target_app.exist? && !target_app.symlink?
onoe "#{target_app} already exists, skipping."
next
end

system "ln", "-sf", app, target_dir
# We cannot use `install_symlink` because we want an absolute link.
FileUtils.ln_sf(app, target_dir)
link_count += 1
end
end

if link_count.zero?
puts "No apps linked to #{target_dir}" if ARGV.verbose?
else
puts "Linked #{link_count} app#{plural(link_count)} to #{target_dir}"
end
end

private

def linkapps_target(opts = {})
local = opts.fetch(:local, false)
Pathname.new(local ? "~/Applications" : "/Applications").expand_path
end
end
3 changes: 3 additions & 0 deletions Library/Homebrew/cmd/prune.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "keg"
require "cmd/tap"
require "cmd/unlinkapps"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel super-strongly about this but we're trying to avoid cmd/ includes now so if you're feeling extra lovely (can be a future PR) pulling these methods into another module would be ❤️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right and I think it makes more sense to address this here in this PR (big chunks have been rewritten anyway) instead of deferring the code move to a new PR. I'll reshuffle things soon. Thanks for the soft nudge. 😻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


module Homebrew
def prune
Expand Down Expand Up @@ -47,5 +48,7 @@ def prune
print "and #{d} directories " if d > 0
puts "from #{HOMEBREW_PREFIX}"
end unless ARGV.dry_run?

unlinkapps_prune(:dry_run => ARGV.dry_run?, :quiet => true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here I wonder if it's worth just adding to uninstall too? No worries if you'd rather leave it for another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to do that in another PR as I expect some discussion. The reason is there are multiple places from where this likely needs to be called, though uninstall is the most obvious one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

end
end
69 changes: 52 additions & 17 deletions Library/Homebrew/cmd/unlinkapps.rb
Original file line number Diff line number Diff line change
@@ -1,33 +1,68 @@
# Unlinks any Applications (.app) found in installed prefixes from /Applications
require "keg"
require "cmd/linkapps"

module Homebrew
def unlinkapps
target_dir = ARGV.include?("--local") ? File.expand_path("~/Applications") : "/Applications"
target_dir = linkapps_target(:local => ARGV.include?("--local"))

return unless File.exist? target_dir
unlinkapps_from_dir(target_dir, :dry_run => ARGV.dry_run?)
end

cellar_apps = Dir[target_dir + "/*.app"].select do |app|
if File.symlink?(app)
should_unlink? File.readlink(app)
end
private

def unlinkapps_prune(opts = {})
opts = opts.merge(:prune => true)
unlinkapps_from_dir(linkapps_target(:local => false), opts)
unlinkapps_from_dir(linkapps_target(:local => true), opts)
end

def unlinkapps_from_dir(target_dir, opts = {})
return unless target_dir.directory?
dry_run = opts.fetch(:dry_run, false)
quiet = opts.fetch(:quiet, false)

apps = Pathname.glob("#{target_dir}/*.app").select do |app|
unlinkapps_unlink?(app, opts)
end

cellar_apps.each do |app|
puts "Unlinking #{app}"
system "unlink", app
ObserverPathnameExtension.reset_counts!

app_kind = opts.fetch(:prune, false) ? " (broken link)" : ""
apps.each do |app|
app.extend(ObserverPathnameExtension)
if dry_run
puts "Would unlink#{app_kind}: #{app}"
else
puts "Unlinking#{app_kind}: #{app}" unless quiet
app.unlink
end
end

puts "Finished unlinking from #{target_dir}" if cellar_apps
return if dry_run

if ObserverPathnameExtension.total.zero?
puts "No apps unlinked from #{target_dir}" if ARGV.verbose?
else
n = ObserverPathnameExtension.total
puts "Unlinked #{n} app#{plural(n)} from #{target_dir}"
end
end

private
UNLINKAPPS_PREFIXES = %W[
#{HOMEBREW_CELLAR}/
#{HOMEBREW_PREFIX}/opt/
].freeze

def unlinkapps_unlink?(target_app, opts = {})
# Skip non-symlinks and symlinks that don't point into the Homebrew prefix.
app = "#{target_app.readlink}" if target_app.symlink?
return false unless app && app.start_with?(*UNLINKAPPS_PREFIXES)

def should_unlink?(file)
if ARGV.named.empty?
file.start_with?("#{HOMEBREW_CELLAR}/", "#{HOMEBREW_PREFIX}/opt/")
if opts.fetch(:prune, false)
!File.exist?(app) # Remove only broken symlinks in prune mode.
elsif ARGV.named.empty?
true
else
ARGV.kegs.any? { |keg| file.start_with?("#{keg}/", "#{keg.opt_record}/") }
ARGV.kegs.any? { |keg| app.start_with?("#{keg}/", "#{keg.opt_record}/") }
end
end
end
27 changes: 17 additions & 10 deletions Library/Homebrew/manpages/brew.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,14 +267,13 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note
If `--force` is passed, Homebrew will allow keg-only formulae to be linked.

* `linkapps` [`--local`] [<formulae>]:
Find installed formulae that have compiled `.app`-style "application"
packages for OS X, and symlink those apps into `/Applications`, allowing
for easier access.
Find installed formulae that provide `.app`-style OS X apps and symlink them
into `/Applications`, allowing for easier access.

If no <formulae> are provided, all of them will have their .apps symlinked.
If no <formulae> are provided, all of them will have their apps symlinked.

If provided, `--local` will move them into the user's `~/Applications`
directory instead of the system directory. It may need to be created, first.
If provided, `--local` will symlink them into the user's `~/Applications`
directory instead of the system directory.

* `ls`, `list` [`--full-name`]:
List all installed formulae. If `--full-name` is passed, print formulae with
Expand Down Expand Up @@ -341,7 +340,9 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note

* `prune` [`--dry-run`]:
Remove dead symlinks from the Homebrew prefix. This is generally not
needed, but can be useful when doing DIY installations.
needed, but can be useful when doing DIY installations. Also remove broken
app symlinks from `/Applications` and `~/Applications` that were previously
created by `brew linkapps`.

If `--dry-run` or `-n` is passed, show what would be removed, but do not
actually remove anything.
Expand Down Expand Up @@ -446,10 +447,16 @@ With `--verbose` or `-v`, many commands print extra debugging information. Note
If `--dry-run` or `-n` is passed, Homebrew will list all files which would
be unlinked, but will not actually unlink or delete any files.

* `unlinkapps` [`--local`] [<formulae>]:
Removes links created by `brew linkapps`.
* `unlinkapps` [`--local`] [`--dry-run`] [<formulae>]:
Remove symlinks created by `brew linkapps` from `/Applications`.

If no <formulae> are provided, all linked app will be removed.
If no <formulae> are provided, all linked apps will be removed.

If provided, `--local` will remove symlinks from the user's `~/Applications`
directory instead of the system directory.

If `--dry-run` or `-n` is passed, Homebrew will list all symlinks which
would be removed, but will not actually delete any files.

* `unpack` [`--git`|`--patch`] [`--destdir=`<path>] <formulae>:
Unpack the source files for <formulae> into subdirectories of the current
Expand Down
8 changes: 5 additions & 3 deletions Library/Homebrew/test/test_integration_cmds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ class Testball < Formula

source_dir = HOMEBREW_CELLAR/"testball/0.1/TestBall.app"
source_dir.mkpath
assert_match "Linking #{source_dir} to",
assert_match "Linking: #{source_dir}",
cmd("linkapps", "--local", {"HOME" => home})
ensure
formula_file.unlink
Expand All @@ -460,7 +460,7 @@ class Testball < Formula

FileUtils.ln_s source_app, "#{apps_dir}/TestBall.app"

assert_match "Unlinking #{apps_dir}/TestBall.app",
assert_match "Unlinking: #{apps_dir}/TestBall.app",
cmd("unlinkapps", "--local", {"HOME" => home})
ensure
formula_file.unlink
Expand Down Expand Up @@ -695,7 +695,9 @@ def test_prune
assert (share/"notpruneable").directory?
refute (share/"pruneable_symlink").symlink?

assert_equal "Nothing pruned",
assert_match "Nothing pruned\n" \
"No apps unlinked from /Applications\n" \
"No apps unlinked from /Users/", # ...<username>/Applications
cmd("prune", "--verbose")
ensure
share.rmtree
Expand Down
25 changes: 16 additions & 9 deletions share/doc/homebrew/brew.1.html
Original file line number Diff line number Diff line change
Expand Up @@ -214,14 +214,13 @@ <h2 id="COMMANDS">COMMANDS</h2>
actually link or delete any files.</p>

<p>If <code>--force</code> is passed, Homebrew will allow keg-only formulae to be linked.</p></dd>
<dt><code>linkapps</code> [<code>--local</code>] [<var>formulae</var>]</dt><dd><p>Find installed formulae that have compiled <code>.app</code>-style "application"
packages for OS X, and symlink those apps into <code>/Applications</code>, allowing
for easier access.</p>
<dt><code>linkapps</code> [<code>--local</code>] [<var>formulae</var>]</dt><dd><p>Find installed formulae that provide <code>.app</code>-style OS X apps and symlink them
into <code>/Applications</code>, allowing for easier access.</p>

<p>If no <var>formulae</var> are provided, all of them will have their .apps symlinked.</p>
<p>If no <var>formulae</var> are provided, all of them will have their apps symlinked.</p>

<p>If provided, <code>--local</code> will move them into the user's <code>~/Applications</code>
directory instead of the system directory. It may need to be created, first.</p></dd>
<p>If provided, <code>--local</code> will symlink them into the user's <code>~/Applications</code>
directory instead of the system directory.</p></dd>
<dt><code>ls</code>, <code>list</code> [<code>--full-name</code>]</dt><dd><p>List all installed formulae. If <code>--full-name</code> is passed, print formulae with
full-qualified names.</p></dd>
<dt><code>ls</code>, <code>list</code> <code>--unbrewed</code></dt><dd><p>List all files in the Homebrew prefix not installed by Homebrew.</p></dd>
Expand Down Expand Up @@ -268,7 +267,9 @@ <h2 id="COMMANDS">COMMANDS</h2>
<dt><code>pin</code> <var>formulae</var></dt><dd><p>Pin the specified <var>formulae</var>, preventing them from being upgraded when
issuing the <code>brew upgrade</code> command. See also <code>unpin</code>.</p></dd>
<dt><code>prune</code> [<code>--dry-run</code>]</dt><dd><p>Remove dead symlinks from the Homebrew prefix. This is generally not
needed, but can be useful when doing DIY installations.</p>
needed, but can be useful when doing DIY installations. Also remove broken
app symlinks from <code>/Applications</code> and <code>~/Applications</code> that were previously
created by <code>brew linkapps</code>.</p>

<p>If <code>--dry-run</code> or <code>-n</code> is passed, show what would be removed, but do not
actually remove anything.</p></dd>
Expand Down Expand Up @@ -336,9 +337,15 @@ <h2 id="COMMANDS">COMMANDS</h2>

<p>If <code>--dry-run</code> or <code>-n</code> is passed, Homebrew will list all files which would
be unlinked, but will not actually unlink or delete any files.</p></dd>
<dt><code>unlinkapps</code> [<code>--local</code>] [<var>formulae</var>]</dt><dd><p>Removes links created by <code>brew linkapps</code>.</p>
<dt><code>unlinkapps</code> [<code>--local</code>] [<code>--dry-run</code>] [<var>formulae</var>]</dt><dd><p>Remove symlinks created by <code>brew linkapps</code> from <code>/Applications</code>.</p>

<p>If no <var>formulae</var> are provided, all linked app will be removed.</p></dd>
<p>If no <var>formulae</var> are provided, all linked apps will be removed.</p>

<p>If provided, <code>--local</code> will remove symlinks from the user's <code>~/Applications</code>
directory instead of the system directory.</p>

<p>If <code>--dry-run</code> or <code>-n</code> is passed, Homebrew will list all symlinks which
would be removed, but will not actually delete any files.</p></dd>
<dt><code>unpack</code> [<code>--git</code>|<code>--patch</code>] [<code>--destdir=</code><var>path</var>] <var>formulae</var></dt><dd><p>Unpack the source files for <var>formulae</var> into subdirectories of the current
working directory. If <code>--destdir=</code><var>path</var> is given, the subdirectories will
be created in the directory named by <code>&lt;path></code> instead.</p>
Expand Down
20 changes: 13 additions & 7 deletions share/man/man1/brew.1
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,13 @@ If \fB\-\-force\fR is passed, Homebrew will allow keg\-only formulae to be linke
.
.TP
\fBlinkapps\fR [\fB\-\-local\fR] [\fIformulae\fR]
Find installed formulae that have compiled \fB\.app\fR\-style "application" packages for OS X, and symlink those apps into \fB/Applications\fR, allowing for easier access\.
Find installed formulae that provide \fB\.app\fR\-style OS X apps and symlink them into \fB/Applications\fR, allowing for easier access\.
.
.IP
If no \fIformulae\fR are provided, all of them will have their \.apps symlinked\.
If no \fIformulae\fR are provided, all of them will have their apps symlinked\.
.
.IP
If provided, \fB\-\-local\fR will move them into the user\'s \fB~/Applications\fR directory instead of the system directory\. It may need to be created, first\.
If provided, \fB\-\-local\fR will symlink them into the user\'s \fB~/Applications\fR directory instead of the system directory\.
.
.TP
\fBls\fR, \fBlist\fR [\fB\-\-full\-name\fR]
Expand Down Expand Up @@ -365,7 +365,7 @@ Pin the specified \fIformulae\fR, preventing them from being upgraded when issui
.
.TP
\fBprune\fR [\fB\-\-dry\-run\fR]
Remove dead symlinks from the Homebrew prefix\. This is generally not needed, but can be useful when doing DIY installations\.
Remove dead symlinks from the Homebrew prefix\. This is generally not needed, but can be useful when doing DIY installations\. Also remove broken app symlinks from \fB/Applications\fR and \fB~/Applications\fR that were previously created by \fBbrew linkapps\fR\.
.
.IP
If \fB\-\-dry\-run\fR or \fB\-n\fR is passed, show what would be removed, but do not actually remove anything\.
Expand Down Expand Up @@ -473,11 +473,17 @@ Remove symlinks for \fIformula\fR from the Homebrew prefix\. This can be useful
If \fB\-\-dry\-run\fR or \fB\-n\fR is passed, Homebrew will list all files which would be unlinked, but will not actually unlink or delete any files\.
.
.TP
\fBunlinkapps\fR [\fB\-\-local\fR] [\fIformulae\fR]
Removes links created by \fBbrew linkapps\fR\.
\fBunlinkapps\fR [\fB\-\-local\fR] [\fB\-\-dry\-run\fR] [\fIformulae\fR]
Remove symlinks created by \fBbrew linkapps\fR from \fB/Applications\fR\.
.
.IP
If no \fIformulae\fR are provided, all linked app will be removed\.
If no \fIformulae\fR are provided, all linked apps will be removed\.
.
.IP
If provided, \fB\-\-local\fR will remove symlinks from the user\'s \fB~/Applications\fR directory instead of the system directory\.
.
.IP
If \fB\-\-dry\-run\fR or \fB\-n\fR is passed, Homebrew will list all symlinks which would be removed, but will not actually delete any files\.
.
.TP
\fBunpack\fR [\fB\-\-git\fR|\fB\-\-patch\fR] [\fB\-\-destdir=\fR\fIpath\fR] \fIformulae\fR
Expand Down