From db8d488db418653d3a454dc2654f5a7d256ce379 Mon Sep 17 00:00:00 2001 From: apainintheneck Date: Fri, 17 Mar 2023 23:24:37 -0700 Subject: [PATCH] service: add info to API json and load from api As a part of serializing the hash, certain path placeholders are used for the HOMEBREW_PREFIX and $HOME directories. Beyond that certain elements need to be turned back into strings like cron and sockets and symbols need to be preserved as well. The run command accepts either an arg or kwargs so it has to be treated specially here. --- Library/Homebrew/formula.rb | 3 +- Library/Homebrew/formulary.rb | 15 +++++ Library/Homebrew/service.rb | 105 +++++++++++++++++++++++++++++++--- 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index bc3bb68d26..c71a854cc2 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -1043,7 +1043,7 @@ class Formula def service return unless service? - Homebrew::Service.new(self, &self.class.service) + @service ||= Homebrew::Service.new(self, &self.class.service) end # @private @@ -2132,6 +2132,7 @@ class Formula "disabled" => disabled?, "disable_date" => disable_date, "disable_reason" => disable_reason, + "service" => service&.serialize, "tap_git_head" => tap_git_head, "ruby_source_checksum" => {}, } diff --git a/Library/Homebrew/formulary.rb b/Library/Homebrew/formulary.rb index aa179fbcfa..0113ace173 100644 --- a/Library/Homebrew/formulary.rb +++ b/Library/Homebrew/formulary.rb @@ -263,6 +263,21 @@ module Formulary raise "Cannot build from source from abstract formula." end + if (service_hash = json_formula["service"]) + service_hash = Homebrew::Service.deserialize(service_hash) + run_params = service_hash.delete("run") + service do + if run_params.is_a?(Hash) + run(**run_params) + else + run run_params + end + service_hash.each do |key, arg| + public_send(key, arg) + end + end + end + @caveats_string = json_formula["caveats"] def caveats self.class.instance_variable_get(:@caveats_string) diff --git a/Library/Homebrew/service.rb b/Library/Homebrew/service.rb index 4725a8acb0..7f149b3a1f 100644 --- a/Library/Homebrew/service.rb +++ b/Library/Homebrew/service.rb @@ -45,6 +45,9 @@ module Homebrew ).returns(T.nilable(Array)) } def run(command = nil, macos: nil, linux: nil) + # Save parameters for serialization + @run_params ||= command || { macos: macos, linux: linux }.compact + command ||= on_system_conditional(macos: macos, linux: linux) case T.unsafe(command) when nil @@ -156,7 +159,7 @@ module Homebrew # @return [Boolean] sig { returns(T::Boolean) } def requires_root? - instance_eval(&@service_block) + eval_service_block @require_root.present? && @require_root == true end @@ -192,7 +195,7 @@ module Homebrew # @return [Boolean] sig { returns(T::Boolean) } def keep_alive? - instance_eval(&@service_block) + eval_service_block @keep_alive.present? && @keep_alive[:always] != false end @@ -320,7 +323,7 @@ module Homebrew parsed end - sig { params(variables: T::Hash[String, String]).returns(T.nilable(T::Hash[String, String])) } + sig { params(variables: T::Hash[Symbol, String]).returns(T.nilable(T::Hash[Symbol, String])) } def environment_variables(variables = {}) case T.unsafe(variables) when Hash @@ -351,7 +354,7 @@ module Homebrew sig { returns(T.nilable(T::Array[String])) } def command - instance_eval(&@service_block) + eval_service_block @run&.map(&:to_s) end @@ -359,7 +362,7 @@ module Homebrew # @return [String] sig { returns(String) } def manual_command - instance_eval(&@service_block) + eval_service_block vars = @environment_variables.except(:PATH) .map { |k, v| "#{k}=\"#{v}\"" } @@ -372,7 +375,7 @@ module Homebrew # @return [Boolean] sig { returns(T::Boolean) } def timed? - instance_eval(&@service_block) + eval_service_block @run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL end @@ -484,7 +487,7 @@ module Homebrew Unit=#{@formula.service_name} EOS - instance_eval(&@service_block) + eval_service_block options = [] options << "Persistent=true" if @run_type == RUN_TYPE_CRON options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL @@ -497,5 +500,93 @@ module Homebrew timer + options.join("\n") end + + # Only evaluate the service block once. + sig { void } + def eval_service_block + return if @eval_service_block + + instance_eval(&@service_block) + @eval_service_block = true + end + + # Recursively prepare the service hash for inclusion in the formula API JSON. + # - Replace each Symbol with a String prefixed by ":" + # - Replace the local HOMEBREW_PREFIX with "$HOMEBREW_PREFIX" + # - Replace the local home directory with "$HOME" + def serialize(elem = to_h) + case elem + when String, Pathname + elem.to_s.gsub(HOMEBREW_PREFIX, HOMEBREW_PREFIX_PLACEHOLDER) + .gsub(Dir.home, "$HOME") + when Symbol + elem.inspect + when Array + elem.map { |value| serialize(value) } + when Hash + elem.to_h do |key, value| + key = key.inspect if key.is_a?(Symbol) + [key, serialize(value)] + end + else + elem + end + end + + # Recursively turn the service API hash values back into what is expected by the formula DSL. + # - Replace each String prefixed with ":" with a Symbol + # - Replace "$HOMEBREW_PREFIX" with the local HOMEBREW_PREFIX environment variable + # - Replace "$HOME" with the local home directory + def self.deserialize(elem) + case elem + when String + return T.must(elem[1..]).to_sym if elem.start_with?(":") + + elem.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX) + .gsub("$HOME", Dir.home) + when Array + elem.map { |value| deserialize(value) } + when Hash + elem.to_h do |key, value| + key = key[1..].to_sym if key.start_with?(":") + [key, deserialize(value)] + end + else + elem + end + end + + sig { returns(Hash) } + def to_h + eval_service_block + + cron_string = if @cron.present? + [:Minute, :Hour, :Day, :Month, :Weekday] + .map { |key| @cron[key].to_s } + .join(" ") + end + + sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present? + + { + "run" => @run_params, + "run_type" => @run_type, + "interval" => @interval, + "cron" => cron_string, + "keep_alive" => @keep_alive, + "launch_only_once" => @launch_only_once, + "require_root" => @require_root, + "environment_variables" => @environment_variables.presence, + "working_dir" => @working_dir, + "root_dir" => @root_dir, + "input_path" => @input_path, + "log_path" => @log_path, + "error_log_path" => @error_log_path, + "restart_delay" => @restart_delay, + "process_type" => @process_type, + "macos_legacy_timers" => @macos_legacy_timers, + "sockets" => sockets_string, + }.compact + end end end