Merge pull request #15007 from apainintheneck/add-service-block-to-formula-api
Add service block to formula api
This commit is contained in:
commit
86c518e2d7
@ -6,6 +6,7 @@ require "cask/config"
|
||||
require "cask/dsl"
|
||||
require "cask/metadata"
|
||||
require "utils/bottles"
|
||||
require "extend/api_hashable"
|
||||
|
||||
module Cask
|
||||
# An instance of a cask.
|
||||
@ -16,49 +17,14 @@ module Cask
|
||||
|
||||
extend Forwardable
|
||||
extend Predicable
|
||||
extend APIHashable
|
||||
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_accessor :download, :allow_reassignment
|
||||
|
||||
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
|
||||
# TODO: ideally avoid using ARGV by moving to e.g. CLI::Parser
|
||||
if ARGV.exclude?("--eval-all") && !Homebrew::EnvConfig.eval_all?
|
||||
|
||||
@ -327,9 +327,9 @@ module Cask
|
||||
|
||||
def from_h_string_gsubs(string, appdir)
|
||||
string.to_s
|
||||
.gsub(Cask::HOME_PLACEHOLDER, Dir.home)
|
||||
.gsub(Cask::HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
|
||||
.gsub(Cask::APPDIR_PLACEHOLDER, appdir)
|
||||
.gsub(HOMEBREW_HOME_PLACEHOLDER, Dir.home)
|
||||
.gsub(HOMEBREW_PREFIX_PLACEHOLDER, HOMEBREW_PREFIX)
|
||||
.gsub(HOMEBREW_CASK_APPDIR_PLACEHOLDER, appdir)
|
||||
end
|
||||
|
||||
def from_h_array_gsubs(array, appdir)
|
||||
|
||||
@ -377,7 +377,7 @@ module Cask
|
||||
|
||||
# @api public
|
||||
def appdir
|
||||
return Cask::APPDIR_PLACEHOLDER if Cask.generating_hash?
|
||||
return HOMEBREW_CASK_APPDIR_PLACEHOLDER if Cask.generating_hash?
|
||||
|
||||
cask.config.appdir
|
||||
end
|
||||
|
||||
@ -50,6 +50,7 @@ module Homebrew
|
||||
FileUtils.mkdir_p directories
|
||||
|
||||
Formulary.enable_factory_cache!
|
||||
Formula.generating_hash!
|
||||
|
||||
tap.formula_names.each do |name|
|
||||
formula = Formulary.factory(name)
|
||||
|
||||
34
Library/Homebrew/extend/api_hashable.rb
Normal file
34
Library/Homebrew/extend/api_hashable.rb
Normal 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
|
||||
@ -30,6 +30,7 @@ require "find"
|
||||
require "utils/spdx"
|
||||
require "extend/on_system"
|
||||
require "api"
|
||||
require "extend/api_hashable"
|
||||
|
||||
# A formula provides instructions and metadata for Homebrew to install a piece
|
||||
# of software. Every Homebrew formula is a {Formula}.
|
||||
@ -69,6 +70,7 @@ class Formula
|
||||
extend Forwardable
|
||||
extend Cachable
|
||||
extend Predicable
|
||||
extend APIHashable
|
||||
|
||||
# The name of this {Formula}.
|
||||
# e.g. `this-formula`
|
||||
@ -1043,7 +1045,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 +2134,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" => {},
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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_REPOSITORY = ENV.fetch("HOMEBREW_LINUX_DEFAULT_REPOSITORY").freeze
|
||||
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_OLDEST_SUPPORTED = ENV.fetch("HOMEBREW_MACOS_OLDEST_SUPPORTED").freeze
|
||||
|
||||
@ -45,6 +45,10 @@ module Homebrew
|
||||
).returns(T.nilable(Array))
|
||||
}
|
||||
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)
|
||||
case T.unsafe(command)
|
||||
when nil
|
||||
@ -156,7 +160,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 +196,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 +324,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 +355,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 +363,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 +376,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 +488,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 +501,101 @@ 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
|
||||
|
||||
# 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
|
||||
|
||||
@ -337,6 +337,8 @@ describe Cask::Cask, :cask do
|
||||
expect(JSON.pretty_generate(h["variations"])).to eq expected_sha256_variations.strip
|
||||
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
|
||||
described_class.generating_hash!
|
||||
expect(described_class).to be_generating_hash
|
||||
|
||||
@ -272,6 +272,11 @@ describe Formulary do
|
||||
"conflicts_with_reasons" => ["it does"],
|
||||
"link_overwrite" => ["bin/abc"],
|
||||
"caveats" => "example caveat string",
|
||||
"service" => {
|
||||
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
|
||||
"run_type" => "immediate",
|
||||
"working_dir" => "/$HOME",
|
||||
},
|
||||
}.merge(extra_items),
|
||||
}
|
||||
end
|
||||
@ -354,6 +359,11 @@ describe Formulary do
|
||||
|
||||
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
|
||||
formula.install
|
||||
end.to raise_error("Cannot build from source from abstract formula.")
|
||||
|
||||
@ -913,4 +913,67 @@ describe Homebrew::Service do
|
||||
expect(command).to eq(["#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd", "test", "macos"])
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user