Merge pull request #23369 from Homebrew/change-dylib-id-dsl

Add explicit dylib ID helpers
This commit is contained in:
Mike McQuaid
2026-07-31 07:41:22 +00:00
committed by GitHub
10 changed files with 152 additions and 9 deletions
+23
View File
@@ -2325,6 +2325,29 @@ class Formula
end
end
# Changes the dynamic library ID of one Mach-O file and codesigns it on
# Apple Silicon. The source and new ID are both explicit. Set
# `resolve_source: true` to edit the target of a source symlink.
#
# ### Examples
#
# ```ruby
# change_dylib_id lib/"libfoo.dylib", opt_lib/"libfoo.dylib"
# change_dylib_id lib/"libfoo.dylib", "@rpath/libfoo.1.dylib", resolve_source: true
# ```
#
# @api public
sig {
params(
file: Pathname,
id: T.any(String, Pathname),
resolve_source: T::Boolean,
).void
}
def change_dylib_id(file, id, resolve_source: false)
Homebrew::InstallSteps.change_dylib_id(file, id, resolve_source:)
end
# Replaces a universal binary with its native slice.
#
# If called with no parameters, does this with all compatible
+32
View File
@@ -23,6 +23,17 @@ module Homebrew
SystemCommandArg = T.type_alias { T.any(String, Pathname) }
TemplateTokenValue = T.type_alias { T.any(String, Pathname) }
sig { params(file: Pathname, id: T.any(String, Pathname), resolve_source: T::Boolean).void }
def self.change_dylib_id(file, id, resolve_source: false)
file = file.realpath if resolve_source
require "macho"
file.ensure_writable do
MachO::Tools.change_dylib_id file, id.to_s
MachO.codesign! file if Hardware::CPU.arm?
end
end
class DSL
((instance_methods + private_instance_methods) -
(BasicObject.instance_methods + BasicObject.private_instance_methods) -
@@ -540,6 +551,21 @@ module Homebrew
"non_recursive" => !recursive)
end
sig {
params(
source: ::T.any(::String, ::Pathname),
id: ::T.any(::String, ::Pathname),
base: ::T.nilable(::T.any(::String, ::Symbol)),
resolve_source: ::T::Boolean,
).void
}
def change_dylib_id(source, id, base: nil, resolve_source: false)
add_step("change_dylib_id",
"source" => path_spec(source, base:, default_base: @default_source_base),
"id" => id.to_s,
"resolve_source" => resolve_source)
end
sig {
params(
command: ::T.any(::String, ::Pathname),
@@ -872,6 +898,12 @@ module Homebrew
run_serialised_command(step)
when "terminate_process"
run_terminate_process(step)
when "change_dylib_id"
Homebrew::InstallSteps.change_dylib_id(
resolve_path(step_path(step, "source")),
expand_template_tokens(step_string(step, "id")),
resolve_source: step["resolve_source"] == true,
)
when "warn"
opoo expand_template_tokens(step_string(step, "message"))
when "configure_gcc_runtime"
@@ -33,6 +33,7 @@ module RuboCop
"depends_on" => "formula.rb",
"deprecated_option" => "formula.rb",
"desc" => "formula.rb",
"change_dylib_id" => "formula.rb",
"env_script_all_files" => "extend/pathname.rb",
"fails_with" => "formula.rb",
"post_install_steps" => "formula.rb",
@@ -16,6 +16,7 @@ module RuboCop
KEYCHAIN_STEP_METHODS = [:delete_keychain_certificate].freeze
PERMISSION_STEP_METHODS = [:set_permissions, :set_ownership].freeze
COMMAND_STEP_METHODS = [:run, :terminate_process].freeze
MACHO_STEP_METHODS = [:change_dylib_id].freeze
NOTICE_STEP_METHODS = [:warn].freeze
FORMULA_ACTION_STEP_METHODS =
[:configure_gcc_runtime, :install_gzipped_executable, :configure_glibc_runtime,
@@ -24,12 +25,12 @@ module RuboCop
ALLOWED_STEP_METHODS = T.let(
[*FILE_PREPARATION_STEP_METHODS, *LINK_STEP_METHODS, *CONFIG_WRITE_STEP_METHODS, *SERVICE_DATA_STEP_METHODS,
*REBUILD_ACTION_STEP_METHODS, :set_permissions, *COMMAND_STEP_METHODS, *NOTICE_STEP_METHODS,
*FORMULA_ACTION_STEP_METHODS, *STEP_SCOPE_METHODS].freeze,
*MACHO_STEP_METHODS, *FORMULA_ACTION_STEP_METHODS, *STEP_SCOPE_METHODS].freeze,
T::Array[Symbol],
)
CASK_ALLOWED_STEP_METHODS = T.let(
[*FILE_PREPARATION_STEP_METHODS, *CONFIG_WRITE_STEP_METHODS, *KEYCHAIN_STEP_METHODS,
*PERMISSION_STEP_METHODS, *COMMAND_STEP_METHODS, *STEP_SCOPE_METHODS].freeze,
*PERMISSION_STEP_METHODS, *COMMAND_STEP_METHODS, *MACHO_STEP_METHODS, *STEP_SCOPE_METHODS].freeze,
T::Array[Symbol],
)
@@ -3,6 +3,7 @@
require "install_steps"
require "cask/quarantine"
require "macho"
RSpec.describe Homebrew::InstallSteps do
let(:root) { Pathname(TEST_TMPDIR)/"install-steps" }
@@ -31,6 +32,22 @@ RSpec.describe Homebrew::InstallSteps do
end
end
specify "changes the resolved dylib ID and restores its mode" do
dylib = root/"lib/libfoo.1.dylib"
source = root/"lib/libfoo.dylib"
dylib.dirname.mkpath
dylib.write "Mach-O"
dylib.chmod 0444
FileUtils.ln_s dylib, source
allow(Hardware::CPU).to receive(:arm?).and_return(true)
expect(MachO::Tools).to receive(:change_dylib_id).with(dylib, "@rpath/libfoo.1.dylib")
expect(MachO).to receive(:codesign!).with(dylib)
described_class.change_dylib_id source, "@rpath/libfoo.1.dylib", resolve_source: true
expect(dylib.stat.mode & 0777).to eq(0444)
end
specify "runs mkdir, touch, move and symlink steps", :aggregate_failures do
steps = Homebrew::InstallSteps::DSL.build(default_base: :var, default_source_base: :staged_path,
default_target_base: :staged_path) do
@@ -52,6 +69,20 @@ RSpec.describe Homebrew::InstallSteps do
expect((root/"stage/linked-target").readlink).to eq(Pathname("move-target"))
end
specify "changes an explicit Mach-O dylib ID" do
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :prefix) do
on_macos do
change_dylib_id "lib/libfoo.dylib", "{{HOMEBREW_PREFIX}}/opt/foo/lib/libfoo.1.dylib",
resolve_source: true
end
end
allow(Homebrew::SimulateSystem).to receive(:simulating_or_running_on_macos?).and_return(true)
expect(described_class).to receive(:change_dylib_id)
.with(root/"prefix/lib/libfoo.dylib", "#{HOMEBREW_PREFIX}/opt/foo/lib/libfoo.1.dylib", resolve_source: true)
Homebrew::InstallSteps::Runner.new(context:).run(steps)
end
specify "links every source matched by a glob into a directory", :aggregate_failures do
steps = Homebrew::InstallSteps::DSL.build(default_source_base: :prefix,
default_target_base: :prefix) do
@@ -5,6 +5,41 @@ require "test/support/fixtures/testball"
require "formula"
RSpec.describe Formula do
describe "#change_dylib_id" do
subject(:f) do
formula "dylib-id-test" do
url "foo-1.0"
end
end
let(:dylib) { f.lib/"libfoo.1.dylib" }
before do
dylib.dirname.mkpath
FileUtils.touch dylib
end
after { f.prefix.rmtree }
it "uses the explicit source and dylib ID" do
unversioned_dylib = f.lib/"libfoo.dylib"
FileUtils.ln_s dylib, unversioned_dylib
expect(Homebrew::InstallSteps).to receive(:change_dylib_id)
.with(unversioned_dylib, f.opt_lib/"libfoo.dylib", resolve_source: false)
f.change_dylib_id unversioned_dylib, f.opt_lib/"libfoo.dylib"
end
it "can resolve the source symlink and codesigns on ARM" do
unversioned_dylib = f.lib/"libfoo.dylib"
FileUtils.ln_s dylib, unversioned_dylib
expect(Homebrew::InstallSteps).to receive(:change_dylib_id)
.with(unversioned_dylib, "@rpath/libfoo.dylib", resolve_source: true)
f.change_dylib_id unversioned_dylib, "@rpath/libfoo.dylib", resolve_source: true
end
end
describe "#uses_from_macos" do
before do
allow(OS).to receive(:mac?).and_return(true)
@@ -30,7 +30,7 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do
preflight_steps do
system "true"
^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `change_dylib_id`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
CASK
@@ -44,7 +44,7 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do
preflight_steps do
update_desktop_database
^^^^^^^^^^^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `change_dylib_id`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
CASK
@@ -70,6 +70,7 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do
set_ownership "Foo.app", user: "root", group: "wheel"
run "foo", args: ["--repair"]
terminate_process "foo", attempts: 3
change_dylib_id "Foo.app/Contents/Frameworks/libfoo.dylib", "@rpath/libfoo.dylib"
delete_keychain_certificate "Charles"
delete_keychain_certificate "NodeMITMProxyCA", matching_certificate: "~/Library/Application Support/betwixt/ssl/certs/ca.pem"
on_macos do
@@ -95,7 +96,7 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do
preflight_steps do
touch "#{appdir}/state"
^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `change_dylib_id`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
CASK
@@ -110,7 +111,7 @@ RSpec.describe RuboCop::Cop::Cask::InstallSteps, :config do
preflight_steps do
on_macos do
system "true"
^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `write`, `delete_keychain_certificate`, `set_permissions`, `set_ownership`, `run`, `terminate_process`, `change_dylib_id`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
end
@@ -41,7 +41,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
system "true"
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
RUBY
@@ -66,6 +66,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
set_permissions "foo", "0755"
run "foo", args: ["--repair"]
terminate_process "foo", attempts: 3
change_dylib_id "lib/libfoo.dylib", "{{opt_prefix}}/lib/libfoo.1.dylib", resolve_source: true
warn "foo exists"
configure_gcc_runtime
install_gzipped_executable "compressed.gz", "bin/executable"
@@ -106,7 +107,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
on_macos do
system "true"
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
end
@@ -120,7 +121,7 @@ RSpec.describe RuboCop::Cop::FormulaAudit::InstallSteps do
post_install_steps do
write "foo.conf", "prefix = #{prefix}"
^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
^^^^^^^^^ FormulaAudit/InstallSteps: Steps blocks may only contain install step DSL calls: `mkdir`, `mkdir_p`, `touch`, `move`, `mv`, `move_children`, `move_contents`, `copy`, `remove`, `inreplace`, `symlink`, `ln_s`, `ln_sf`, `link_dir`, `link_children`, `write`, `init_data_dir`, `compile_gsettings_schemas`, `gio_querymodules`, `gdk_pixbuf_query_loaders`, `gtk_update_icon_cache`, `update_mime_database`, `update_desktop_database`, `set_permissions`, `run`, `terminate_process`, `warn`, `change_dylib_id`, `configure_gcc_runtime`, `install_gzipped_executable`, `configure_glibc_runtime`, `configure_clang_system`, `if_path_exists`, `unless_path_exists`, `on_macos`, `on_linux`.
end
end
RUBY
+1
View File
@@ -665,6 +665,7 @@ Relative paths default to `staged_path` for `base:`, `source_base:` and `target_
* `set_ownership`: recursively change existing path ownership with `sudo chown`; example: `set_ownership "Shared/payload", user: "root", group: "wheel"`. Missing paths are ignored. When `user:` is omitted, the current user is used. When `group:` is omitted, `staff` is used.
* `run`: run one executable with literal arguments; example: `run "Example.app/Contents/MacOS/helper", args: ["--repair"], base: :appdir`.
* `terminate_process`: terminate a process by name; example: `terminate_process "Example", attempts: 3, must_succeed: false`. `attempts:` sets the total number of attempts and defaults to one. The step also supports `match: :full`, `notices:` shown before the first attempt and a `failure_message:` warning.
* `change_dylib_id`: change one Mach-O dynamic library ID; pass the complete source and new ID, use `resolve_source: true` for a source symlink and wrap the step in `on_macos`.
Use `if_path_exists` and `unless_path_exists` blocks to guard one or more steps by a path, and `on_macos` and `on_linux` blocks for platform-specific steps. Each guard is evaluated once for its whole block. `copy`, `move` and symlink steps accept `source_glob: true`; path collections used by `remove`, `set_permissions` and `set_ownership` expand globs automatically. Symlink removal can additionally match the serialised source during uninstall, while `remove` can restrict removal with `symlink_target_contains:` or `content_contains:`.
+17
View File
@@ -250,6 +250,22 @@ $ otool -L /opt/homebrew/bin/ldapvi
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1213.0.0)
```
### Changing a dynamic library ID
Use [`change_dylib_id`](/rubydoc/Formula.html#change_dylib_id-instance_method)
inside `install` or `post_install` when one Mach-O dynamic library needs a
non-standard ID. Both the source and its complete new ID are explicit. Set
`resolve_source: true` when the source is a symlink and its target should be
edited. For example:
```ruby
change_dylib_id lib/"libfoo.dylib", opt_lib/"libfoo.dylib"
change_dylib_id lib/"libfoo.dylib", "@rpath/libfoo.1.dylib", resolve_source: true
```
The helper preserves the file's permissions and codesigns the modified library
on Apple Silicon.
### Specifying macOS components as dependencies
If a formula dependency is required on all platforms but can be handled by a component that ships with macOS, specify it with [`uses_from_macos`](/rubydoc/Formula.html#uses_from_macos-class_method). On Linux it acts like [`depends_on`](/rubydoc/Formula.html#depends_on-class_method), while on macOS it's ignored unless the host system is older than the optional `since:` parameter.
@@ -1090,6 +1106,7 @@ represented by structured steps.
* `ln_s`: alias for `symlink`; example: `ln_s "cert.pem", "foo/cert.pem", source_base: :relative`.
* `ln_sf`: create or replace a symlink; example: `ln_sf "cert.pem", "foo/cert.pem", source_base: :relative`.
* `set_permissions`: change existing path permissions; example: `set_permissions "foo", "0755"`.
* `change_dylib_id`: change one Mach-O dynamic library ID; pass the complete source and new ID, use `resolve_source: true` for a source symlink and wrap the step in `on_macos`.
Use `if_path_exists` and `unless_path_exists` blocks to guard one or more steps by a path, and `on_macos` and `on_linux` blocks for platform-specific steps. Each guard is evaluated once for its whole block. `copy`, `move` and symlink steps accept `source_glob: true`; path collections used by `remove` and `set_permissions` expand globs automatically. Removals may additionally be restricted with `symlink_target_contains:` or `content_contains:`.