Merge pull request #15007 from apainintheneck/add-service-block-to-formula-api

Add service block to formula api
This commit is contained in:
Kevin 2023-03-23 17:30:04 -07:00 committed by GitHub
commit 86c518e2d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 245 additions and 48 deletions

View File

@ -6,6 +6,7 @@ require "cask/config"
require "cask/dsl" require "cask/dsl"
require "cask/metadata" require "cask/metadata"
require "utils/bottles" require "utils/bottles"
require "extend/api_hashable"
module Cask module Cask
# An instance of a cask. # An instance of a cask.
@ -16,49 +17,14 @@ module Cask
extend Forwardable extend Forwardable
extend Predicable extend Predicable
extend APIHashable
include Metadata include Metadata
# Needs a leading slash to avoid `File.expand.path` complaining about non-absolute home.
HOME_PLACEHOLDER = "/$HOME"
HOMEBREW_PREFIX_PLACEHOLDER = "$HOMEBREW_PREFIX"
APPDIR_PLACEHOLDER = "$APPDIR"
attr_reader :token, :sourcefile_path, :source, :config, :default_config, :loader attr_reader :token, :sourcefile_path, :source, :config, :default_config, :loader
attr_accessor :download, :allow_reassignment attr_accessor :download, :allow_reassignment
attr_predicate :loaded_from_api? attr_predicate :loaded_from_api?
class << self
def generating_hash!
return if generating_hash?
# Apply monkeypatches for API generation
@old_homebrew_prefix = HOMEBREW_PREFIX
@old_home = Dir.home
Object.send(:remove_const, :HOMEBREW_PREFIX)
Object.const_set(:HOMEBREW_PREFIX, Pathname(HOMEBREW_PREFIX_PLACEHOLDER))
ENV["HOME"] = HOME_PLACEHOLDER
@generating_hash = true
end
def generated_hash!
return unless generating_hash?
# Revert monkeypatches for API generation
Object.send(:remove_const, :HOMEBREW_PREFIX)
Object.const_set(:HOMEBREW_PREFIX, @old_homebrew_prefix)
ENV["HOME"] = @old_home
@generating_hash = false
end
def generating_hash?
@generating_hash ||= false
@generating_hash == true
end
end
def self.all def self.all
# TODO: ideally avoid using ARGV by moving to e.g. CLI::Parser # TODO: ideally avoid using ARGV by moving to e.g. CLI::Parser
if ARGV.exclude?("--eval-all") && !Homebrew::EnvConfig.eval_all? if ARGV.exclude?("--eval-all") && !Homebrew::EnvConfig.eval_all?

View File

@ -327,9 +327,9 @@ module Cask
def from_h_string_gsubs(string, appdir) def from_h_string_gsubs(string, appdir)
string.to_s string.to_s
.gsub(Cask::HOME_PLACEHOLDER, Dir.home) .gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
.gsub(Cask::HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX) .gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(Cask::APPDIR_PLACEHOLDER, appdir) .gsub(HOMEBREW_CASK_APPDIR_PLACEHOLDER, appdir)
end end
def from_h_array_gsubs(array, appdir) def from_h_array_gsubs(array, appdir)

View File

@ -377,7 +377,7 @@ module Cask
# @api public # @api public
def appdir def appdir
return Cask::APPDIR_PLACEHOLDER if Cask.generating_hash? return HOMEBREW_CASK_APPDIR_PLACEHOLDER if Cask.generating_hash?
cask.config.appdir cask.config.appdir
end end

View File

@ -50,6 +50,7 @@ module Homebrew
FileUtils.mkdir_p directories FileUtils.mkdir_p directories
Formulary.enable_factory_cache! Formulary.enable_factory_cache!
Formula.generating_hash!
tap.formula_names.each do |name| tap.formula_names.each do |name|
formula = Formulary.factory(name) formula = Formulary.factory(name)

View File

