diff --git a/cipd_packages/ruby/third_party/ruby_ship/auto_relink_dylibs.rb b/cipd_packages/ruby/third_party/ruby_ship/auto_relink_dylibs.rb index a27243245f..5866b34034 100644 --- a/cipd_packages/ruby/third_party/ruby_ship/auto_relink_dylibs.rb +++ b/cipd_packages/ruby/third_party/ruby_ship/auto_relink_dylibs.rb @@ -1,4 +1,5 @@ require "fileutils" +require "shellwords" @@ -7,7 +8,54 @@ FileUtils.mkdir_p(@new_dylib_path) -def fix_dylib_for_file(file) +# Extracts the LC_RPATH entries from `otool -l` output. +def parse_rpaths(otool_l_output) + otool_l_output.scan(/cmd\s+LC_RPATH\s+cmdsize\s+\d+\s+path\s+(.+?)\s+\(offset\s+\d+\)/).flatten +end + +# Returns the LC_RPATH entries of a Mach-O file. +def rpaths_for_file(file) + parse_rpaths(`otool -l #{Shellwords.escape(file)} 2> /dev/null`) +end + +# Resolves a dependency reported by `otool -L` to a real file on disk. +# +# Absolute paths are returned unchanged. Homebrew bottles (e.g. brotli 1.2.0) +# now reference sibling libraries as `@rpath/libfoo.dylib` (or `@loader_path/`, +# `@executable_path/`), which is not a filesystem path and cannot be copied +# directly. For those, look for the library next to the referencing library's +# original location (source_dir), then in each of its LC_RPATH entries, then +# among the dylibs that were already copied. Returns nil if nothing matches. +# +# `@loader_path` is expanded relative to source_dir. `@executable_path` depends +# on whichever executable ends up loading the library, which is not known at +# packaging time, so those references are only matched by basename. +# +# See https://github.com/flutter/flutter/issues/164665 and +# https://ci.chromium.org/b/8669644619614462833. +def resolve_dylib_path(libfile, source_dir, rpaths) + return libfile unless libfile.start_with?("@") + + basename = File.basename(libfile) + candidates = [] + if libfile.start_with?("@loader_path/") + candidates << File.expand_path(libfile.sub(/\A@loader_path/, source_dir)) + end + candidates << File.join(source_dir, basename) + rpaths.each do |rpath| + next if rpath.start_with?("@executable_path") + rpath = File.expand_path(rpath.sub(/\A@loader_path/, source_dir)) + candidates << File.join(rpath, basename) + end + candidates << File.join(@new_dylib_path, basename) + + candidates.find { |candidate| File.file?(candidate) } +end + +# `source_dir` is the directory the file was originally copied from. It is used +# to resolve `@rpath`-style dependencies, since a copied dylib no longer sits +# next to its siblings. +def fix_dylib_for_file(file, source_dir = File.dirname(File.expand_path(file))) results = `otool -L "#{file}"` #Will get information about which dylibs to link if results.is_a?(String) && results != "" && !results.include?("is not an object file") && !results.include?("Assertion failed:") @@ -25,6 +73,7 @@ def fix_dylib_for_file(file) itterate = lines[1..-1] itterate = [] unless itterate + rpaths = nil itterate.each do |libfile_line| libfile = libfile_line.split(" (compatibility version")[0].strip @@ -34,14 +83,22 @@ def fix_dylib_for_file(file) next if libfile.include?("/usr/lib/") # These are global and assumed to be present on all versions of osx next if libfile.include?("/System/Library/") # Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation - new_libfile = File.join(@new_dylib_path, File.split(libfile)[-1]) + rpaths ||= rpaths_for_file(file) if libfile.start_with?("@") + source_libfile = resolve_dylib_path(libfile, source_dir, rpaths || []) + if source_libfile.nil? + puts "ERROR: could not resolve #{libfile} referenced by #{file} (source dir: #{source_dir}, rpaths: #{rpaths.inspect})" + exit 1 + end + + new_libfile = File.join(@new_dylib_path, File.split(source_libfile)[-1]) unless File.file?(new_libfile) - FileUtils.copy(libfile, new_libfile) + FileUtils.copy(source_libfile, new_libfile) puts "--COPIED-- #{new_libfile}" - fix_dylib_for_file(new_libfile) + fix_dylib_for_file(new_libfile, File.dirname(source_libfile)) end + # `libfile` must be the exact string from the load command (e.g. `@rpath/libfoo.dylib`). relink_command_results = `install_name_tool -change #{libfile} #{new_libfile} #{file} 2> /dev/null` # Will relink external library puts "Linked: #{new_libfile} to #{file}" diff --git a/cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh b/cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh index 439bbcd908..1a69e1a064 100644 --- a/cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh +++ b/cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh @@ -51,7 +51,7 @@ echo "############################" cd $DIR/../cleanup/extracted_ruby/$RUBYDIR if [[ "$OS" == "darwin" ]]; then OPTS="" - OPTS+="$(brew --prefix openssl)" + OPTS+="$(brew --prefix openssl@3)" OPTS+=":$(brew --prefix readline)" OPTS+=":$(brew --prefix libyaml)" OPTS+=":$(brew --prefix gdbm)" @@ -71,7 +71,7 @@ find $(brew --prefix gdbm)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bi find $(brew --prefix libffi)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; find $(brew --prefix libyaml)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; find $(brew --prefix openldap)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; -find $(brew --prefix openssl)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; +find $(brew --prefix openssl@3)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; find $(brew --prefix readline)/lib/ -name '*.dylib' -exec cp {} -f "$DIR/../build/bin/darwin_ruby/dylibs/" \; # Setting up reference directories. @@ -239,12 +239,17 @@ codesign --force -s - $DIR/../build/bin/darwin_ruby/bin/ruby remove_dylib_signatures "$DIR/../build/bin/darwin_ruby/dylibs" # Install bundler $DIR/../build/bin/gem cleanup bundler -$DIR/../build/bin/gem install -f bundler +$DIR/../build/bin/gem install -f bundler -v '~> 2.5.0' # bundler >= 2.7 requires Ruby >= 3.2. remove_dylib_signatures "$DIR/../build/bin/darwin_ruby/dylibs" # Install cococoapods +$DIR/../build/bin/gem install concurrent-ruby -v 1.3.4 # 1.3.5+ dropped the implicit require "logger" that activesupport 7.0 relies on. $DIR/../build/bin/gem install activesupport -v 7.0.8 # Pin this dep version. $DIR/../build/bin/gem install cocoapods -v $COCOAPODS_VERSION +# RubyGems 3.3 (bundled with Ruby 3.1) still upgrades already-satisfied dependencies to the newest +# release, so activesupport/cocoapods pull in a second, newer concurrent-ruby that would be the one +# activated at runtime. Remove anything other than the pinned 1.3.4 (no-op if nothing else is installed). +$DIR/../build/bin/gem uninstall concurrent-ruby -a -x -I -v '> 1.3.4' remove_dylib_signatures "$DIR/../build/bin/darwin_ruby/dylibs" # Cleanup temp folder.