Speed up linking and unlinking kegs

- `Keg#link_dir` and `Keg#unlink` called `relative_path_from` on every
  path `find` yielded and `make_relative_symlink` once more per link.
  Ruby's method cleans both paths and walks them a component at a time,
  which made it a large part of linking and unlinking big kegs.
- Add `Utils::Path.relative_path_from` for these calls. For clean
  absolute paths it slices the base off a path below it and compares
  components otherwise, leaving anything else to Ruby's method.
- `Keg#unlink` checks `symlink?` before `directory?`, so each linked
  file takes one `lstat` rather than an `lstat` and a `stat`.
This commit is contained in:
Justin Krehel committed 2026-10-24 08:42:26 -04:00
1 parent 9811f64e2c
commit cbdfb03ce7
4 files changed
+68 -9

No files matched your search

+1 -1
View File
@@ -249,7 +249,7 @@ class Pathname
sig { params(src: Pathname).void }
def make_relative_symlink(src)
dirname.mkpath
File.symlink(src.relative_path_from(dirname), self)
File.symlink(Utils::Path.relative_path_from(src, dirname), self)
end
sig { params(block: T.proc.void).void }
+8 -6
View File
@@ -367,13 +367,15 @@ class Keg
keg_directories.each do |dir|
dir.find do |src|
dst = HOMEBREW_PREFIX + src.relative_path_from(path)
dst = HOMEBREW_PREFIX + relative_path_from(src, path)
dst.extend(ObserverPathnameExtension)
dirs << dst if dst.directory? && !dst.symlink?
unless dst.symlink?
dirs << dst if dst.directory?
next
end
# check whether the file to be unlinked is from the current keg first
next unless dst.symlink?
next if src != resolved_path(dst)
if dry_run
@@ -912,7 +914,7 @@ class Keg
root.find do |src|
next if src == root
relative_src = src.relative_path_from(path)
relative_src = relative_path_from(src, path)
dst = HOMEBREW_PREFIX + relative_src
dst.extend ObserverPathnameExtension
@@ -929,7 +931,7 @@ class Keg
# file is in the way.
Find.prune if PYC_EXTENSIONS.include?(src.extname) && src.to_s.include?("/site-packages/")
case yield src.relative_path_from(root)
case yield relative_path_from(src, root)
when :skip_file, nil
Find.prune
when :info
@@ -948,7 +950,7 @@ class Keg
# spotlight, or the open command and actual mac apps use an equivalent
Find.prune if src.extname == ".app"
case yield src.relative_path_from(root)
case yield relative_path_from(src, root)
when :skip_dir
Find.prune
when :mkpath
+31
View File
@@ -225,6 +225,37 @@ RSpec.describe Utils::Path do
end
end
describe "::relative_path_from" do
it "returns the part of a path below its base" do
expect(described_class.relative_path_from(Pathname("/opt/homebrew/Cellar/wget/1.0/bin/wget"),
Pathname("/opt/homebrew/Cellar/wget/1.0")))
.to eq(Pathname("bin/wget"))
end
it "relates clean absolute paths" do
expect(described_class.relative_path_from(Pathname("/opt/homebrew/Cellar/wget/1.0/bin/wget"),
Pathname("/opt/homebrew/bin")))
.to eq(Pathname("../Cellar/wget/1.0/bin/wget"))
end
it "relates clean absolute paths with hidden directories" do
expect(described_class.relative_path_from(Pathname("/home/linuxbrew/.linuxbrew/Cellar/wget/1.0/bin/wget"),
Pathname("/home/linuxbrew/.linuxbrew/bin")))
.to eq(Pathname("../Cellar/wget/1.0/bin/wget"))
end
it "cleans `..` components" do
expect(described_class.relative_path_from(Pathname("/opt/homebrew/lib/../bin/wget"),
Pathname("/opt/homebrew/bin")))
.to eq(Pathname("wget"))
end
it "rejects mixing absolute and relative paths" do
expect { described_class.relative_path_from(Pathname("/opt/homebrew"), Pathname("bin")) }
.to raise_error(ArgumentError)
end
end
describe "::formula_opt_prefix" do
it "returns a formula opt prefix without loading a Formula object" do
expect(described_class.formula_opt_prefix("foo")).to eq(HOMEBREW_PREFIX/"opt/foo")
+28 -2
View File
@@ -107,8 +107,34 @@ module Utils
path.chmod saved_perms if saved_perms
end
# `Pathname#relative_path_from` without its `cleanpath` calls for clean absolute paths.
sig { params(path: Pathname, base: Pathname).returns(Pathname) }
def relative_path_from(path, base)
path_string = path.to_s
base_string = base.to_s
if File::FNM_SYSCASE.zero? && clean_absolute_path?(path_string) && clean_absolute_path?(base_string)
base_prefix = "#{base_string}/"
return Pathname(path_string.delete_prefix(base_prefix)) if path_string.start_with?(base_prefix)
path_names = path_string.split("/").drop(1)
base_names = base_string.split("/").drop(1)
common = 0
common += 1 while common < path_names.length && path_names[common] == base_names[common]
names = Array.new(base_names.length - common, "..") + path_names.drop(common)
return Pathname(names.empty? ? "." : File.join(*names))
end
path.relative_path_from(base)
end
private
sig { params(path: String).returns(T::Boolean) }
def clean_absolute_path?(path)
path.start_with?("/") && path.exclude?("//") && path.exclude?("/./") && path.exclude?("/../") &&
!path.end_with?("/.", "/..") && (path == "/" || !path.end_with?("/"))
end
# `child_of?` is lexical, so resolve the deepest existing component first.
# Stop at a symlink too: `exist?` follows one, hiding a dangling link.
sig { params(path: T.any(Pathname, String)).returns(Pathname) }
@@ -124,11 +150,11 @@ module Utils
extend Helpers
private :child_of?, :ensure_child_of!, :cp_path_sub, :rmdir_if_possible, :text_executable?, :resolved_path,
:resolved_path_exists?, :ensure_writable
:resolved_path_exists?, :ensure_writable, :relative_path_from
class << self
public :child_of?, :ensure_child_of!, :cp_path_sub, :rmdir_if_possible, :text_executable?, :resolved_path,
:resolved_path_exists?, :ensure_writable
:resolved_path_exists?, :ensure_writable, :relative_path_from
end
sig { params(path: Pathname, verbose: T::Boolean).void }