Skip to content

Commit 486ec7d

Browse files
committed
Add brew bundle services helper
1 parent 75de3aa commit 486ec7d

File tree

10 files changed

+147
-16
lines changed

10 files changed

+147
-16
lines changed

Library/Homebrew/bundle.rb

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
require "bundle/dumper"
2626
require "bundle/installer"
2727
require "bundle/lister"
28+
require "bundle/services"
2829
require "bundle/commands/install"
2930
require "bundle/commands/dump"
3031
require "bundle/commands/cleanup"
@@ -33,6 +34,7 @@
3334
require "bundle/commands/list"
3435
require "bundle/commands/add"
3536
require "bundle/commands/remove"
37+
require "bundle/commands/services"
3638
require "bundle/whalebrew_installer"
3739
require "bundle/whalebrew_dumper"
3840
require "bundle/vscode_extension_checker"

Library/Homebrew/bundle/bundle.rb

+16
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,22 @@ def exchange_uid_if_needed!(&block)
8282

8383
return_value
8484
end
85+
86+
sig { returns(T::Hash[String, String]) }
87+
def formula_version_map
88+
formula_versions = {}
89+
ENV.each do |key, value|
90+
match = key.match(/^HOMEBREW_BUNDLE_EXEC_FORMULA_VERSION_(.+)$/)
91+
next if match.blank?
92+
93+
formula_name = match[1]
94+
next if formula_name.blank?
95+
96+
ENV.delete(key)
97+
formula_versions[formula_name.downcase] = value
98+
end
99+
formula_versions
100+
end
85101
end
86102
end
87103
end

Library/Homebrew/bundle/commands/exec.rb

+1-12
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,7 @@ def run(*args, global: false, file: nil, subcommand: "")
109109
end
110110

111111
# Replace the formula versions from the environment variables
112-
formula_versions = {}
113-
ENV.each do |key, value|
114-
match = key.match(/^HOMEBREW_BUNDLE_EXEC_FORMULA_VERSION_(.+)$/)
115-
next if match.blank?
116-
117-
formula_name = match[1]
118-
next if formula_name.blank?
119-
120-
ENV.delete(key)
121-
formula_versions[formula_name.downcase] = value
122-
end
123-
formula_versions.each do |formula_name, formula_version|
112+
Bundle.formula_version_map.each do |formula_name, formula_version|
124113
ENV.each do |key, value|
125114
opt = %r{/opt/#{formula_name}([/:$])}
126115
next unless value.match(opt)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
module Homebrew
5+
module Bundle
6+
module Commands
7+
module Services
8+
sig { params(args: String, global: T::Boolean, file: T.nilable(String)).void }
9+
def self.run(*args, global:, file:)
10+
raise UsageError, "invalid `brew bundle services` arguments" if args.length != 1
11+
12+
parsed_entries = Brewfile.read(global:, file:).entries
13+
14+
subcommand = args.first
15+
case subcommand
16+
when "run"
17+
Homebrew::Bundle::Services.run(parsed_entries)
18+
when "stop"
19+
Homebrew::Bundle::Services.stop(parsed_entries)
20+
else
21+
raise UsageError, "unknown bundle services subcommand: #{subcommand}"
22+
end
23+
end
24+
end
25+
end
26+
end
27+
end

Library/Homebrew/bundle/services.rb

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "formula"
5+
require "services/system"
6+
7+
module Homebrew
8+
module Bundle
9+
module Services
10+
sig {
11+
params(
12+
entries: T::Array[Homebrew::Bundle::Dsl::Entry],
13+
_block: T.proc.params(info: T::Hash[String, T.anything], service_file: Pathname).void,
14+
).void
15+
}
16+
private_class_method def self.map_entries(entries, &_block)
17+
formula_versions = Bundle.formula_version_map
18+
19+
entries.filter_map do |entry|
20+
next if entry.type != :brew
21+
22+
formula = Formula[entry.name]
23+
next unless formula.any_version_installed?
24+
25+
version = formula_versions[entry.name.downcase]
26+
prefix = formula.rack/version if version
27+
28+
service_file = if prefix&.directory?
29+
if Homebrew::Services::System.launchctl?
30+
prefix/"#{formula.plist_name}.plist"
31+
else
32+
prefix/"#{formula.service_name}.service"
33+
end
34+
end
35+
36+
unless service_file&.file?
37+
prefix = formula.any_installed_prefix
38+
next if prefix.nil?
39+
40+
service_file = if Homebrew::Services::System.launchctl?
41+
prefix/"#{formula.plist_name}.plist"
42+
else
43+
prefix/"#{formula.service_name}.service"
44+
end
45+
end
46+
47+
next unless service_file.file?
48+
49+
# We parse from a command invocation so that brew wrappers can invoke special actions
50+
# for the elevated nature of `brew services`
51+
output = Utils.safe_popen_read(HOMEBREW_BREW_FILE, "services", "info", "--json", formula.full_name)
52+
info = JSON.parse(output)
53+
54+
raise "Failed to get service info for #{entry.name}" if info.length != 1
55+
56+
yield info.first, service_file
57+
end
58+
end
59+
60+
sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void }
61+
def self.run(entries)
62+
map_entries(entries) do |info, service_file|
63+
next if info["running"]
64+
65+
safe_system HOMEBREW_BREW_FILE, "services", "run", "--file=#{service_file}", info["name"]
66+
end
67+
end
68+
69+
sig { params(entries: T::Array[Homebrew::Bundle::Dsl::Entry]).void }
70+
def self.stop(entries)
71+
map_entries(entries) do |info, _service_file|
72+
next unless info["loaded"]
73+
74+
# Try avoid services not started by `brew bundle services`
75+
next if Homebrew::Services::System.launchctl? && info["registered"]
76+
77+
safe_system HOMEBREW_BREW_FILE, "services", "stop", info["name"]
78+
end
79+
end
80+
end
81+
end
82+
end