@ -0,0 +1,34 @@
# typed: true
# frozen_string_literal: true
# Used to substitute common paths with generic placeholders when generating JSON for the API.
module APIHashable
def generating_hash!
return if generating_hash?
# Apply monkeypatches for API generation
@old_homebrew_prefix = HOMEBREW_PREFIX
@old_home = Dir.home
Object.send(:remove_const, :HOMEBREW_PREFIX)
Object.const_set(:HOMEBREW_PREFIX, Pathname.new(HOMEBREW_PREFIX_PLACEHOLDER))
ENV["HOME"] = HOMEBREW_HOME_PLACEHOLDER
@generating_hash = true
end
def generated_hash!
return unless generating_hash?
# Revert monkeypatches for API generation
Object.send(:remove_const, :HOMEBREW_PREFIX)
Object.const_set(:HOMEBREW_PREFIX, @old_homebrew_prefix)
ENV["HOME"] = @old_home
@generating_hash = false
end
def generating_hash?
@generating_hash ||= false
@generating_hash == true
end
end

View File

@ -30,6 +30,7 @@ require "find"
require "utils/spdx" require "utils/spdx"
require "extend/on_system" require "extend/on_system"
require "api" require "api"
require "extend/api_hashable"
# A formula provides instructions and metadata for Homebrew to install a piece # A formula provides instructions and metadata for Homebrew to install a piece
# of software. Every Homebrew formula is a {Formula}. # of software. Every Homebrew formula is a {Formula}.
@ -69,6 +70,7 @@ class Formula
extend Forwardable extend Forwardable
extend Cachable extend Cachable
extend Predicable extend Predicable
extend APIHashable
# The name of this {Formula}. # The name of this {Formula}.
# e.g. `this-formula` # e.g. `this-formula`
@ -1043,7 +1045,7 @@ class Formula
def service def service
return unless service? return unless service?
Homebrew::Service.new(self, &self.class.service) @service ||= Homebrew::Service.new(self, &self.class.service)
end end
# @private # @private
@ -2132,6 +2134,7 @@ class Formula
"disabled" => disabled?, "disabled" => disabled?,
"disable_date" => disable_date, "disable_date" => disable_date,
"disable_reason" => disable_reason, "disable_reason" => disable_reason,
"service" => service&.serialize,
"tap_git_head" => tap_git_head, "tap_git_head" => tap_git_head,
"ruby_source_checksum" => {}, "ruby_source_checksum" => {},
} }

View File

@ -263,6 +263,21 @@ module Formulary
raise "Cannot build from source from abstract formula." raise "Cannot build from source from abstract formula."
end 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"] @caveats_string = json_formula["caveats"]
def caveats def caveats
self.class.instance_variable_get(:@caveats_string) self.class.instance_variable_get(:@caveats_string)

View File

