
Combined with https://github.com/Homebrew/homebrew-test-bot/pull/247 this will test Homebrew's use of frozen strings in CI. After this we will then enable it for Homebrew developers and eventually all Homebrew users.
492 lines
11 KiB
Ruby
492 lines
11 KiB
Ruby
require "resource"
|
|
require "metafiles"
|
|
|
|
module DiskUsageExtension
|
|
def disk_usage
|
|
return @disk_usage if @disk_usage
|
|
|
|
compute_disk_usage
|
|
@disk_usage
|
|
end
|
|
|
|
def file_count
|
|
return @file_count if @file_count
|
|
|
|
compute_disk_usage
|
|
@file_count
|
|
end
|
|
|
|
def abv
|
|
out = +""
|
|
compute_disk_usage
|
|
out << "#{number_readable(@file_count)} files, " if @file_count > 1
|
|
out << disk_usage_readable(@disk_usage).to_s
|
|
out.freeze
|
|
end
|
|
|
|
private
|
|
|
|
def compute_disk_usage
|
|
if symlink? && !exist?
|
|
@file_count = 1
|
|
@disk_usage = 0
|
|
return
|
|
end
|
|
|
|
path = if symlink?
|
|
resolved_path
|
|
else
|
|
self
|
|
end
|
|
|
|
if path.directory?
|
|
scanned_files = Set.new
|
|
@file_count = 0
|
|
@disk_usage = 0
|
|
path.find do |f|
|
|
if f.directory?
|
|
@disk_usage += f.lstat.size
|
|
else
|
|
@file_count += 1 if f.basename.to_s != ".DS_Store"
|
|
# use Pathname#lstat instead of Pathname#stat to get info of symlink itself.
|
|
stat = f.lstat
|
|
file_id = [stat.dev, stat.ino]
|
|
# count hardlinks only once.
|
|
unless scanned_files.include?(file_id)
|
|
@disk_usage += stat.size
|
|
scanned_files.add(file_id)
|
|
end
|
|
end
|
|
end
|
|
else
|
|
@file_count = 1
|
|
@disk_usage = path.lstat.size
|
|
end
|
|
end
|
|
end
|
|
|
|
# Homebrew extends Ruby's `Pathname` to make our code more readable.
|
|
# @see https://ruby-doc.org/stdlib-1.8.7/libdoc/pathname/rdoc/Pathname.html Ruby's Pathname API
|
|
class Pathname
|
|
include DiskUsageExtension
|
|
|
|
# @private
|
|
BOTTLE_EXTNAME_RX = /(\.[a-z0-9_]+\.bottle\.(\d+\.)?tar\.gz)$/.freeze
|
|
|
|
# Moves a file from the original location to the {Pathname}'s.
|
|
def install(*sources)
|
|
sources.each do |src|
|
|
case src
|
|
when Resource
|
|
src.stage(self)
|
|
when Resource::Partial
|
|
src.resource.stage { install(*src.files) }
|
|
when Array
|
|
if src.empty?
|
|
opoo "tried to install empty array to #{self}"
|
|
break
|
|
end
|
|
src.each { |s| install_p(s, File.basename(s)) }
|
|
when Hash
|
|
if src.empty?
|
|
opoo "tried to install empty hash to #{self}"
|
|
break
|
|
end
|
|
src.each { |s, new_basename| install_p(s, new_basename) }
|
|
else
|
|
install_p(src, File.basename(src))
|
|
end
|
|
end
|
|
end
|
|
|
|
def install_p(src, new_basename)
|
|
raise Errno::ENOENT, src.to_s unless File.symlink?(src) || File.exist?(src)
|
|
|
|
src = Pathname(src)
|
|
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.
|
|
if src.symlink?
|
|
raise unless Kernel.system "mv", src, dst
|
|
else
|
|
FileUtils.mv src, dst
|
|
end
|
|
end
|
|
private :install_p
|
|
|
|
# Creates symlinks to sources in this folder.
|
|
def install_symlink(*sources)
|
|
sources.each do |src|
|
|
case src
|
|
when Array
|
|
src.each { |s| install_symlink_p(s, File.basename(s)) }
|
|
when Hash
|
|
src.each { |s, new_basename| install_symlink_p(s, new_basename) }
|
|
else
|
|
install_symlink_p(src, File.basename(src))
|
|
end
|
|
end
|
|
end
|
|
|
|
def install_symlink_p(src, new_basename)
|
|
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)
|
|
end
|
|
private :install_symlink_p
|
|
|
|
# @private
|
|
alias old_write write
|
|
|
|
# We assume this pathname object is a file, obviously
|
|
def write(content, *open_args)
|
|
raise "Will not overwrite #{self}" if exist?
|
|
|
|
dirname.mkpath
|
|
open("w", *open_args) { |f| f.write(content) }
|
|
end
|
|
|
|
# Only appends to a file that is already created.
|
|
def append_lines(content, *open_args)
|
|
raise "Cannot append file that doesn't exist: #{self}" unless exist?
|
|
|
|
open("a", *open_args) { |f| f.puts(content) }
|
|
end
|
|
|
|
# NOTE: This always overwrites.
|
|
def atomic_write(content)
|
|
old_stat = stat if exist?
|
|
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)
|
|
rescue Errno::EPERM, Errno::EACCES
|
|
# Changing file ownership failed, moving on.
|
|
nil
|
|
end
|
|
begin
|
|
# This operation will affect filesystem ACL's
|
|
chmod(old_stat.mode)
|
|
rescue Errno::EPERM, Errno::EACCES
|
|
# Changing file permissions failed, moving on.
|
|
nil
|
|
end
|
|
end
|
|
|
|
# @private
|
|
def cp_path_sub(pattern, replacement)
|
|
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
|
|
|
|
# @private
|
|
alias extname_old extname
|
|
|
|
# Extended to support common double extensions
|
|
def extname(path = to_s)
|
|
basename = File.basename(path)
|
|
|
|
bottle_ext = basename[BOTTLE_EXTNAME_RX, 1]
|
|
return bottle_ext if bottle_ext
|
|
|
|
archive_ext = basename[/(\.(tar|cpio|pax)\.(gz|bz2|lz|xz|Z))\Z/, 1]
|
|
return archive_ext if archive_ext
|
|
|
|
# Don't treat version numbers as extname.
|
|
return "" if basename.match?(/\b\d+\.\d+[^\.]*\Z/) && !basename.end_with?(".7z")
|
|
|
|
File.extname(basename)
|
|
end
|
|
|
|
# For filetypes we support, basename without extension
|
|
def stem
|
|
File.basename((path = to_s), extname(path))
|
|
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
|
|
# @private
|
|
def rmdir_if_possible
|
|
rmdir
|
|
true
|
|
rescue Errno::ENOTEMPTY
|
|
if (ds_store = join(".DS_Store")).exist? && children.length == 1
|
|
ds_store.unlink
|
|
retry
|
|
else
|
|
false
|
|
end
|
|
rescue Errno::EACCES, Errno::ENOENT, Errno::EBUSY
|
|
false
|
|
end
|
|
|
|
# @private
|
|
def version
|
|
require "version"
|
|
Version.parse(basename)
|
|
end
|
|
|
|
# @private
|
|
def text_executable?
|
|
/^#!\s*\S+/ =~ open("r") { |f| f.read(1024) }
|
|
end
|
|
|
|
def sha256
|
|
require "digest/sha2"
|
|
Digest::SHA256.file(self).hexdigest
|
|
end
|
|
|
|
def verify_checksum(expected)
|
|
raise ChecksumMissingError if expected.nil? || expected.empty?
|
|
|
|
actual = Checksum.new(expected.hash_type, send(expected.hash_type).downcase)
|
|
raise ChecksumMismatchError.new(self, expected, actual) unless expected == actual
|
|
end
|
|
|
|
alias to_str to_s
|
|
|
|
def cd
|
|
Dir.chdir(self) { yield self }
|
|
end
|
|
|
|
def subdirs
|
|
children.select(&:directory?)
|
|
end
|
|
|
|
# @private
|
|
def resolved_path
|
|
symlink? ? dirname.join(readlink) : self
|
|
end
|
|
|
|
# @private
|
|
def resolved_path_exists?
|
|
link = readlink
|
|
rescue ArgumentError
|
|
# The link target contains NUL bytes
|
|
false
|
|
else
|
|
dirname.join(link).exist?
|
|
end
|
|
|
|
# @private
|
|
def make_relative_symlink(src)
|
|
dirname.mkpath
|
|
File.symlink(src.relative_path_from(dirname), self)
|
|
end
|
|
|
|
# @private
|
|
def ensure_writable
|
|
saved_perms = nil
|
|
unless writable_real?
|
|
saved_perms = stat.mode
|
|
FileUtils.chmod "u+rw", to_path
|
|
end
|
|
yield
|
|
ensure
|
|
chmod saved_perms if saved_perms
|
|
end
|
|
|
|
# @private
|
|
def install_info
|
|
quiet_system "/usr/bin/install-info", "--quiet", to_s, "#{dirname}/dir"
|
|
end
|
|
|
|
# @private
|
|
def uninstall_info
|
|
quiet_system "/usr/bin/install-info", "--delete", "--quiet", to_s, "#{dirname}/dir"
|
|
end
|
|
|
|
# Writes an exec script in this folder for each target pathname
|
|
def write_exec_script(*targets)
|
|
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}" "$@"
|
|
SH
|
|
end
|
|
end
|
|
|
|
# Writes an exec script that sets environment variables
|
|
def write_env_script(target, env)
|
|
env_export = +""
|
|
env.each { |key, value| env_export << "#{key}=\"#{value}\" " }
|
|
dirname.mkpath
|
|
write <<~SH
|
|
#!/bin/bash
|
|
#{env_export}exec "#{target}" "$@"
|
|
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?
|
|
|
|
dst.install(file)
|
|
new_file = dst.join(file.basename)
|
|
file.write_env_script(new_file, env)
|
|
end
|
|
end
|
|
|
|
# Writes an exec script that invokes a Java jar
|
|
def write_jar_script(target_jar, script_name, java_opts = "", java_version: nil)
|
|
mkpath
|
|
java_home = ("JAVA_HOME=\"#{Language::Java.java_home_shell(java_version)}\" " if java_version)
|
|
join(script_name).write <<~SH
|
|
#!/bin/bash
|
|
#{java_home}exec java #{java_opts} -jar #{target_jar} "$@"
|
|
SH
|
|
end
|
|
|
|
def install_metafiles(from = Pathname.pwd)
|
|
Pathname(from).children.each do |p|
|
|
next if p.directory?
|
|
next unless Metafiles.copy?(p.basename.to_s)
|
|
|
|
# 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?
|
|
|
|
filename.chmod 0644
|
|
install(filename)
|
|
end
|
|
end
|
|
|
|
def ds_store?
|
|
basename.to_s == ".DS_Store"
|
|
end
|
|
|
|
# https://bugs.ruby-lang.org/issues/9915
|
|
if RUBY_VERSION == "2.0.0"
|
|
prepend Module.new {
|
|
def inspect
|
|
super.force_encoding(@path.encoding)
|
|
end
|
|
}
|
|
end
|
|
|
|
def binary_executable?
|
|
false
|
|
end
|
|
|
|
def mach_o_bundle?
|
|
false
|
|
end
|
|
|
|
def dylib?
|
|
false
|
|
end
|
|
end
|
|
|
|
require "extend/os/pathname"
|
|
|
|
# @private
|
|
module ObserverPathnameExtension
|
|
class << self
|
|
attr_accessor :n, :d
|
|
|
|
def reset_counts!
|
|
@n = @d = 0
|
|
@put_verbose_trimmed_warning = false
|
|
end
|
|
|
|
def total
|
|
n + d
|
|
end
|
|
|
|
def counts
|
|
[n, d]
|
|
end
|
|
|
|
MAXIMUM_VERBOSE_OUTPUT = 100
|
|
|
|
def verbose?
|
|
return ARGV.verbose? unless ENV["CI"]
|
|
return false unless ARGV.verbose?
|
|
|
|
if total < MAXIMUM_VERBOSE_OUTPUT
|
|
true
|
|
else
|
|
unless @put_verbose_trimmed_warning
|
|
puts "Only the first #{MAXIMUM_VERBOSE_OUTPUT} operations were output."
|
|
@put_verbose_trimmed_warning = true
|
|
end
|
|
false
|
|
end
|
|
end
|
|
end
|
|
|
|
def unlink
|
|
super
|
|
puts "rm #{self}" if ObserverPathnameExtension.verbose?
|
|
ObserverPathnameExtension.n += 1
|
|
end
|
|
|
|
def mkpath
|
|
super
|
|
puts "mkdir -p #{self}" if ObserverPathnameExtension.verbose?
|
|
end
|
|
|
|
def rmdir
|
|
super
|
|
puts "rmdir #{self}" if ObserverPathnameExtension.verbose?
|
|
ObserverPathnameExtension.d += 1
|
|
end
|
|
|
|
def make_relative_symlink(src)
|
|
super
|
|
puts "ln -s #{src.relative_path_from(dirname)} #{basename}" if ObserverPathnameExtension.verbose?
|
|
ObserverPathnameExtension.n += 1
|
|
end
|
|
|
|
def install_info
|
|
super
|
|
puts "info #{self}" if ObserverPathnameExtension.verbose?
|
|
end
|
|
|
|
def uninstall_info
|
|
super
|
|
puts "uninfo #{self}" if ObserverPathnameExtension.verbose?
|
|
end
|
|
end
|