mirror of
https://github.com/Homebrew/brew.git
synced 2026-10-28 09:34:42 +08:00
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:
4 files changed
+68
-9
No files matched your search
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in new issue
Block a user