Reduce shape variations in Homebrew::Service

This commit is contained in:
Douglas Eichelberger 2025-07-28 16:50:18 -07:00
parent 00c528bc54
commit c4b02465f5
No known key found for this signature in database
GPG Key ID: F90193CBD547EB81
2 changed files with 65 additions and 50 deletions

View File

@ -306,7 +306,7 @@ module FormulaCellarChecks
return unless formula.service? return unless formula.service?
return unless formula.service.command? return unless formula.service.command?
"Service command does not exist" unless File.exist?(T.must(formula.service.command).first) "Service command does not exist" unless File.exist?(formula.service.command.first)
end end
sig { params(formula: Formula).returns(T.nilable(String)) } sig { params(formula: Formula).returns(T.nilable(String)) }

View File

@ -1,4 +1,4 @@
# typed: true # rubocop:todo Sorbet/StrictSigil # typed: strict
# frozen_string_literal: true # frozen_string_literal: true
require "ipaddr" require "ipaddr"
@ -23,13 +23,38 @@ module Homebrew
PROCESS_TYPE_ADAPTIVE = :adaptive PROCESS_TYPE_ADAPTIVE = :adaptive
KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze KEEP_ALIVE_KEYS = [:always, :successful_exit, :crashed, :path].freeze
SOCKET_STRING_REGEX = %r{^([a-z]+)://(.+):([0-9]+)$}i
# sig { params(formula: Formula).void } RunParam = T.type_alias { T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)) }
Sockets = T.type_alias { T::Hash[Symbol, { host: String, port: String, type: String }] }
sig { returns(String) }
attr_reader :plist_name, :service_name
sig { params(formula: Formula, block: T.nilable(T.proc.void)).void }
def initialize(formula, &block) def initialize(formula, &block)
@cron = T.let({}, T::Hash[Symbol, T.any(Integer, String)])
@environment_variables = T.let({}, T::Hash[Symbol, String])
@error_log_path = T.let(nil, T.nilable(String))
@formula = formula @formula = formula
@run_type = RUN_TYPE_IMMEDIATE @input_path = T.let(nil, T.nilable(String))
@run_at_load = true @interval = T.let(nil, T.nilable(Integer))
@environment_variables = {} @keep_alive = T.let({}, T::Hash[Symbol, T.untyped])
@launch_only_once = T.let(false, T::Boolean)
@log_path = T.let(nil, T.nilable(String))
@macos_legacy_timers = T.let(false, T::Boolean)
@plist_name = T.let(default_plist_name, String)
@process_type = T.let(nil, T.nilable(Symbol))
@require_root = T.let(false, T::Boolean)
@restart_delay = T.let(nil, T.nilable(Integer))
@root_dir = T.let(nil, T.nilable(String))
@run = T.let([], T::Array[String])
@run_at_load = T.let(true, T::Boolean)
@run_params = T.let(nil, T.any(RunParam, T::Hash[Symbol, RunParam]))
@run_type = T.let(RUN_TYPE_IMMEDIATE, Symbol)
@service_name = T.let(default_service_name, String)
@sockets = T.let({}, Sockets)
@working_dir = T.let(nil, T.nilable(String))
instance_eval(&block) if block instance_eval(&block) if block
end end
@ -43,21 +68,11 @@ module Homebrew
"homebrew.mxcl.#{@formula.name}" "homebrew.mxcl.#{@formula.name}"
end end
sig { returns(String) }
def plist_name
@plist_name ||= default_plist_name
end
sig { returns(String) } sig { returns(String) }
def default_service_name def default_service_name
"homebrew.#{@formula.name}" "homebrew.#{@formula.name}"
end end
sig { returns(String) }
def service_name
@service_name ||= default_service_name
end
sig { params(macos: T.nilable(String), linux: T.nilable(String)).void } sig { params(macos: T.nilable(String), linux: T.nilable(String)).void }
def name(macos: nil, linux: nil) def name(macos: nil, linux: nil)
raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String) raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String)
@ -68,9 +83,9 @@ module Homebrew
sig { sig {
params( params(
command: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), command: T.nilable(RunParam),
macos: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), macos: T.nilable(RunParam),
linux: T.nilable(T.any(T::Array[T.any(String, Pathname)], String, Pathname)), linux: T.nilable(RunParam),
).returns(T.nilable(T::Array[T.any(String, Pathname)])) ).returns(T.nilable(T::Array[T.any(String, Pathname)]))
} }
def run(command = nil, macos: nil, linux: nil) def run(command = nil, macos: nil, linux: nil)
@ -78,7 +93,7 @@ module Homebrew
if command if command
@run_params = command @run_params = command
elsif macos || linux elsif macos || linux
@run_params = { macos:, linux: }.compact @run_params = { macos:, linux: }.compact, T.any(RunParam, T::Hash[Symbol, RunParam])
end end
command ||= on_system_conditional(macos:, linux:) command ||= on_system_conditional(macos:, linux:)
@ -86,9 +101,9 @@ module Homebrew
when nil when nil
@run @run
when String, Pathname when String, Pathname
@run = [command] @run = [command.to_s]
when Array when Array
@run = command @run = command.map(&:to_s)
end end
end end
@ -161,12 +176,12 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def require_root(value = nil) def require_root(value = nil)
case value case value
when nil when nil
@require_root @require_root
when true, false when TrueClass, FalseClass
@require_root = value @require_root = value
end end
end end
@ -183,16 +198,14 @@ module Homebrew
case value case value
when nil when nil
@run_at_load @run_at_load
when true, false when TrueClass, FalseClass
@run_at_load = value @run_at_load = value
end end
end end
SOCKET_STRING_REGEX = %r{^([a-z]+)://(.+):([0-9]+)$}i
sig { sig {
params(value: T.nilable(T.any(String, T::Hash[Symbol, String]))) params(value: T.nilable(T.any(String, T::Hash[Symbol, String])))
.returns(T.nilable(T::Hash[Symbol, T::Hash[Symbol, String]])) .returns(T::Hash[Symbol, T::Hash[Symbol, String]])
} }
def sockets(value = nil) def sockets(value = nil)
return @sockets if value.nil? return @sockets if value.nil?
@ -204,9 +217,11 @@ module Homebrew
value value
end.transform_values do |socket_string| end.transform_values do |socket_string|
match = socket_string.match(SOCKET_STRING_REGEX) match = socket_string.match(SOCKET_STRING_REGEX)
raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" if match.blank? raise TypeError, "Service#sockets a formatted socket definition as <type>://<host>:<port>" unless match
type, host, port = match.captures type = T.must(match[1])
host = T.must(match[2])
port = T.must(match[3])
begin begin
IPAddr.new(host) IPAddr.new(host)
@ -222,15 +237,15 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def keep_alive? def keep_alive?
@keep_alive.present? && @keep_alive[:always] != false !@keep_alive.empty? && @keep_alive[:always] != false
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def launch_only_once(value = nil) def launch_only_once(value = nil)
case value case value
when nil when nil
@launch_only_once @launch_only_once
when true, false when TrueClass, FalseClass
@launch_only_once = value @launch_only_once = value
end end
end end
@ -281,13 +296,13 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(String)).returns(T.nilable(Hash)) } sig { params(value: T.nilable(String)).returns(T::Hash[Symbol, T.any(Integer, String)]) }
def cron(value = nil) def cron(value = nil)
case value case value
when nil when nil
@cron @cron
when String when String
@cron = parse_cron(T.must(value)) @cron = parse_cron(value)
end end
end end
@ -345,12 +360,12 @@ module Homebrew
end end
end end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) } sig { params(value: T.nilable(T::Boolean)).returns(T::Boolean) }
def macos_legacy_timers(value = nil) def macos_legacy_timers(value = nil)
case value case value
when nil when nil
@macos_legacy_timers @macos_legacy_timers
when true, false when TrueClass, FalseClass
@macos_legacy_timers = value @macos_legacy_timers = value
end end
end end
@ -362,14 +377,14 @@ module Homebrew
"#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin" "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
end end
sig { returns(T.nilable(T::Array[String])) } sig { returns(T::Array[String]) }
def command def command
@run&.map(&:to_s)&.map { |arg| arg.start_with?("~") ? File.expand_path(arg) : arg } @run.map(&:to_s).map { |arg| arg.start_with?("~") ? File.expand_path(arg) : arg }
end end
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def command? def command?
@run.present? !@run.empty?
end end
# Returns the `String` command to run manually instead of the service. # Returns the `String` command to run manually instead of the service.
@ -379,8 +394,8 @@ module Homebrew
vars = @environment_variables.except(:PATH) vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" } .map { |k, v| "#{k}=\"#{v}\"" }
out = vars + T.must(command).map { |arg| Utils::Shell.sh_quote(arg) } if command? vars.concat(command.map { |arg| Utils::Shell.sh_quote(arg) })
out.join(" ") vars.join(" ")
end end
# Returns a `Boolean` describing if a service is timed. # Returns a `Boolean` describing if a service is timed.
@ -425,7 +440,7 @@ module Homebrew
end end
end end
if @sockets.present? unless @sockets.empty?
base[:Sockets] = {} base[:Sockets] = {}
@sockets.each do |name, info| @sockets.each do |name, info|
base[:Sockets][name] = { base[:Sockets][name] = {
@ -436,7 +451,7 @@ module Homebrew
end end
end end
if @cron.present? && @run_type == RUN_TYPE_CRON if !@cron.empty? && @run_type == RUN_TYPE_CRON
base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" } base[:StartCalendarInterval] = @cron.reject { |_, value| value == "*" }
end end
@ -456,8 +471,8 @@ module Homebrew
sig { returns(String) } sig { returns(String) }
def to_systemd_unit def to_systemd_unit
# command needs to be first because it initializes all other values # command needs to be first because it initializes all other values
cmd = command&.map { |arg| Utils::Service.systemd_quote(arg) } cmd = command.map { |arg| Utils::Service.systemd_quote(arg) }
&.join(" ") .join(" ")
options = [] options = []
options << "Type=#{(@launch_only_once == true) ? "oneshot" : "simple"}" options << "Type=#{(@launch_only_once == true) ? "oneshot" : "simple"}"
@ -512,7 +527,7 @@ module Homebrew
end end
# Prepare the service hash for inclusion in the formula API JSON. # Prepare the service hash for inclusion in the formula API JSON.
sig { returns(Hash) } sig { returns(T::Hash[Symbol, T.untyped]) }
def to_hash def to_hash
name_params = { name_params = {
macos: (plist_name if plist_name != default_plist_name), macos: (plist_name if plist_name != default_plist_name),
@ -521,13 +536,13 @@ module Homebrew
return { name: name_params }.compact_blank if @run_params.blank? return { name: name_params }.compact_blank if @run_params.blank?
cron_string = if @cron.present? cron_string = unless @cron.empty?
[:Minute, :Hour, :Day, :Month, :Weekday] [:Minute, :Hour, :Day, :Month, :Weekday]
.map { |key| @cron[key].to_s } .map { |key| @cron[key].to_s }
.join(" ") .join(" ")
end end
sockets_var = if @sockets.present? sockets_var = unless @sockets.empty?
@sockets.transform_values { |info| "#{info[:type]}://#{info[:host]}:#{info[:port]}" } @sockets.transform_values { |info| "#{info[:type]}://#{info[:host]}:#{info[:port]}" }
.then do |sockets_hash| .then do |sockets_hash|
# TODO: Remove this code when all users are running on versions of Homebrew # TODO: Remove this code when all users are running on versions of Homebrew
@ -565,7 +580,7 @@ module Homebrew
end end
# Turn the service API hash values back into what is expected by the formula DSL. # Turn the service API hash values back into what is expected by the formula DSL.
sig { params(api_hash: Hash).returns(Hash) } sig { params(api_hash: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.from_hash(api_hash) def self.from_hash(api_hash)
hash = {} hash = {}
hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name") hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name")