@ -58,6 +58,9 @@ HOMEBREW_MACOS_ARM_DEFAULT_REPOSITORY = ENV.fetch("HOMEBREW_MACOS_ARM_DEFAULT_RE
HOMEBREW_LINUX_DEFAULT_PREFIX = ENV.fetch("HOMEBREW_LINUX_DEFAULT_PREFIX").freeze HOMEBREW_LINUX_DEFAULT_PREFIX = ENV.fetch("HOMEBREW_LINUX_DEFAULT_PREFIX").freeze
HOMEBREW_LINUX_DEFAULT_REPOSITORY = ENV.fetch("HOMEBREW_LINUX_DEFAULT_REPOSITORY").freeze HOMEBREW_LINUX_DEFAULT_REPOSITORY = ENV.fetch("HOMEBREW_LINUX_DEFAULT_REPOSITORY").freeze
HOMEBREW_PREFIX_PLACEHOLDER = "$HOMEBREW_PREFIX" HOMEBREW_PREFIX_PLACEHOLDER = "$HOMEBREW_PREFIX"
# Needs a leading slash to avoid `File.expand.path` complaining about non-absolute home.
HOMEBREW_HOME_PLACEHOLDER = "/$HOME"
HOMEBREW_CASK_APPDIR_PLACEHOLDER = "$APPDIR"
HOMEBREW_MACOS_NEWEST_UNSUPPORTED = ENV.fetch("HOMEBREW_MACOS_NEWEST_UNSUPPORTED").freeze HOMEBREW_MACOS_NEWEST_UNSUPPORTED = ENV.fetch("HOMEBREW_MACOS_NEWEST_UNSUPPORTED").freeze
HOMEBREW_MACOS_OLDEST_SUPPORTED = ENV.fetch("HOMEBREW_MACOS_OLDEST_SUPPORTED").freeze HOMEBREW_MACOS_OLDEST_SUPPORTED = ENV.fetch("HOMEBREW_MACOS_OLDEST_SUPPORTED").freeze

View File

@ -45,6 +45,10 @@ module Homebrew
).returns(T.nilable(Array)) ).returns(T.nilable(Array))
} }
def run(command = nil, macos: nil, linux: nil) def run(command = nil, macos: nil, linux: nil)
# Save parameters for serialization
@run_params ||= command
@run_params ||= { macos: macos, linux: linux }.compact
command ||= on_system_conditional(macos: macos, linux: linux) command ||= on_system_conditional(macos: macos, linux: linux)
case T.unsafe(command) case T.unsafe(command)
when nil when nil
@ -156,7 +160,7 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def requires_root? def requires_root?
instance_eval(&@service_block) eval_service_block
@require_root.present? && @require_root == true @require_root.present? && @require_root == true
end end
@ -192,7 +196,7 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def keep_alive? def keep_alive?
instance_eval(&@service_block) eval_service_block
@keep_alive.present? && @keep_alive[:always] != false @keep_alive.present? && @keep_alive[:always] != false
end end
@ -320,7 +324,7 @@ module Homebrew
parsed parsed
end 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 = {}) def environment_variables(variables = {})
case T.unsafe(variables) case T.unsafe(variables)
when Hash when Hash
@ -351,7 +355,7 @@ module Homebrew
sig { returns(T.nilable(T::Array[String])) } sig { returns(T.nilable(T::Array[String])) }
def command def command
instance_eval(&@service_block) eval_service_block
@run&.map(&:to_s) @run&.map(&:to_s)
end end
@ -359,7 +363,7 @@ module Homebrew
# @return [String] # @return [String]
sig { returns(String) } sig { returns(String) }
def manual_command def manual_command
instance_eval(&@service_block) eval_service_block
vars = @environment_variables.except(:PATH) vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" } .map { |k, v| "#{k}=\"#{v}\"" }
@ -372,7 +376,7 @@ module Homebrew
# @return [Boolean] # @return [Boolean]
sig { returns(T::Boolean) } sig { returns(T::Boolean) }
def timed? def timed?
instance_eval(&@service_block) eval_service_block
@run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL @run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL
end end
@ -484,7 +488,7 @@ module Homebrew
Unit=#{@formula.service_name} Unit=#{@formula.service_name}
EOS EOS
instance_eval(&@service_block) eval_service_block
options = [] options = []
options << "Persistent=true" if @run_type == RUN_TYPE_CRON options << "Persistent=true" if @run_type == RUN_TYPE_CRON
options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL
@ -497,5 +501,101 @@ module Homebrew
timer + options.join("\n") timer + options.join("\n")
end 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
# Prepare the service hash for inclusion in the formula API JSON.
sig { returns(Hash) }
def serialize
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
# Turn the service API hash values back into what is expected by the formula DSL.
sig { params(api_hash: Hash).returns(Hash) }
def self.deserialize(api_hash)
hash = {}
hash[:run] =
case api_hash["run"]
when Hash
api_hash["run"].to_h do |key, array|
[
key.to_sym,
array.map(&method(:replace_placeholders)),
]
end
when Array
api_hash["run"].map(&method(:replace_placeholders))
end
hash[:keep_alive] = api_hash["keep_alive"].transform_keys(&:to_sym) if api_hash.key?("keep_alive")
if api_hash.key?("environment_variables")
hash[:environment_variables] = api_hash["environment_variables"].to_h do |key, value|
[key.to_sym, replace_placeholders(value)]
end
end
%w[run_type process_type].each do |key|
next unless (value = api_hash[key])
hash[key.to_sym] = value.to_sym
end
%w[working_dir root_dir input_path log_path error_log_path].each do |key|
next unless (value = api_hash[key])
hash[key.to_sym] = replace_placeholders(value)
end
%w[interval cron launch_only_once require_root restart_delay macos_legacy_timers sockets].each do |key|
next if (value = api_hash[key]).nil?
hash[key.to_sym] = value
end
hash
end
# Replace API path placeholders with local paths.
sig { params(string: String).returns(String) }
def self.replace_placeholders(string)
string.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
end
end end
end end

