Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add postinstall support for brew and cask #1555

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,13 @@ cask_args appdir: "~/Applications", require_sha: true

# 'brew install'
brew "imagemagick"
# 'brew install --with-rmtp', 'brew link --overwrite', 'brew services restart' on version changes
brew "denji/nginx/nginx-full", link: :overwrite, args: ["with-rmtp"], restart_service: :changed
# 'brew install --with-rmtp', 'brew link --overwrite', 'brew services restart' even if no install/upgrade
brew "denji/nginx/nginx-full", link: :overwrite, args: ["with-rmtp"], restart_service: :always
# 'brew install', always 'brew services restart', 'brew link', 'brew unlink mysql' (if it is installed)
brew "[email protected]", restart_service: true, link: true, conflicts_with: ["mysql"]
brew "[email protected]", restart_service: :changed, link: true, conflicts_with: ["mysql"]
# 'brew install' and run a command if installer or upgraded.
brew "postgresql@16",
postinstall: "${HOMEBREW_PREFIX}/opt/postgresql@16/bin/postgres -D ${HOMEBREW_PREFIX}/var/postgresql@16"
MikeMcQuaid marked this conversation as resolved.
Show resolved Hide resolved
# install only on specified OS
brew "gnupg" if OS.mac?
brew "glibc" if OS.linux?
Expand All @@ -55,6 +58,8 @@ cask "firefox", args: { no_quarantine: true }
cask "opera", greedy: true
# 'brew install --cask' only if '/usr/libexec/java_home --failfast' fails
cask "java" unless system "/usr/libexec/java_home", "--failfast"
# 'brew install --cask' and run a command if installer or upgraded.
cask "google-cloud-sdk", postinstall: "${HOMEBREW_PREFIX}/bin/gcloud components update"

# 'mas install'
mas "1Password", id: 443_987_910
Expand Down
2 changes: 1 addition & 1 deletion lib/bundle/brew_dumper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def dump(describe: false, no_restart: false)

args = f[:args].map { |arg| "\"#{arg}\"" }.sort.join(", ")
brewline += ", args: [#{args}]" unless f[:args].empty?
brewline += ", restart_service: true" if !no_restart && BrewServices.started?(f[:full_name])
brewline += ", restart_service: :changed" if !no_restart && BrewServices.started?(f[:full_name])
brewline += ", link: #{f[:link?]}" unless f[:link?].nil?
brewline
end.join("\n")
Expand Down
21 changes: 16 additions & 5 deletions lib/bundle/brew_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def initialize(name, options = {})
@restart_service = options[:restart_service]
@start_service = options.fetch(:start_service, @restart_service)
@link = options.fetch(:link, nil)
@postinstall = options.fetch(:postinstall, nil)
@changed = nil
end

Expand All @@ -46,12 +47,14 @@ def install(preinstall: true, no_upgrade: false, verbose: false, force: false)
result = install_result

if installed?
if install_result
service_result = service_change_state!(verbose:)
result &&= service_result
end
service_result = service_change_state!(verbose:)
result &&= service_result

link_result = link_change_state!(verbose:)
result &&= link_result

postinstall_result = postinstall_change_state!(verbose:)
result &&= postinstall_result
end

result
Expand Down Expand Up @@ -83,7 +86,7 @@ def restart_service_needed?
return false unless restart_service?

# Restart if `restart_service: :always`, or if the formula was installed or upgraded
@restart_service.to_s != "changed" || changed?
@restart_service.to_s == "always" || changed?
end

def changed?
Expand Down Expand Up @@ -132,6 +135,14 @@ def link_change_state!(verbose: false)
true
end

def postinstall_change_state!(verbose:)
return true if @postinstall.blank?
return true unless changed?

puts "Running postinstall for #{@name}." if verbose
Bundle.system(@postinstall, verbose:)
MikeMcQuaid marked this conversation as resolved.
Show resolved Hide resolved
end

