brew/Library/Homebrew/extend/pathname.rb

457 lines
12 KiB
Ruby
Raw Normal View History

rubocop: Use `Sorbet/StrictSigil` as it's better than comments - Previously I thought that comments were fine to discourage people from wasting their time trying to bump things that used `undef` that Sorbet didn't support. But RuboCop is better at this since it'll complain if the comments are unnecessary. - Suggested in https://github.com/Homebrew/brew/pull/18018#issuecomment-2283369501. - I've gone for a mixture of `rubocop:disable` for the files that can't be `typed: strict` (use of undef, required before everything else, etc) and `rubocop:todo` for everything else that should be tried to make strictly typed. There's no functional difference between the two as `rubocop:todo` is `rubocop:disable` with a different name. - And I entirely disabled the cop for the docs/ directory since `typed: strict` isn't going to gain us anything for some Markdown linting config files. - This means that now it's easier to track what needs to be done rather than relying on checklists of files in our big Sorbet issue: ```shell $ git grep 'typed: true # rubocop:todo Sorbet/StrictSigil' | wc -l 268 ``` - And this is confirmed working for new files: ```shell $ git status On branch use-rubocop-for-sorbet-strict-sigils Untracked files: (use "git add <file>..." to include in what will be committed) Library/Homebrew/bad.rb Library/Homebrew/good.rb nothing added to commit but untracked files present (use "git add" to track) $ brew style Offenses: bad.rb:1:1: C: Sorbet/StrictSigil: Sorbet sigil should be at least strict got true. ^^^^^^^^^^^^^ 1340 files inspected, 1 offense detected ```
2024-08-12 10:30:59 +01:00
# typed: true # rubocop:todo Sorbet/StrictSigil
# frozen_string_literal: true
require "system_command"
require "extend/pathname/disk_usage_extension"
require "extend/pathname/observer_pathname_extension"
# Homebrew extends Ruby's `Pathname` to make our code more readable.
2020-11-03 12:39:26 -05:00
# @see https://ruby-doc.org/stdlib-2.6.3/libdoc/pathname/rdoc/Pathname.html Ruby's Pathname API
class Pathname
include SystemCommand::Mixin
include DiskUsageExtension
# Moves a file from the original location to the {Pathname}'s.
#
# @api public
sig {
params(sources: T.any(
Resource, Resource::Partial, String, Pathname,
T::Array[T.any(String, Pathname)], T::Hash[T.any(String, Pathname), String]
)).void
}
def install(*sources)
2012-02-09 18:43:47 -08:00
sources.each do |src|
case src
when Resource
src.stage(self)
when Resource::Partial
src.resource.stage { install(*src.files) }
2012-02-09 18:43:47 -08:00
when Array
if src.empty?
opoo "Tried to install empty array to #{self}"
2016-09-21 09:58:26 +02:00
break
end
src.each { |s| install_p(s, File.basename(s)) }
2012-02-09 18:43:47 -08:00
when Hash
if src.empty?
opoo "Tried to install empty hash to #{self}"
2016-09-21 09:58:26 +02:00
break
end
src.each { |s, new_basename| install_p(s, new_basename) }
2012-02-09 18:43:47 -08:00
else
install_p(src, File.basename(src))
2012-02-09 18:43:47 -08:00
end
end
end
sig { params(src: T.any(String, Pathname), new_basename: String).void }
def install_p(src, new_basename)
2015-03-26 22:22:45 -04:00
src = Pathname(src)
raise Errno::ENOENT, src.to_s if !src.symlink? && !src.exist?
2015-03-26 22:22:45 -04:00
dst = join(new_basename)
dst = yield(src, dst) if block_given?
return unless dst
mkpath
# Use `FileUtils.mv` over `File.rename` to handle filesystem boundaries. If `src`
# is a symlink and its target is moved first, `FileUtils.mv` will fail
# (https://bugs.ruby-lang.org/issues/7707).
#
# In that case, use the system `mv` command.
2015-03-26 22:22:45 -04:00
if src.symlink?
2023-11-05 08:55:58 -08:00
raise unless Kernel.system "mv", src.to_s, dst
else
FileUtils.mv src, dst
end
end
private :install_p
2012-02-12 10:36:16 -08:00
# Creates symlinks to sources in this folder.
#
# @api public
sig {
params(
sources: T.any(String, Pathname, T::Array[T.any(String, Pathname)], T::Hash[T.any(String, Pathname), String]),
).void
}
def install_symlink(*sources)
2012-02-12 10:36:16 -08:00
sources.each do |src|
case src
when Array
src.each { |s| install_symlink_p(s, File.basename(s)) }
2012-02-12 10:36:16 -08:00
when Hash
src.each { |s, new_basename| install_symlink_p(s, new_basename) }
2012-02-12 10:36:16 -08:00
else
install_symlink_p(src, File.basename(src))
2012-02-12 10:36:16 -08:00
end
end
end
def install_symlink_p(src, new_basename)
2012-02-12 10:36:16 -08:00
mkpath
dstdir = realpath
src = Pathname(src).expand_path(dstdir)
src = src.dirname.realpath/src.basename if src.dirname.exist?
FileUtils.ln_sf(src.relative_path_from(dstdir), dstdir/new_basename)
2012-02-12 10:36:16 -08:00
end
private :install_symlink_p
2012-02-12 10:36:16 -08:00
# Only appends to a file that is already created.
#
# @api public
sig { params(content: String, open_args: T.untyped).void }
def append_lines(content, **open_args)
raise "Cannot append file that doesn't exist: #{self}" unless exist?
2018-09-17 02:45:00 +02:00
T.unsafe(self).open("a", **open_args) { |f| f.puts(content) }
end
# Write to a file atomically.
#
# NOTE: This always overwrites.
#
# @api public
sig { params(content: String).void }
def atomic_write(content)
require "extend/file/atomic"
2019-04-13 17:18:14 +00:00
old_stat = stat if exist?
2018-11-22 23:37:57 +01:00
File.atomic_write(self) do |file|
file.write(content)
end
return unless old_stat
# Try to restore original file's permissions separately
# atomic_write does it itself, but it actually erases
# them if chown fails
begin
# Set correct permissions on new file
chown(old_stat.uid, nil)
chown(nil, old_stat.gid)
2019-04-13 17:18:14 +00:00
rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
2019-04-13 17:18:14 +00:00
nil
end
begin
# This operation will affect filesystem ACL's
chmod(old_stat.mode)
2019-04-13 17:18:14 +00:00
rescue Errno::EPERM, Errno::EACCES
# Changing file permissions failed, moving on.
2019-04-13 17:18:14 +00:00
nil
end
end
def cp_path_sub(pattern, replacement)
2016-09-11 17:53:00 +01:00
raise "#{self} does not exist" unless exist?
dst = sub(pattern, replacement)
raise "#{self} is the same file as #{dst}" if self == dst
if directory?
dst.mkpath
else
dst.dirname.mkpath
dst = yield(self, dst) if block_given?
FileUtils.cp(self, dst)
end
end
# Extended to support common double extensions.
#
# @api public
sig { returns(String) }
def extname
basename = File.basename(self)
2018-08-10 00:54:03 +02:00
bottle_ext, = HOMEBREW_BOTTLES_EXTNAME_REGEX.match(basename).to_a
return bottle_ext if bottle_ext
2018-08-10 00:54:03 +02:00
2021-09-16 15:56:31 +01:00
archive_ext = basename[/(\.(tar|cpio|pax)\.(gz|bz2|lz|xz|zst|Z))\Z/, 1]
return archive_ext if archive_ext
2018-08-10 00:54:03 +02:00
# Don't treat version numbers as extname.
2020-06-02 09:49:23 +01:00
return "" if basename.match?(/\b\d+\.\d+[^.]*\Z/) && !basename.end_with?(".7z")
2018-08-10 00:54:03 +02:00
File.extname(basename)
end
# For filetypes we support, returns basename without extension.
#
# @api public
sig { returns(String) }
def stem
File.basename(self, extname)
end
# I don't trust the children.length == 0 check particularly, not to mention
# it is slow to enumerate the whole directory just to see if it is empty,
# instead rely on good ol' libc and the filesystem
sig { returns(T::Boolean) }
def rmdir_if_possible
rmdir
true
2013-04-27 15:21:05 -05:00
rescue Errno::ENOTEMPTY
2017-06-01 16:06:51 +02:00
if (ds_store = join(".DS_Store")).exist? && children.length == 1
2013-04-27 15:21:05 -05:00
ds_store.unlink
retry
2013-04-27 15:21:05 -05:00
else
false
end
rescue Errno::EACCES, Errno::ENOENT, Errno::EBUSY, Errno::EPERM
false
end
sig { returns(Version) }
def version
require "version"
Version.parse(basename)
end
sig { returns(T::Boolean) }
def text_executable?
/\A#!\s*\S+/.match?(open("r") { |f| f.read(1024) })
end
sig { returns(String) }
2013-10-14 22:05:30 -05:00
def sha256
require "digest/sha2"
Digest::SHA256.file(self).hexdigest
2009-12-30 18:56:46 +00:00
end
sig { params(expected: T.nilable(Checksum)).void }
def verify_checksum(expected)
2020-12-01 17:04:59 +00:00
raise ChecksumMissingError if expected.blank?
2018-09-17 02:45:00 +02:00
actual = Checksum.new(sha256.downcase)
2023-04-18 15:06:50 -07:00
raise ChecksumMismatchError.new(self, expected, actual) if expected != actual
end
2009-12-30 18:56:46 +00:00
alias to_str to_s
2010-02-27 12:29:45 +00:00
# Change to this directory, optionally executing the given block.
#
# @api public
sig {
type_parameters(:U).params(
_block: T.proc.params(path: Pathname).returns(T.type_parameter(:U)),
).returns(T.type_parameter(:U))
}
def cd(&_block)
2017-02-22 07:19:16 +01:00
Dir.chdir(self) { yield self }
2010-02-27 12:29:45 +00:00
end
# Get all sub-directories of this directory.
#
# @api public
2020-10-20 12:03:48 +02:00
sig { returns(T::Array[Pathname]) }
2010-02-27 12:29:45 +00:00
def subdirs
children.select(&:directory?)
2010-02-27 12:29:45 +00:00
end
2020-10-20 12:03:48 +02:00
sig { returns(Pathname) }
2010-07-25 12:07:35 -07:00
def resolved_path
2017-06-01 16:06:51 +02:00
symlink? ? dirname.join(readlink) : self
2010-07-25 12:07:35 -07:00
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
def resolved_path_exists?
link = readlink
rescue ArgumentError
# The link target contains NUL bytes
false
else
2017-06-01 16:06:51 +02:00
dirname.join(link).exist?
end
def make_relative_symlink(src)
dirname.mkpath
File.symlink(src.relative_path_from(dirname), self)
2010-08-15 17:17:26 -07:00
end
def ensure_writable
2011-06-16 17:38:52 +01:00
saved_perms = nil
2024-03-27 06:26:32 +00:00
unless writable?
2011-06-16 17:38:52 +01:00
saved_perms = stat.mode
FileUtils.chmod "u+rw", to_path
end
yield
ensure
chmod saved_perms if saved_perms
end
def which_install_info
@which_install_info ||=
if File.executable?("/usr/bin/install-info")
"/usr/bin/install-info"
elsif Formula["texinfo"].any_version_installed?
Formula["texinfo"].opt_bin/"install-info"
end
end
def install_info
quiet_system(which_install_info, "--quiet", to_s, "#{dirname}/dir")
end
def uninstall_info
quiet_system(which_install_info, "--delete", "--quiet", to_s, "#{dirname}/dir")
end
2012-03-02 20:28:54 +00:00
# Writes an exec script in this folder for each target pathname.
def write_exec_script(*targets)
2013-06-20 16:18:01 -05:00
targets.flatten!
if targets.empty?
opoo "Tried to write exec scripts to #{self} for an empty list of targets"
return
end
mkpath
targets.each do |target|
target = Pathname.new(target) # allow pathnames or strings
join(target.basename).write <<~SH
#!/bin/bash
exec "#{target}" "$@"
2018-07-11 15:17:40 +02:00
SH
end
end
# Writes an exec script that sets environment variables.
def write_env_script(target, args, env = nil)
unless env
env = args
args = nil
end
env_export = +""
env.each { |key, value| env_export << "#{key}=\"#{value}\" " }
dirname.mkpath
2018-07-11 15:17:40 +02:00
write <<~SH
2017-10-15 02:28:32 +02:00
#!/bin/bash
#{env_export}exec "#{target}" #{args} "$@"
2018-07-11 15:17:40 +02:00
SH
end
# Writes a wrapper env script and moves all files to the dst.
def env_script_all_files(dst, env)
dst.mkpath
Pathname.glob("#{self}/*") do |file|
next if file.directory?
2018-09-17 02:45:00 +02:00
dst.install(file)
2017-06-01 16:06:51 +02:00
new_file = dst.join(file.basename)
file.write_env_script(new_file, env)
end
end
# Writes an exec script that invokes a Java jar.
sig {
params(
target_jar: T.any(String, Pathname),
script_name: T.any(String, Pathname),
java_opts: String,
java_version: T.nilable(String),
).returns(Integer)
}
def write_jar_script(target_jar, script_name, java_opts = "", java_version: nil)
mkpath
(self/script_name).write <<~EOS
#!/bin/bash
export JAVA_HOME="#{Language::Java.overridable_java_home_env(java_version)[:JAVA_HOME]}"
exec "${JAVA_HOME}/bin/java" #{java_opts} -jar "#{target_jar}" "$@"
EOS
end
def install_metafiles(from = Pathname.pwd)
require "metafiles"
2014-06-07 17:52:11 -05:00
Pathname(from).children.each do |p|
next if p.directory?
2023-03-07 08:54:02 +00:00
next if File.empty?(p)
2014-06-07 23:40:26 -05:00
next unless Metafiles.copy?(p.basename.to_s)
2018-09-17 02:45:00 +02:00
# Some software symlinks these files (see help2man.rb)
filename = p.resolved_path
# Some software links metafiles together, so by the time we iterate to one of them
# we may have already moved it. libxml2's COPYING and Copyright are affected by this.
next unless filename.exist?
2018-09-17 02:45:00 +02:00
filename.chmod 0644
2014-06-07 17:52:11 -05:00
install(filename)
end
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
2017-02-24 17:44:18 +09:00
def ds_store?
basename.to_s == ".DS_Store"
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
def binary_executable?
false
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
2017-12-01 16:43:00 -08:00
def mach_o_bundle?
false
end
2020-10-20 12:03:48 +02:00
sig { returns(T::Boolean) }
def dylib?
false
end
sig { params(_wanted_arch: Symbol).returns(T::Boolean) }
def arch_compatible?(_wanted_arch)
true
end
sig { returns(T::Array[String]) }
def rpaths
[]
end
2023-03-20 13:15:43 -07:00
2023-03-20 13:35:29 -07:00
sig { returns(String) }
2023-03-20 13:15:43 -07:00
def magic_number
@magic_number ||= if directory?
""
else
# Length of the longest regex (currently Tar).
max_magic_number_length = 262
2024-01-26 12:00:13 -08:00
binread(max_magic_number_length) || ""
2023-03-20 13:15:43 -07:00
end
end
2023-03-20 13:35:29 -07:00
sig { returns(String) }
2023-03-20 13:15:43 -07:00
def file_type
@file_type ||= system_command("file", args: ["-b", self], print_stderr: false)
.stdout.chomp
end
2023-03-20 13:35:29 -07:00
sig { returns(T::Array[String]) }
2023-03-20 13:15:43 -07:00
def zipinfo
@zipinfo ||= system_command("zipinfo", args: ["-1", self], print_stderr: false)
.stdout
.encode(Encoding::UTF_8, invalid: :replace)
.split("\n")
end
# Like regular `rmtree`, except it never ignores errors.
#
# This was the default behaviour in Ruby 3.1 and earlier.
#
# @api public
def rmtree(noop: nil, verbose: nil, secure: nil)
# Ideally we'd odeprecate this but probably can't given gems so let's
# create a RuboCop autocorrect instead soon.
# This is why monkeypatching is non-ideal (but right solution to get
# Ruby 3.3 over the line).
odisabled "rmtree", "FileUtils#rm_r"
FileUtils.rm_r(@path, noop:, verbose:, secure:)
nil
end
end
require "extend/os/pathname"