From 3368bc386cb1f40c8fffc642c50899fc9fd96c36 Mon Sep 17 00:00:00 2001 From: Mike McQuaid Date: Thu, 23 Jan 2025 15:42:07 +0000 Subject: [PATCH] commands/exec: allow customising environment. Provide `HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS` and `HOMEBREW_BUNDLE_EXEC_FORMULA_VERSION_*` variables to allow adjusting the environment variables that are set in `brew bundle exec`. --- lib/bundle/commands/exec.rb | 31 +++++++- spec/bundle/commands/exec_command_spec.rb | 96 +++++++++++------------ 2 files changed, 77 insertions(+), 50 deletions(-) diff --git a/lib/bundle/commands/exec.rb b/lib/bundle/commands/exec.rb index a7c695be6..ddbb816d6 100644 --- a/lib/bundle/commands/exec.rb +++ b/lib/bundle/commands/exec.rb @@ -35,7 +35,15 @@ def run(*args, global: false, file: nil) Formulary.factory(entry.name) end - ENV.keg_only_deps = ENV.deps.select(&:keg_only?) + + # Allow setting all dependencies to be keg-only + # (i.e. should be explicitly in HOMEBREW_*PATHs ahead of HOMEBREW_PREFIX) + ENV.keg_only_deps = if ENV["HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS"].present? + ENV.delete("HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS") + ENV.deps + else + ENV.deps.select(&:keg_only?) + end ENV.setup_build_environment # Enable compiler flag filtering @@ -58,6 +66,27 @@ def run(*args, global: false, file: nil) # Ensure the Ruby path we saved goes before anything else, if the command was in the PATH ENV.prepend_path "PATH", command_path if command_path.present? + # Replace the formula versions from the environment variables + formula_versions = {} + ENV.each do |key, value| + match = key.match(/^HOMEBREW_BUNDLE_EXEC_FORMULA_VERSION_(.+)$/) + next if match.blank? + + formula_name = match[1] + next if formula_name.blank? + + ENV.delete(key) + formula_versions[formula_name.downcase] = value + end + formula_versions.each do |formula_name, formula_version| + ENV.each do |key, value| + opt = %r{/opt/#{formula_name}([/:$])} + next unless value.match(opt) + + ENV[key] = value.gsub(opt, "/Cellar/#{formula_name}/#{formula_version}\\1") + end + end + exec(*args) end end diff --git a/spec/bundle/commands/exec_command_spec.rb b/spec/bundle/commands/exec_command_spec.rb index 7faba8279..9e15980c0 100644 --- a/spec/bundle/commands/exec_command_spec.rb +++ b/spec/bundle/commands/exec_command_spec.rb @@ -10,35 +10,47 @@ end context "when a Brewfile is found" do - it "does not raise an error" do - allow(described_class).to receive(:exec).and_return(nil) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'openssl'") + let(:brewfile_contents) { "brew 'openssl'" } - expect { described_class.run("bundle", "install") }.not_to raise_error + before do + allow_any_instance_of(Pathname).to receive(:read) + .and_return(brewfile_contents) end - it "is able to run without bundle arguments" do - allow(described_class).to receive(:exec).with("bundle", "install").and_return(nil) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'openssl'") + context "with valid command setup" do + before do + allow(described_class).to receive(:exec).and_return(nil) + end - expect { described_class.run("bundle", "install") }.not_to raise_error - end + it "does not raise an error" do + expect { described_class.run("bundle", "install") }.not_to raise_error + end - it "raises an exception if called without a command" do - allow(described_class).to receive(:exec).and_return(nil) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'openssl'") + it "does not raise an error when HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS is set" do + ENV["HOMEBREW_BUNDLE_EXEC_ALL_KEG_ONLY_DEPS"] = "1" + expect { described_class.run("bundle", "install") }.not_to raise_error + end - expect { described_class.run }.to raise_error(RuntimeError) + it "uses the formula version from the environment variable" do + openssl_version = "1.1.1" + ENV["PATH"] = "/opt/homebrew/opt/openssl/bin" + ENV["HOMEBREW_BUNDLE_EXEC_FORMULA_VERSION_OPENSSL"] = openssl_version + described_class.run("bundle", "install") + expect(ENV.fetch("PATH")).to include("/Cellar/openssl/1.1.1/bin") + end + + it "is able to run without bundle arguments" do + allow(described_class).to receive(:exec).with("bundle", "install").and_return(nil) + expect { described_class.run("bundle", "install") }.not_to raise_error + end + + it "raises an exception if called without a command" do + expect { described_class.run }.to raise_error(RuntimeError) + end end it "raises if called with a command that's not on the PATH" do allow(described_class).to receive_messages(exec: nil, which: nil) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'openssl'") - expect { described_class.run("bundle", "install") }.to raise_error(RuntimeError) end @@ -47,49 +59,35 @@ expect(described_class).to receive(:which).and_return(Pathname("/usr/local/bin/bundle")) allow(ENV).to receive(:prepend_path).with(any_args).and_call_original expect(ENV).to receive(:prepend_path).with("PATH", "/usr/local/bin").once.and_call_original - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'openssl'") described_class.run("bundle", "install") end describe "when running a command which exists but is not on the PATH" do - it "does not raise if the command is a relative path with current directory indicator" do - allow(described_class).to receive(:exec).with("./configure").and_return(nil) - expect(described_class).not_to receive(:which) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'zlib'") - - expect { described_class.run("./configure") }.not_to raise_error + let(:brewfile_contents) { "brew 'zlib'" } + + shared_examples "allows command execution" do |command| + it "does not raise" do + allow(described_class).to receive(:exec).with(command).and_return(nil) + expect(described_class).not_to receive(:which) + expect { described_class.run(command) }.not_to raise_error + end end - it "does not raise if the command is a relative path without current directory indicator" do - allow(described_class).to receive(:exec).with("bin/install").and_return(nil) - expect(described_class).not_to receive(:which) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'zlib'") - - expect { described_class.run("bin/install") }.not_to raise_error - end - - it "does not raise if the command is an absolute path" do - allow(described_class).to receive(:exec).with("/Users/admin/Downloads/command").and_return(nil) - expect(described_class).not_to receive(:which) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'zlib'") - - expect { described_class.run("/Users/admin/Downloads/command") }.not_to raise_error - end + it_behaves_like "allows command execution", "./configure" + it_behaves_like "allows command execution", "bin/install" + it_behaves_like "allows command execution", "/Users/admin/Downloads/command" end describe "when the Brewfile contains rbenv" do - before { ENV["HOMEBREW_RBENV_ROOT"] = rbenv_root.to_s } - let(:rbenv_root) { Pathname.new("/tmp/.rbenv") } + let(:brewfile_contents) { "brew 'rbenv'" } + + before do + ENV["HOMEBREW_RBENV_ROOT"] = rbenv_root.to_s + end it "prepends the path of the rbenv shims to PATH before running" do allow(described_class).to receive(:exec).with("/usr/bin/true").and_return(0) - allow_any_instance_of(Pathname).to receive(:read) - .and_return("brew 'rbenv'") allow(ENV).to receive(:fetch).with(any_args).and_call_original allow(ENV).to receive(:prepend_path).with(any_args).once.and_call_original