Skip to content

Commit

Permalink
unpack_strategy/directory: use FileUtils.mv
Browse files Browse the repository at this point in the history
This should allow automatically deal with file systems though it may not
allow for GNU `cp -a` preservation of hardlinks.
  • Loading branch information
cho-m committed Oct 19, 2024
1 parent 6f17b06 commit a803cfe
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 26 deletions.
5 changes: 3 additions & 2 deletions Library/Homebrew/test/unpack_strategy/directory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
expect(unpack_dir/"folderSymlink").to be_a_symlink
end

it "preserves hardlinks" do
strategy.extract(to: unpack_dir)
it "preserves hardlinks with move enabled" do
strategy_with_move = described_class.new(path, move: true)
strategy_with_move.extract(to: unpack_dir)
expect((unpack_dir/"file").stat.ino).to eq (unpack_dir/"hardlink").stat.ino
end

Expand Down
2 changes: 1 addition & 1 deletion Library/Homebrew/unpack_strategy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def extract_nestedly(to: nil, basename: nil, verbose: false, prioritize_extensio
FileUtils.chmod "u+w", path, verbose:
end

Directory.new(tmp_unpack_dir).extract(to:, verbose:)
Directory.new(tmp_unpack_dir, move: true).extract(to:, verbose:)
end
end

Expand Down
68 changes: 45 additions & 23 deletions Library/Homebrew/unpack_strategy/directory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,55 @@ def self.can_extract?(path)
path.directory?
end

sig {
params(
path: T.any(String, Pathname),
ref_type: T.nilable(Symbol),
ref: T.nilable(String),
merge_xattrs: T::Boolean,
move: T::Boolean,
).void
}
def initialize(path, ref_type: nil, ref: nil, merge_xattrs: false, move: false)
super(path, ref_type:, ref:, merge_xattrs:)
@move = move
end

private

sig { override.params(unpack_dir: Pathname, basename: Pathname, verbose: T::Boolean).void }
def extract_to_dir(unpack_dir, basename:, verbose:)
path_children = path.children
return if path_children.empty?

existing = unpack_dir.children

# We run a few cp attempts in the following order:
#
# 1. Start with `-al` to create hardlinks rather than copying files if the source and
# target are on the same filesystem. On macOS, this is the only cp option that can
# preserve hardlinks but it is only available since macOS 12.3 (file_cmds-353.100.22).
# 2. Try `-a` as GNU `cp -a` preserves hardlinks. macOS `cp -a` is identical to `cp -pR`.
# 3. Fall back on `-pR` to handle the case where GNU `cp -a` failed. This may happen if
# installing into a filesystem that doesn't support hardlinks like an exFAT USB drive.
cp_arg_attempts = ["-a", "-pR"]
cp_arg_attempts.unshift("-al") if path.stat.dev == unpack_dir.stat.dev

cp_arg_attempts.each do |arg|
args = [arg, *path_children, unpack_dir]
must_succeed = print_stderr = (arg == cp_arg_attempts.last)
result = system_command("cp", args:, verbose:, must_succeed:, print_stderr:)
break if result.success?

FileUtils.rm_r(unpack_dir.children - existing)
if @move
path.find(ignore_error: false) do |src|
next if src == path

dst = unpack_dir/src.relative_path_from(path)

if dst.exist?
dst_real_dir = dst.directory? && !dst.symlink?
src_real_dir = src.directory? && !src.symlink?

Check warning on line 45 in Library/Homebrew/unpack_strategy/directory.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/unpack_strategy/directory.rb#L45

Added line #L45 was not covered by tests
# Avoid trying to move a non-directory over an existing directory or vice versa.
# This is similar to `cp` which fails with 'cp: <dst>: Not a directory'.
# However, unlike `cp`, this will fail early rather than at the end.
raise "Cannot extract when only one of #{src} and #{dst} is a directory" if dst_real_dir ^ src_real_dir

# Remove non-directory and just let `mv` handle to simplify handling
dst.unlink unless dst_real_dir
end

# Defer writing over existing directories to let `cp` handle preserving information
unless dst.directory?
FileUtils.mv(src, dst)
Find.prune
end
end
end

path.each_child do |child|
system_command! "cp",
args: ["-pR", (child.directory? && !child.symlink?) ? "#{child}/." : child,
unpack_dir/child.basename],
verbose:
end
end
end
Expand Down

0 comments on commit a803cfe

Please sign in to comment.