def self.formula_installed_and_up_to_date?(formula, no_upgrade: false)
return false unless formula_installed?(formula)
return true if no_upgrade
Expand Down
52 changes: 35 additions & 17 deletions lib/bundle/cask_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,51 @@ def install(name, preinstall: true, no_upgrade: false, verbose: false, force: fa

full_name = options.fetch(:full_name, name)

if installed_casks.include?(name) && upgrading?(no_upgrade, name, options)
install_result = if installed_casks.include?(name) && upgrading?(no_upgrade, name, options)
status = "#{options[:greedy] ? "may not be" : "not"} up-to-date"
puts "Upgrading #{name} cask. It is installed but #{status}." if verbose
return Bundle.brew("upgrade", "--cask", full_name, verbose:)
end
Bundle.brew("upgrade", "--cask", full_name, verbose:)
else
args = options.fetch(:args, []).filter_map do |k, v|
case v
when TrueClass
"--#{k}"
when FalseClass
nil
else
"--#{k}=#{v}"
end
end

args << "--force" if force
args.uniq!

with_args = " with #{args.join(" ")}" if args.present?
puts "Installing #{name} cask#{with_args}. It is not currently installed." if verbose

args = options.fetch(:args, []).filter_map do |k, v|
case v
when TrueClass
"--#{k}"
when FalseClass
nil
if Bundle.brew("install", "--cask", full_name, *args, verbose:)
installed_casks << name
true
else
"--#{k}=#{v}"
false
end
end
result = install_result

args << "--force" if force
args.uniq!
if cask_installed?(name)
postinstall_result = postinstall_change_state!(name:, options:, verbose:)
result &&= postinstall_result
end

with_args = " with #{args.join(" ")}" if args.present?
puts "Installing #{name} cask#{with_args}. It is not currently installed." if verbose
result
end

return false unless Bundle.brew("install", "--cask", full_name, *args, verbose:)
def postinstall_change_state!(name:, options:, verbose:)
postinstall = options.fetch(:postinstall, nil)
return true if postinstall.blank?

installed_casks << name
true
puts "Running postinstall for #{name}." if verbose
Bundle.system(postinstall, verbose:)
end

def self.cask_installed_and_up_to_date?(cask, no_upgrade: false)
Expand Down
98 changes: 79 additions & 19 deletions spec/bundle/brew_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
end
end

context "with a true restart_service option" do
context "with an always restart_service option" do
before do
allow_any_instance_of(described_class).to receive(:install_change_state!).and_return(true)
allow_any_instance_of(described_class).to receive(:installed?).and_return(true)
Expand All @@ -71,15 +71,15 @@
context "with a successful installation" do
it "restart service" do
expect(Bundle::BrewServices).to receive(:restart).with(formula, verbose: false).and_return(true)
described_class.preinstall(formula, restart_service: true)
described_class.install(formula, restart_service: true)
described_class.preinstall(formula, restart_service: :always)
described_class.install(formula, restart_service: :always)
end
end

context "with a skipped installation" do
it "restart service" do
expect(Bundle::BrewServices).to receive(:restart).with(formula, verbose: false).and_return(true)
described_class.install(formula, preinstall: false, restart_service: true)
described_class.install(formula, preinstall: false, restart_service: :always)
end
end
end
Expand Down Expand Up @@ -177,31 +177,69 @@
allow_any_instance_of(described_class).to receive(:upgrade!).and_return(true)
end

def expectations(verbose:)
it "unlinks conflicts and stops their services" do
verbose = false
expect(Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "unlink", "mysql55",
verbose:).and_return(true)
expect(Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "unlink", "mysql56",
verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:stop).with("mysql55", verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:stop).with("mysql56", verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:restart).with(formula, verbose:).and_return(true)
end

# These tests wrap expect() calls in `expectations`
# rubocop:disable RSpec/NoExpectationExample
it "unlinks conflicts and stops their services" do
expectations(verbose: false)
described_class.preinstall(formula, restart_service: true, conflicts_with: ["mysql56"])
described_class.install(formula, restart_service: true, conflicts_with: ["mysql56"])
described_class.preinstall(formula, restart_service: :always, conflicts_with: ["mysql56"])
described_class.install(formula, restart_service: :always, conflicts_with: ["mysql56"])
end

it "prints a message" do
allow_any_instance_of(described_class).to receive(:puts)
expectations(verbose: true)
described_class.preinstall(formula, restart_service: true, conflicts_with: ["mysql56"], verbose: true)
described_class.install(formula, restart_service: true, conflicts_with: ["mysql56"], verbose: true)
verbose = true
expect(Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "unlink", "mysql55",
verbose:).and_return(true)
expect(Bundle).to receive(:system).with(HOMEBREW_BREW_FILE, "unlink", "mysql56",
verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:stop).with("mysql55", verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:stop).with("mysql56", verbose:).and_return(true)
expect(Bundle::BrewServices).to receive(:restart).with(formula, verbose:).and_return(true)
described_class.preinstall(formula, restart_service: :always, conflicts_with: ["mysql56"], verbose: true)
described_class.install(formula, restart_service: :always, conflicts_with: ["mysql56"], verbose: true)
end
end

context "when the postinstall option is provided" do
before do
allow_any_instance_of(described_class).to receive(:install_change_state!).and_return(true)
allow_any_instance_of(described_class).to receive(:installed?).and_return(true)
end

context "when formula has changed" do
before do
allow_any_instance_of(described_class).to receive(:changed?).and_return(true)
end

it "runs the postinstall command" do
expect(Bundle).to receive(:system).with("custom command", verbose: false).and_return(true)
described_class.preinstall(formula, postinstall: "custom command")
described_class.install(formula, postinstall: "custom command")
end

it "reports a failure" do
expect(Bundle).to receive(:system).with("custom command", verbose: false).and_return(false)
described_class.preinstall(formula, postinstall: "custom command")
expect(described_class.install(formula, postinstall: "custom command")).to be(false)
end
end

context "when formula has not changed" do
before do
allow_any_instance_of(described_class).to receive(:changed?).and_return(false)
end

it "does not run the postinstall command" do
expect(Bundle).not_to receive(:system)
described_class.preinstall(formula, postinstall: "custom command")
described_class.install(formula, postinstall: "custom command")
end
end
# rubocop:enable RSpec/NoExpectationExample
end
end

Expand Down Expand Up @@ -396,6 +434,10 @@ def expectations(verbose:)
it "is false with {restart_service: :changed}" do
expect(described_class.new(formula, restart_service: :changed).start_service_needed?).to be(false)
end

it "is false with {restart_service: :always}" do
expect(described_class.new(formula, restart_service: :always).start_service_needed?).to be(false)
end
end

context "when a service is not started" do
Expand All @@ -418,6 +460,10 @@ def expectations(verbose:)
it "is true if {restart_service: :changed}" do
expect(described_class.new(formula, restart_service: :changed).start_service_needed?).to be(true)
end

it "is true if {restart_service: :always}" do
expect(described_class.new(formula, restart_service: :always).start_service_needed?).to be(true)
end
end
end

Expand All @@ -432,6 +478,12 @@ def expectations(verbose:)
end
end

context "when the restart_service option is always" do
it "is true" do
expect(described_class.new(formula, restart_service: :always).restart_service?).to be(true)
end
end

context "when the restart_service option is changed" do
it "is true" do
expect(described_class.new(formula, restart_service: :changed).restart_service?).to be(true)
Expand All @@ -449,8 +501,12 @@ def expectations(verbose:)
allow_any_instance_of(described_class).to receive(:changed?).and_return(false)
end

it "is true with {restart_service: true}" do
expect(described_class.new(formula, restart_service: true).restart_service_needed?).to be(true)
it "is false with {restart_service: true}" do
expect(described_class.new(formula, restart_service: true).restart_service_needed?).to be(false)
end
MikeMcQuaid marked this conversation as resolved.
Show resolved Hide resolved

it "is true with {restart_service: :always}" do
expect(described_class.new(formula, restart_service: :always).restart_service_needed?).to be(true)
end

it "is false if {restart_service: :changed}" do
Expand All @@ -467,6 +523,10 @@ def expectations(verbose:)
expect(described_class.new(formula, restart_service: true).restart_service_needed?).to be(true)
end

it "is true with {restart_service: :always}" do
expect(described_class.new(formula, restart_service: :always).restart_service_needed?).to be(true)
end

it "is true if {restart_service: :changed}" do
expect(described_class.new(formula, restart_service: :changed).restart_service_needed?).to be(true)
end
Expand Down
22 changes: 22 additions & 0 deletions spec/bundle/cask_installer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,27 @@
end
end
end

context "when the postinstall option is provided" do
before do
Bundle::CaskDumper.reset!
allow(Bundle::CaskDumper).to receive_messages(cask_names: ["google-chrome"],
outdated_cask_names: ["google-chrome"])
allow(Bundle).to receive(:brew).and_return(true)
allow(described_class).to receive(:upgrading?).and_return(true)
end

it "runs the postinstall command" do
expect(Bundle).to receive(:system).with("custom command", verbose: false).and_return(true)
expect(described_class.preinstall("google-chrome", postinstall: "custom command")).to be(true)
expect(described_class.install("google-chrome", postinstall: "custom command")).to be(true)
end

it "reports a failure when postinstall fails" do
expect(Bundle).to receive(:system).with("custom command", verbose: false).and_return(false)
expect(described_class.preinstall("google-chrome", postinstall: "custom command")).to be(true)
expect(described_class.install("google-chrome", postinstall: "custom command")).to be(false)
end
end
end
end
Loading