From f71a570430ae4a9bcbda86ee8fa0598b56516495 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 28 Sep 2026 20:26:18 +0000 Subject: [PATCH 1/6] [ruby] Resolve @rpath/@loader_path dylib references when relinking The `Mac_arm64 ruby` CIPD builder fails in the vendored ruby_ship packaging step (auto_relink_dylibs.rb) after Ruby has been compiled: fileutils.rb:1394:in 'initialize': No such file or directory @ rb_sysopen - @rpath/libbrotlicommon.1.dylib (Errno::ENOENT) from auto_relink_dylibs.rb:40 (FileUtils.copy) The script walks `otool -L` output and copies every dependency into bin/darwin_ruby/dylibs using the literal string reported by otool. Homebrew's brotli 1.2.0 bottle (pulled in by curl) references its sibling library as `@rpath/libbrotlicommon.1.dylib`, which is a dyld search token rather than a filesystem path, so the copy fails. Because the script recurses into the *copied* dylib, it also no longer knows where the original lived and could not find the sibling even if it tried. Fix: - Thread the original source directory through the recursion. - Before copying an `@rpath/`, `@loader_path/` or `@executable_path/` dependency, resolve it to a real file by trying, in order: the referencing library's original directory, each LC_RPATH entry from `otool -l` (with `@loader_path` expanded against the original directory), and finally the dylibs directory itself if the library was already copied. - If none of those exist, print which reference could not be resolved and exit 1 instead of crashing with an opaque ENOENT. - `install_name_tool -change` still receives the exact original string from the load command as its `old` argument, so the rewrite matches. Absolute paths behave exactly as before, and `/usr/lib/` and `/System/Library/` entries are still skipped. Note that this script is vendored from stephan-nordnes-eriksen/ruby_ship (third_party/ruby_ship); this is the first local change to the .rb file (ruby_ship_build.sh was already modified locally in #3186). Failing build: https://ci.chromium.org/b/8669644619614462833 Related: https://github.com/flutter/flutter/issues/164665 --- .../ruby_ship/auto_relink_dylibs.rb | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) 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..92896061c5 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 @@ -7,7 +7,49 @@ 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 LC_RPATH\n\s*cmdsize \d+\n\s*path (.+?) \(offset \d+\)/).flatten +end + +# Returns the LC_RPATH entries of a Mach-O file. +def rpaths_for_file(file) + parse_rpaths(`otool -l "#{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. +# +# 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.split(libfile)[-1] + candidates = [] + if libfile.start_with?("@loader_path/", "@executable_path/") + candidates << File.expand_path(libfile.sub(/\A@(loader_path|executable_path)/, source_dir)) + end + candidates << File.join(source_dir, basename) + rpaths.each do |rpath| + rpath = File.expand_path(rpath.sub(/\A@(loader_path|executable_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 +67,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 +77,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}" From e090be5852dca99eb88f4ef06385c69c474d6555 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 28 Sep 2026 20:42:42 +0000 Subject: [PATCH 2/6] [ruby] Use `brew --prefix openssl@3` explicitly On 2026-09-27 homebrew-core moved the `openssl` alias from openssl@3 to openssl@4 (Homebrew/homebrew-core 9a12b94a). build.sh installs `openssl@3`, but ruby_ship_build.sh resolved `brew --prefix openssl`, which now returns /opt/homebrew/opt/openssl@4 -- a directory that does not exist on the bots. The resulting `-L/opt/homebrew/opt/openssl@4/lib` makes ld emit "directory not found" warnings, Ruby 3.1's configure treats any warning under -Werror as a failed probe (e.g. "function name string predefined identifier... no"), and the build then dies with `use of undeclared identifier 'RUBY_FUNCTION_NAME_STRING'`. Seen on led build https://ci.chromium.org/b/8669384638160948033; the previous prod run (2 days earlier) still resolved the alias to openssl@3. Pin the prefix to openssl@3 in both places it is used, matching what build.sh installs and the openssl@3 cert.pem path it already copies. Related: https://github.com/flutter/flutter/issues/164665 --- cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..2af257c0fb 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. From 7fb1c65f21a44ad38578bdb7d2a9d4efba5ee0b0 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 28 Sep 2026 22:37:57 +0000 Subject: [PATCH 3/6] [ruby] Pin bundler to 2.5.x for Ruby 3.1 bundler 4.0.21 (released 2026-09-16) requires Ruby >= 3.2 and RubyGems >= 3.4.1. `gem install -f bundler` skips that check, so it was force- installed onto the Ruby 3.1.3 build, and the `bin/bundler --version` smoke test in tools/build.sh crashed inside the RubyGems resolver: rubygems/resolver/conflict.rb:47:in `conflicting_dependencies': undefined method `request' for nil:NilClass (NoMethodError) Seen on led build https://ci.chromium.org/b/8669383997159272673, right after the dylib relink and gem installs had all succeeded. Pin to `~> 2.5.0`, which resolves to bundler 2.5.23 (Ruby >= 3.0, RubyGems >= 3.2.3; Ruby 3.1.3 ships RubyGems 3.3.26). Note that a bare `~> 2.5` would resolve to 2.7.2, which already requires Ruby >= 3.2, so the patch-level constraint is deliberate. Related: https://github.com/flutter/flutter/issues/164665 --- cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2af257c0fb..949c4cb838 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 @@ -239,7 +239,7 @@ 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 From feb4db816d4ff4e5b4451be96f2a1782888bb83a Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Mon, 28 Sep 2026 22:39:14 +0000 Subject: [PATCH 4/6] [ruby] Address review comments on auto_relink_dylibs.rb - Only expand `@loader_path/` references (and LC_RPATH entries) relative to the referencing library's original directory. `@executable_path` depends on whichever executable loads the library, which is not known when packaging a Homebrew dylib, so such references are matched by basename only (original directory, LC_RPATH dirs, already-copied dylibs) and `@executable_path` LC_RPATH entries are skipped. - Make the `otool -l` LC_RPATH regex tolerant of arbitrary whitespace. - Shell-escape the path passed to `otool -l`. - Use File.basename. --- .../ruby_ship/auto_relink_dylibs.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 92896061c5..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" @@ -9,12 +10,12 @@ # Extracts the LC_RPATH entries from `otool -l` output. def parse_rpaths(otool_l_output) - otool_l_output.scan(/cmd LC_RPATH\n\s*cmdsize \d+\n\s*path (.+?) \(offset \d+\)/).flatten + 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 "#{file}" 2> /dev/null`) + parse_rpaths(`otool -l #{Shellwords.escape(file)} 2> /dev/null`) end # Resolves a dependency reported by `otool -L` to a real file on disk. @@ -26,19 +27,24 @@ def rpaths_for_file(file) # 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.split(libfile)[-1] + basename = File.basename(libfile) candidates = [] - if libfile.start_with?("@loader_path/", "@executable_path/") - candidates << File.expand_path(libfile.sub(/\A@(loader_path|executable_path)/, source_dir)) + 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| - rpath = File.expand_path(rpath.sub(/\A@(loader_path|executable_path)/, source_dir)) + 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) From cde7b6019de3239a8b3c5861081c8192e8c4d07e Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Tue, 29 Sep 2026 01:12:59 +0000 Subject: [PATCH 5/6] [ruby] Pin concurrent-ruby to 1.3.4 so cocoapods loads on Ruby 3.1 `gem install activesupport -v 7.0.8` pulls in the newest concurrent-ruby (1.3.8 today). concurrent-ruby 1.3.5 (2025-01-15) removed its implicit `require "logger"`, which activesupport 7.0 silently relied on, so the `bin/pod --version` smoke test fails with: activesupport-7.0.8/lib/active_support/logger_thread_safe_level.rb:12: uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError) Seen on led build https://ci.chromium.org/b/8669375943591764321, after the relink, openssl@3 and bundler fixes had taken the build all the way to the last smoke test. The package is built into a fresh GEM_HOME, so installing concurrent-ruby 1.3.4 first means RubyGems keeps it when resolving activesupport (which requires `~> 1.0, >= 1.0.2`; cocoapods-core wants `~> 1.1`, i18n and tzinfo `~> 1.0`, all satisfied by 1.3.4), and `pod` activates 1.3.4 at runtime instead of 1.3.8. Related: https://github.com/flutter/flutter/issues/162341 --- cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh | 1 + 1 file changed, 1 insertion(+) 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 949c4cb838..4b0810158f 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 @@ -243,6 +243,7 @@ $DIR/../build/bin/gem install -f bundler -v '~> 2.5.0' # bundler >= 2.7 requires 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 remove_dylib_signatures "$DIR/../build/bin/darwin_ruby/dylibs" From 39e4de31e2daecac2531b9119e555e522ddf7371 Mon Sep 17 00:00:00 2001 From: Elijah Okoroh Date: Tue, 29 Sep 2026 01:35:38 +0000 Subject: [PATCH 6/6] [ruby] Remove newer concurrent-ruby after installing cocoapods Installing concurrent-ruby 1.3.4 first was not enough on its own: the RubyGems bundled with Ruby 3.1.3 (3.3.26) still resolves activesupport's `concurrent-ruby ~> 1.0` to the newest release and installs 1.3.8 next to 1.3.4, and at runtime `pod` activates the newest installed version, so `bin/pod --version` kept failing with uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (led build https://ci.chromium.org/b/8669366981838817409, which shows both `Successfully installed concurrent-ruby-1.3.4` and, during the activesupport install, `Successfully installed concurrent-ruby-1.3.8`). Newer RubyGems (3.6) keeps the already-installed 1.3.4, which is why a local dry run did not show the problem. After the cocoapods install, uninstall every concurrent-ruby newer than 1.3.4 (`-a` all matching versions, `-x` executables, `-I` skip the dependency prompt). `gem uninstall` exits 0 with "is not installed" when nothing matches, so this is a no-op on RubyGems versions that already keep 1.3.4. Related: https://github.com/flutter/flutter/issues/162341 --- cipd_packages/ruby/third_party/ruby_ship/ruby_ship_build.sh | 4 ++++ 1 file changed, 4 insertions(+) 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 4b0810158f..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 @@ -246,6 +246,10 @@ remove_dylib_signatures "$DIR/../build/bin/darwin_ruby/dylibs" $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.