brew/Library/Homebrew/linkage_checker.rb

331 lines
10 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
2016-07-07 20:41:14 +08:00
require "keg"
require "formula"
require "linkage_cache_store"
require "fiddle"
2016-07-07 20:41:14 +08:00
2020-08-17 19:56:24 +02:00
# Check for broken/missing linkage in a formula's keg.
2016-07-07 20:41:14 +08:00
class LinkageChecker
2020-07-07 11:29:33 +01:00
attr_reader :undeclared_deps, :keg, :formula, :store
def initialize(keg, formula = nil, cache_db:, rebuild_cache: false)
2016-07-07 20:41:14 +08:00
@keg = keg
@formula = formula || resolve_formula(keg)
@store = LinkageCacheStore.new(keg.to_s, cache_db)
@system_dylibs = Set.new
@broken_dylibs = Set.new
@variable_dylibs = Set.new
@brewed_dylibs = Hash.new { |h, k| h[k] = Set.new }
@reverse_links = Hash.new { |h, k| h[k] = Set.new }
@broken_deps = Hash.new { |h, k| h[k] = [] }
@indirect_deps = []
@undeclared_deps = []
@unnecessary_deps = []
@unwanted_system_dylibs = []
@version_conflict_deps = []
@files_missing_rpaths = []
@executable_path_dylibs = []
2024-03-07 16:20:20 +00:00
check_dylibs(rebuild_cache:)
end
def display_normal_output
display_items "System libraries", @system_dylibs
display_items "Homebrew libraries", @brewed_dylibs
display_items "Indirect dependencies with linkage", @indirect_deps
display_items "@rpath-referenced libraries", @variable_dylibs
display_items "Missing libraries", @broken_dylibs
display_items "Broken dependencies", @broken_deps
display_items "Undeclared dependencies with linkage", @undeclared_deps
display_items "Dependencies with no linkage", @unnecessary_deps
display_items "Unwanted system libraries", @unwanted_system_dylibs
display_items "Files with missing rpath", @files_missing_rpaths
display_items "@executable_path references in libraries", @executable_path_dylibs
end
def display_reverse_output
return if @reverse_links.empty?
2018-09-17 02:45:00 +02:00
2018-06-02 20:58:34 +05:30
sorted = @reverse_links.sort
sorted.each do |dylib, files|
puts dylib
files.each do |f|
2018-09-15 00:04:01 +02:00
unprefixed = f.to_s.delete_prefix "#{keg}/"
puts " #{unprefixed}"
end
puts if dylib != sorted.last.first
end
end
def display_test_output(puts_output: true, strict: false)
2024-03-07 16:20:20 +00:00
display_items("Missing libraries", @broken_dylibs, puts_output:)
display_items("Broken dependencies", @broken_deps, puts_output:)
display_items("Unwanted system libraries", @unwanted_system_dylibs, puts_output:)
display_items("Conflicting libraries", @version_conflict_deps, puts_output:)
return unless strict
display_items("Indirect dependencies with linkage", @indirect_deps, puts_output:)
2024-03-07 16:20:20 +00:00
display_items("Undeclared dependencies with linkage", @undeclared_deps, puts_output:)
display_items("Files with missing rpath", @files_missing_rpaths, puts_output:)
display_items "@executable_path references in libraries", @executable_path_dylibs, puts_output:
end
sig { params(test: T::Boolean, strict: T::Boolean).returns(T::Boolean) }
def broken_library_linkage?(test: false, strict: false)
raise ArgumentError, "Strict linkage checking requires test mode to be enabled." if strict && !test
issues = [@broken_deps, @broken_dylibs]
if test
issues += [@unwanted_system_dylibs, @version_conflict_deps]
issues += [@indirect_deps, @undeclared_deps, @files_missing_rpaths, @executable_path_dylibs] if strict
end
issues.any?(&:present?)
2020-07-10 21:20:46 +00:00
end
private
def dylib_to_dep(dylib)
2020-11-16 22:18:56 +01:00
dylib =~ %r{#{Regexp.escape(HOMEBREW_PREFIX)}/(opt|Cellar)/([\w+-.@]+)/}o
2018-03-20 12:30:14 -05:00
Regexp.last_match(2)
end
sig { params(file: String).returns(T::Boolean) }
def broken_dylibs_allowed?(file)
return false if formula.name != "julia"
file.start_with?("#{formula.prefix.realpath}/share/julia/compiled/")
end
def check_dylibs(rebuild_cache:)
keg_files_dylibs = nil
if rebuild_cache
store&.delete!
else
keg_files_dylibs = store&.fetch(:keg_files_dylibs)
end
keg_files_dylibs_was_empty = false
keg_files_dylibs ||= {}
if keg_files_dylibs.empty?
keg_files_dylibs_was_empty = true
@keg.find do |file|
next if file.symlink? || file.directory?
next if !file.dylib? && !file.binary_executable? && !file.mach_o_bundle?
next unless file.arch_compatible?(Hardware::CPU.arch)
2018-09-17 02:45:00 +02:00
# weakly loaded dylibs may not actually exist on disk, so skip them
# when checking for broken linkage
keg_files_dylibs[file] =
2024-06-10 18:56:50 +01:00
file.dynamically_linked_libraries(except: :DYLIB_USE_WEAK_LINK)
end
end
checked_dylibs = Set.new
keg_files_dylibs.each do |file, dylibs|
file_has_any_rpath_dylibs = T.let(false, T::Boolean)
dylibs.each do |dylib|
2016-07-07 20:41:14 +08:00
@reverse_links[dylib] << file
# Files that link @rpath-prefixed dylibs must include at
# least one rpath in order to resolve it.
if !file_has_any_rpath_dylibs && (dylib.start_with? "@rpath/")
file_has_any_rpath_dylibs = true
pathname = Pathname(file)
@files_missing_rpaths << file if pathname.rpaths.empty? && !broken_dylibs_allowed?(file.to_s)
end
next if checked_dylibs.include? dylib
2018-09-17 02:45:00 +02:00
checked_dylibs << dylib
if dylib.start_with? "@rpath"
2016-07-07 20:41:14 +08:00
@variable_dylibs << dylib
next
elsif dylib.start_with?("@executable_path") && !Pathname(file).binary_executable?
@executable_path_dylibs << dylib
next
end
begin
owner = Keg.for(Pathname(dylib))
rescue NotAKegError
@system_dylibs << dylib
rescue Errno::ENOENT
next if harmless_broken_link?(dylib)
2018-09-17 02:45:00 +02:00
if (dep = dylib_to_dep(dylib))
@broken_deps[dep] |= [dylib]
elsif system_libraries_exist_in_cache? && dylib_found_in_shared_cache?(dylib)
# If we cannot associate the dylib with a dependency, then it may be a system library.
# Check the dylib shared cache for the library to verify this.
@system_dylibs << dylib
elsif !system_framework?(dylib) && !broken_dylibs_allowed?(file.to_s)
@broken_dylibs << dylib
end
2016-07-07 20:41:14 +08:00
else
tap = owner.tab.tap
f = if tap.nil? || tap.core_tap?
owner.name
2016-07-07 20:41:14 +08:00
else
"#{tap}/#{owner.name}"
2016-07-07 20:41:14 +08:00
end
@brewed_dylibs[f] << dylib
2016-07-07 20:41:14 +08:00
end
end
end
2018-05-22 14:46:14 +01:00
if formula
@indirect_deps, @undeclared_deps, @unnecessary_deps,
@version_conflict_deps = check_formula_deps
2018-05-22 14:46:14 +01:00
end
return unless keg_files_dylibs_was_empty
2024-03-07 16:20:20 +00:00
store&.update!(keg_files_dylibs:)
end
2023-11-15 19:52:21 +00:00
def system_libraries_exist_in_cache?
false
end
def dylib_found_in_shared_cache?(dylib)
@dyld_shared_cache_contains_path ||= begin
libc = Fiddle.dlopen("/usr/lib/libSystem.B.dylib")
Fiddle::Function.new(
libc["_dyld_shared_cache_contains_path"],
[Fiddle::TYPE_CONST_STRING],
Fiddle::TYPE_BOOL,
)
end
@dyld_shared_cache_contains_path.call(dylib)
end
def check_formula_deps
filter_out = proc do |dep|
next true if dep.build? || dep.test?
2018-09-17 02:45:00 +02:00
(dep.optional? || dep.recommended?) && formula.build.without?(dep)
end
declared_deps_full_names = formula.deps
.reject { |dep| filter_out.call(dep) }
.map(&:name)
declared_deps_names = declared_deps_full_names.map do |dep|
dep.split("/").last
end
recursive_deps = formula.runtime_formula_dependencies(undeclared: false)
.map(&:name)
indirect_deps = []
undeclared_deps = []
@brewed_dylibs.each_key do |full_name|
name = full_name.split("/").last
next if name == formula.name
2018-09-17 02:45:00 +02:00
if recursive_deps.include?(name)
indirect_deps << full_name unless declared_deps_names.include?(name)
else
undeclared_deps << full_name
end
end
sort_by_formula_full_name!(indirect_deps)
sort_by_formula_full_name!(undeclared_deps)
unnecessary_deps = declared_deps_full_names.reject do |full_name|
next true if Formula[full_name].bin.directory?
2018-09-17 02:45:00 +02:00
name = full_name.split("/").last
@brewed_dylibs.keys.map { |l| l.split("/").last }.include?(name)
end
2018-04-07 15:35:27 -05:00
missing_deps = @broken_deps.values.flatten.map { |d| dylib_to_dep(d) }
unnecessary_deps -= missing_deps
version_hash = {}
version_conflict_deps = Set.new
2020-02-19 11:18:40 +00:00
@brewed_dylibs.each_key do |l|
name = l.split("/").last
unversioned_name, = name.split("@")
version_hash[unversioned_name] ||= Set.new
version_hash[unversioned_name] << name
next if version_hash[unversioned_name].length < 2
2018-09-17 02:45:00 +02:00
version_conflict_deps += version_hash[unversioned_name]
end
[indirect_deps, undeclared_deps,
unnecessary_deps, version_conflict_deps.to_a]
end
def sort_by_formula_full_name!(arr)
arr.sort! do |a, b|
2020-12-01 17:04:59 +00:00
if a.include?("/") && b.exclude?("/")
1
2020-12-01 17:04:59 +00:00
elsif a.exclude?("/") && b.include?("/")
-1
else
a <=> b
2016-07-07 20:41:14 +08:00
end
end
2016-07-07 20:41:14 +08:00
end
# Whether or not dylib is a harmless broken link, meaning that it's
# okay to skip (and not report) as broken.
def harmless_broken_link?(dylib)
# libgcc_s_* is referenced by programs that use the Java Service Wrapper,
# and is harmless on x86(_64) machines
# dyld will fall back to Apple libc++ if LLVM's is not available.
linkage_checker: report linkage with system frameworks Currently, `brew linkage` reports linkage with system frameworks only if they can be found on the file system. This results in this linkage not being reported on Big Sur and newer, where system libraries are stored in the dyld cache instead. Let's fix that by avoiding silently ignoring system frameworks by moving them out of `#harmless_broken_link?`. We retain the behaviour desired from 7228e60da51392b20d55e0c087b2082b86fb3bbf by deferring checking if a broken library is actually a system framework to just before we add it to `@broken_dylibs`. To see how this changes the behaviour of `brew linkage`, here's an example with this change: ❯ brew linkage neovim System libraries: /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium) and without this change: ❯ brew linkage neovim System libraries: /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium)
2022-07-18 11:57:21 +08:00
[
"/usr/lib/libgcc_s_ppc64.1.dylib",
"/opt/local/lib/libgcc/libgcc_s.1.dylib",
# TODO: Report linkage with `/usr/lib/libc++.1.dylib` when this link is broken.
"#{HOMEBREW_PREFIX}/opt/llvm/lib/libc++.1.dylib",
].include?(dylib)
linkage_checker: report linkage with system frameworks Currently, `brew linkage` reports linkage with system frameworks only if they can be found on the file system. This results in this linkage not being reported on Big Sur and newer, where system libraries are stored in the dyld cache instead. Let's fix that by avoiding silently ignoring system frameworks by moving them out of `#harmless_broken_link?`. We retain the behaviour desired from 7228e60da51392b20d55e0c087b2082b86fb3bbf by deferring checking if a broken library is actually a system framework to just before we add it to `@broken_dylibs`. To see how this changes the behaviour of `brew linkage`, here's an example with this change: ❯ brew linkage neovim System libraries: /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium) and without this change: ❯ brew linkage neovim System libraries: /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium)
2022-07-18 11:57:21 +08:00
end
linkage_checker: report linkage with system frameworks Currently, `brew linkage` reports linkage with system frameworks only if they can be found on the file system. This results in this linkage not being reported on Big Sur and newer, where system libraries are stored in the dyld cache instead. Let's fix that by avoiding silently ignoring system frameworks by moving them out of `#harmless_broken_link?`. We retain the behaviour desired from 7228e60da51392b20d55e0c087b2082b86fb3bbf by deferring checking if a broken library is actually a system framework to just before we add it to `@broken_dylibs`. To see how this changes the behaviour of `brew linkage`, here's an example with this change: ❯ brew linkage neovim System libraries: /System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium) and without this change: ❯ brew linkage neovim System libraries: /usr/lib/libSystem.B.dylib /usr/lib/libiconv.2.dylib /usr/lib/libutil.dylib Homebrew libraries: /usr/local/opt/gettext/lib/libintl.8.dylib (gettext) /usr/local/opt/libtermkey/lib/libtermkey.1.dylib (libtermkey) /usr/local/opt/libuv/lib/libuv.1.dylib (libuv) /usr/local/opt/luajit/lib/libluajit-5.1.2.dylib (luajit) /usr/local/opt/luv/lib/libluv.1.dylib (luv) /usr/local/opt/msgpack/lib/libmsgpackc.2.dylib (msgpack) /usr/local/opt/tree-sitter/lib/libtree-sitter.0.dylib (tree-sitter) /usr/local/opt/unibilium/lib/libunibilium.4.dylib (unibilium)
2022-07-18 11:57:21 +08:00
def system_framework?(dylib)
dylib.start_with?("/System/Library/Frameworks/")
end
2016-07-07 20:41:14 +08:00
# Display a list of things.
# Things may either be an array, or a hash of (label -> array).
def display_items(label, things, puts_output: true)
2016-07-07 20:41:14 +08:00
return if things.empty?
2018-09-17 02:45:00 +02:00
output = "#{label}:"
2016-07-07 20:41:14 +08:00
if things.is_a? Hash
things.keys.sort.each do |list_label|
things[list_label].sort.each do |item|
output += "\n #{item} (#{list_label})"
2016-07-07 20:41:14 +08:00
end
end
else
things.sort.each do |item|
2020-07-21 19:56:53 +00:00
output += if item.is_a? Regexp
"\n #{item.inspect}"
2020-07-21 19:56:53 +00:00
else
"\n #{item}"
end
2016-07-07 20:41:14 +08:00
end
end
puts output if puts_output
output
2016-07-07 20:41:14 +08:00
end
def resolve_formula(keg)
2016-07-15 17:45:21 +08:00
Formulary.from_keg(keg)
rescue FormulaUnavailableError
opoo "Formula unavailable: #{keg.name}"
end
2016-07-07 20:41:14 +08:00
end
require "extend/os/linkage_checker"