View File

@ -337,6 +337,8 @@ describe Cask::Cask, :cask do
expect(JSON.pretty_generate(h["variations"])).to eq expected_sha256_variations.strip expect(JSON.pretty_generate(h["variations"])).to eq expected_sha256_variations.strip
end end
# @note The calls to `Cask.generating_hash!` and `Cask.generated_hash!`
# are not idempotent so they can only be used in one test.
it "returns the correct hash placeholders" do it "returns the correct hash placeholders" do
described_class.generating_hash! described_class.generating_hash!
expect(described_class).to be_generating_hash expect(described_class).to be_generating_hash

View File

@ -272,6 +272,11 @@ describe Formulary do
"conflicts_with_reasons" => ["it does"], "conflicts_with_reasons" => ["it does"],
"link_overwrite" => ["bin/abc"], "link_overwrite" => ["bin/abc"],
"caveats" => "example caveat string", "caveats" => "example caveat string",
"service" => {
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate",
"working_dir" => "/$HOME",
},
}.merge(extra_items), }.merge(extra_items),
} }
end end
@ -354,6 +359,11 @@ describe Formulary do
expect(formula.caveats).to eq "example caveat string" expect(formula.caveats).to eq "example caveat string"
expect(formula).to be_a_service
expect(formula.service.command).to eq(["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"])
expect(formula.service.run_type).to eq(:immediate)
expect(formula.service.working_dir).to eq(Dir.home)
expect do expect do
formula.install formula.install
end.to raise_error("Cannot build from source from abstract formula.") end.to raise_error("Cannot build from source from abstract formula.")

View File

@ -913,4 +913,67 @@ describe Homebrew::Service do
expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test", "macos"]) expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test", "macos"])
end end
end end
describe "#serialize" do
let(:serialized_hash) do
{
environment_variables: {
PATH: "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
},
run: [Pathname("$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd"), "test"],
run_type: :immediate,
working_dir: "/$HOME",
cron: "0 0 * * 0",
sockets: "tcp://0.0.0.0:80",
}
end
# @note The calls to `Formula.generating_hash!` and `Formula.generated_hash!`
# are not idempotent so they can only be used in one test.
it "replaces local paths with placeholders" do
f = stub_formula do
service do
run [opt_bin/"beanstalkd", "test"]
environment_variables PATH: std_service_path_env
working_dir Dir.home
cron "@weekly"
sockets "tcp://0.0.0.0:80"
end
end
Formula.generating_hash!
expect(f.service.serialize).to eq(serialized_hash)
Formula.generated_hash!
end
end
describe ".deserialize" do
let(:serialized_hash) do
{
"environment_variables" => {
"PATH" => "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
},
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate",
"working_dir" => HOMEBREW_HOME_PLACEHOLDER,
"keep_alive" => { "successful_exit" => false },
}
end
let(:deserialized_hash) do
{
environment_variables: {
PATH: "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
},
run: ["#{HOMEBREW_PREFIX}/opt/formula_name/bin/beanstalkd", "test"],
run_type: :immediate,
working_dir: Dir.home,
keep_alive: { successful_exit: false },
}
end
it "replaces placeholders with local paths" do
expect(described_class.deserialize(serialized_hash)).to eq(deserialized_hash)
end
end
end end