Library/Homebrew/cmd/bundle.rb

+10-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ class Bundle < AbstractCommand
6161
6262
`brew bundle env`:
6363
Print the environment variables that would be set in a `brew bundle exec` environment.
64+
65+
`brew bundle services run`:
66+
Start services for formulae specified in the `Brewfile`.
67+
68+
`brew bundle services stop`:
69+
Stop services for formulae specified in the `Brewfile`.
6470
EOS
6571
flag "--file=",
6672
description: "Read from or write to the `Brewfile` from this location. " \
@@ -133,7 +139,7 @@ def run
133139
require "bundle"
134140

135141
subcommand = args.named.first.presence
136-
if ["exec", "add", "remove"].exclude?(subcommand) && args.named.size > 1
142+
if %w[exec add remove services].exclude?(subcommand) && args.named.size > 1
137143
raise UsageError, "This command does not take more than 1 subcommand argument."
138144
end
139145

@@ -263,6 +269,9 @@ def run
263269
else
264270
Homebrew::Bundle::Commands::Remove.run(*named_args, type: selected_types.first, global:, file:)
265271
end
272+
when "services"
273+
_, *named_args = args.named
274+
Homebrew::Bundle::Commands::Services.run(*named_args, global:, file:)
266275
else
267276
raise UsageError, "unknown subcommand: #{subcommand}"
268277
end

Library/Homebrew/services/commands/info.rb

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def self.output(hash, verbose:)
5353
return out unless verbose
5454

5555
out += "File: #{hash[:file]} #{pretty_bool(hash[:file].present?)}\n"
56+
out += "Registered at login: #{pretty_bool(hash[:registered])}\n"
5657
out += "Command: #{hash[:command]}\n" unless hash[:command].nil?
5758
out += "Working directory: #{hash[:working_dir]}\n" unless hash[:working_dir].nil?
5859
out += "Root directory: #{hash[:root_dir]}\n" unless hash[:root_dir].nil?

Library/Homebrew/services/formula_wrapper.rb

+1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def to_hash
201201
user: owner,
202202
status: status_symbol,
203203
file: service_file_present? ? dest : service_file,
204+
registered: service_file_present?,
204205
}
205206

206207
return hash unless service?

Library/Homebrew/test/services/commands/info_spec.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,15 @@
8989
it "returns verbose output" do
9090
out = "service ()\nRunning: true\n"
9191
out += "Loaded: true\nSchedulable: false\n"
92-
out += "User: user\nPID: 42\nFile: /dev/null true\nCommand: /bin/command\n"
92+
out += "User: user\nPID: 42\nFile: /dev/null true\nRegistered at login: true\nCommand: /bin/command\n"
9393
out += "Working directory: /working/dir\nRoot directory: /root/dir\nLog: /log/dir\nError log: /log/dir/error\n"
9494
out += "Interval: 3600s\nCron: 5 * * * *\n"
9595
formula = {
9696
name: "service",
9797
user: "user",
9898
status: :started,
9999
file: "/dev/null",
100+
registered: true,
100101
running: true,
101102
loaded: true,
102103
schedulable: false,

Library/Homebrew/test/services/formula_wrapper_spec.rb

+5-2
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,7 @@
371371
loaded: false,
372372
name: "mysql",
373373
pid: nil,
374+
registered: false,
374375
running: false,
375376
schedulable: nil,
376377
service_name: "plist-mysql-test",
@@ -384,13 +385,14 @@
384385
ENV["HOME"] = "/tmp_home"
385386
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
386387
expect(service).to receive(:service?).twice.and_return(false)
387-
expect(service).to receive(:service_file_present?).and_return(true)
388+
expect(service).to receive(:service_file_present?).twice.and_return(true)
388389
expected = {
389390
exit_code: nil,
390391
file: Pathname.new("/tmp_home/Library/LaunchAgents/homebrew.mysql.plist"),
391392
loaded: false,
392393
name: "mysql",
393394
pid: nil,
395+
registered: true,
394396
running: false,
395397
schedulable: nil,
396398
service_name: "plist-mysql-test",
@@ -404,7 +406,7 @@
404406
ENV["HOME"] = "/tmp_home"
405407
allow(Homebrew::Services::System).to receive_messages(launchctl?: true, systemctl?: false)
406408
expect(service).to receive(:service?).twice.and_return(true)
407-
expect(service).to receive(:service_file_present?).and_return(true)
409+
expect(service).to receive(:service_file_present?).twice.and_return(true)
408410
expect(service).to receive(:load_service).twice.and_return(service_object)
409411
expected = {
410412
command: "/bin/cmd",
@@ -417,6 +419,7 @@
417419
log_path: nil,
418420
name: "mysql",
419421
pid: nil,
422+
registered: true,
420423
root_dir: nil,
421424
running: false,
422425
schedulable: false,

0 commit comments

Comments
 (0)