formula: allow plists to be generated
This commit is contained in:
parent
55223555ba
commit
4427fa283f
@ -14,6 +14,7 @@ require "build_options"
|
|||||||
require "formulary"
|
require "formulary"
|
||||||
require "software_spec"
|
require "software_spec"
|
||||||
require "livecheck"
|
require "livecheck"
|
||||||
|
require "service"
|
||||||
require "install_renamed"
|
require "install_renamed"
|
||||||
require "pkg_version"
|
require "pkg_version"
|
||||||
require "keg"
|
require "keg"
|
||||||
@ -391,6 +392,16 @@ class Formula
|
|||||||
# @see .livecheckable?
|
# @see .livecheckable?
|
||||||
delegate livecheckable?: :"self.class"
|
delegate livecheckable?: :"self.class"
|
||||||
|
|
||||||
|
# The service specification for the software.
|
||||||
|
# @!method service
|
||||||
|
# @see .service=
|
||||||
|
delegate service: :"self.class"
|
||||||
|
|
||||||
|
# Is a service specification defined for the software?
|
||||||
|
# @!method service?
|
||||||
|
# @see .service?
|
||||||
|
delegate service?: :"self.class"
|
||||||
|
|
||||||
# The version for the currently active {SoftwareSpec}.
|
# The version for the currently active {SoftwareSpec}.
|
||||||
# The version is autodetected from the URL and/or tag so only needs to be
|
# The version is autodetected from the URL and/or tag so only needs to be
|
||||||
# declared if it cannot be autodetected correctly.
|
# declared if it cannot be autodetected correctly.
|
||||||
@ -2382,6 +2393,13 @@ class Formula
|
|||||||
@livecheckable == true
|
@livecheckable == true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Whether a service specification is defined or not.
|
||||||
|
# It returns true when a service block is present in the {Formula} and
|
||||||
|
# false otherwise, and is used by service.
|
||||||
|
def service?
|
||||||
|
@service.present?
|
||||||
|
end
|
||||||
|
|
||||||
# The `:startup` attribute set by {.plist_options}.
|
# The `:startup` attribute set by {.plist_options}.
|
||||||
# @private
|
# @private
|
||||||
attr_reader :plist_startup
|
attr_reader :plist_startup
|
||||||
@ -2846,6 +2864,22 @@ class Formula
|
|||||||
@livecheck.instance_eval(&block)
|
@livecheck.instance_eval(&block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @!attribute [w] service
|
||||||
|
# Service can be used to define services.
|
||||||
|
# This method evaluates the DSL specified in the service block of the
|
||||||
|
# {Formula} (if it exists) and sets the instance variables of a Service
|
||||||
|
# object accordingly. This is used by `brew install` to generate a plist.
|
||||||
|
#
|
||||||
|
# <pre>service do
|
||||||
|
# run [opt_bin/"foo"]
|
||||||
|
# end</pre>
|
||||||
|
def service(&block)
|
||||||
|
@service ||= Homebrew::Service.new(self)
|
||||||
|
return @service unless block
|
||||||
|
|
||||||
|
@service.instance_eval(&block)
|
||||||
|
end
|
||||||
|
|
||||||
# Defines whether the {Formula}'s bottle can be used on the given Homebrew
|
# Defines whether the {Formula}'s bottle can be used on the given Homebrew
|
||||||
# installation.
|
# installation.
|
||||||
#
|
#
|
||||||
|
|||||||
@ -21,6 +21,7 @@ require "find"
|
|||||||
require "utils/spdx"
|
require "utils/spdx"
|
||||||
require "deprecate_disable"
|
require "deprecate_disable"
|
||||||
require "unlink"
|
require "unlink"
|
||||||
|
require "service"
|
||||||
|
|
||||||
# Installer for a formula.
|
# Installer for a formula.
|
||||||
#
|
#
|
||||||
@ -774,7 +775,7 @@ class FormulaInstaller
|
|||||||
|
|
||||||
ohai "Finishing up" if verbose?
|
ohai "Finishing up" if verbose?
|
||||||
|
|
||||||
install_plist
|
install_service
|
||||||
|
|
||||||
keg = Keg.new(formula.prefix)
|
keg = Keg.new(formula.prefix)
|
||||||
link(keg)
|
link(keg)
|
||||||
@ -1009,13 +1010,25 @@ class FormulaInstaller
|
|||||||
end
|
end
|
||||||
|
|
||||||
sig { void }
|
sig { void }
|
||||||
def install_plist
|
def install_service
|
||||||
return unless formula.plist
|
if formula.service? && formula.plist
|
||||||
|
ofail "Formula specified both service and plist"
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
formula.plist_path.atomic_write(formula.plist)
|
plist = if formula.service?
|
||||||
formula.plist_path.chmod 0644
|
formula.service.to_plist
|
||||||
|
elsif formula.plist
|
||||||
|
formula.plist
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless plist
|
||||||
|
|
||||||
|
plist_path = formula.plist_path
|
||||||
|
plist_path.atomic_write(plist)
|
||||||
|
plist_path.chmod 0644
|
||||||
log = formula.var/"log"
|
log = formula.var/"log"
|
||||||
log.mkpath if formula.plist.include? log.to_s
|
log.mkpath if plist.include? log.to_s
|
||||||
rescue Exception => e # rubocop:disable Lint/RescueException
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
||||||
ofail "Failed to install plist file"
|
ofail "Failed to install plist file"
|
||||||
odebug e, e.backtrace
|
odebug e, e.backtrace
|
||||||
|
|||||||
181
Library/Homebrew/service.rb
Normal file
181
Library/Homebrew/service.rb
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# typed: true
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Homebrew
|
||||||
|
# The {Service} class implements the DSL methods used in a formula's
|
||||||
|
# `service` block and stores related instance variables. Most of these methods
|
||||||
|
# also return the related instance variable when no argument is provided.
|
||||||
|
class Service
|
||||||
|
extend T::Sig
|
||||||
|
|
||||||
|
RUN_TYPE_IMMEDIATE = "immediate"
|
||||||
|
RUN_TYPE_INTERVAL = "interval"
|
||||||
|
RUN_TYPE_CRON = "cron"
|
||||||
|
|
||||||
|
# sig { params(formula: Formula).void }
|
||||||
|
def initialize(formula)
|
||||||
|
@formula = formula
|
||||||
|
@run_type = RUN_TYPE_IMMEDIATE
|
||||||
|
@environment_variables = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(command: T.nilable(T.any(T::Array[String], String, Pathname))).returns(T.nilable(Array)) }
|
||||||
|
def run(command = nil)
|
||||||
|
case T.unsafe(command)
|
||||||
|
when nil
|
||||||
|
@run
|
||||||
|
when String, Pathname
|
||||||
|
@run = [command]
|
||||||
|
when Array
|
||||||
|
@run = command
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#run expects an Array"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
||||||
|
def working_dir(path = nil)
|
||||||
|
case T.unsafe(path)
|
||||||
|
when nil
|
||||||
|
@working_dir
|
||||||
|
when String, Pathname
|
||||||
|
@working_dir = path.to_s
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#working_dir expects a String"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
||||||
|
def log_path(path = nil)
|
||||||
|
case T.unsafe(path)
|
||||||
|
when nil
|
||||||
|
@log_path
|
||||||
|
when String, Pathname
|
||||||
|
@log_path = path.to_s
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#log_path expects a String"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(path: T.nilable(T.any(String, Pathname))).returns(T.nilable(String)) }
|
||||||
|
def error_log_path(path = nil)
|
||||||
|
case T.unsafe(path)
|
||||||
|
when nil
|
||||||
|
@error_log_path
|
||||||
|
when String, Pathname
|
||||||
|
@error_log_path = path.to_s
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#error_log_path expects a String"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
|
||||||
|
def keep_alive(value = nil)
|
||||||
|
case T.unsafe(value)
|
||||||
|
when nil
|
||||||
|
@keep_alive
|
||||||
|
when true, false
|
||||||
|
@keep_alive = value
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#keep_alive expects a Boolean"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(type: T.nilable(T.any(String, Symbol))).returns(T.nilable(String)) }
|
||||||
|
def run_type(type = nil)
|
||||||
|
case T.unsafe(type)
|
||||||
|
when nil
|
||||||
|
@run_type
|
||||||
|
when "immediate", :immediate
|
||||||
|
@run_type = type.to_s
|
||||||
|
when RUN_TYPE_INTERVAL, RUN_TYPE_CRON
|
||||||
|
raise TypeError, "Service#run_type does not support timers"
|
||||||
|
when String
|
||||||
|
raise TypeError, "Service#run_type allows: '#{RUN_TYPE_IMMEDIATE}'/'#{RUN_TYPE_INTERVAL}'/'#{RUN_TYPE_CRON}'"
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#run_type expects a string"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { params(variables: T.nilable(T::Hash[String, String])).returns(T.nilable(T::Hash[String, String])) }
|
||||||
|
def environment_variables(variables = {})
|
||||||
|
case T.unsafe(variables)
|
||||||
|
when nil
|
||||||
|
@environment_variables
|
||||||
|
when Hash
|
||||||
|
@environment_variables = variables
|
||||||
|
else
|
||||||
|
raise TypeError, "Service#environment_variables expects a hash"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# The directory where the formula's variable files should be installed.
|
||||||
|
# This directory is not inside the `HOMEBREW_CELLAR` so it persists
|
||||||
|
# across upgrades.
|
||||||
|
sig { returns(Pathname) }
|
||||||
|
def var
|
||||||
|
HOMEBREW_PREFIX/"var"
|
||||||
|
end
|
||||||
|
|
||||||
|
# The directory where the formula's configuration files should be installed.
|
||||||
|
# Anything using `etc.install` will not overwrite other files on e.g. upgrades
|
||||||
|
# but will write a new file named `*.default`.
|
||||||
|
# This directory is not inside the `HOMEBREW_CELLAR` so it persists
|
||||||
|
# across upgrades.
|
||||||
|
sig { returns(Pathname) }
|
||||||
|
def etc
|
||||||
|
HOMEBREW_PREFIX/"etc"
|
||||||
|
end
|
||||||
|
|
||||||
|
# The directory where the formula's binaries should be installed.
|
||||||
|
# This is symlinked into `HOMEBREW_PREFIX` after installation or with
|
||||||
|
# `brew link` for formulae that are not keg-only.
|
||||||
|
sig { returns(Pathname) }
|
||||||
|
def opt_bin
|
||||||
|
opt_prefix/"bin"
|
||||||
|
end
|
||||||
|
|
||||||
|
# The directory where the formula's binaries should be installed.
|
||||||
|
# This is symlinked into `HOMEBREW_PREFIX` after installation or with
|
||||||
|
# `brew link` for formulae that are not keg-only.
|
||||||
|
sig { returns(Pathname) }
|
||||||
|
def opt_sbin
|
||||||
|
opt_prefix/"sbin"
|
||||||
|
end
|
||||||
|
|
||||||
|
# A stable path for this formula, when installed. Contains the formula name
|
||||||
|
# but no version number. Only the active version will be linked here if
|
||||||
|
# multiple versions are installed.
|
||||||
|
sig { returns(Pathname) }
|
||||||
|
def opt_prefix
|
||||||
|
HOMEBREW_PREFIX/"opt/#{@formula.name}"
|
||||||
|
end
|
||||||
|
|
||||||
|
sig { returns(String) }
|
||||||
|
def std_service_path_env
|
||||||
|
"#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a `String` plist.
|
||||||
|
# @return [String]
|
||||||
|
sig { returns(String) }
|
||||||
|
def to_plist
|
||||||
|
clean_command = @run.select {|i| i.is_a?(Pathname)}
|
||||||
|
.map(&:to_s)
|
||||||
|
|
||||||
|
base = {
|
||||||
|
Label: @formula.plist_name,
|
||||||
|
RunAtLoad: @run_type == RUN_TYPE_IMMEDIATE,
|
||||||
|
ProgramArguments: clean_command,
|
||||||
|
}
|
||||||
|
|
||||||
|
base[:KeepAlive] = @keep_alive if @keep_alive == true
|
||||||
|
base[:WorkingDirectory] = @working_dir if @working_dir.present?
|
||||||
|
base[:StandardOutPath] = @log_path if @log_path.present?
|
||||||
|
base[:StandardErrorPath] = @error_log_path if @error_log_path.present?
|
||||||
|
base[:EnvironmentVariables] = @environment_variables unless @environment_variables.empty?
|
||||||
|
|
||||||
|
base.to_plist
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -218,4 +218,79 @@ describe FormulaInstaller do
|
|||||||
formula_installer.caveats
|
formula_installer.caveats
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#install_service" do
|
||||||
|
it "works if plist is set" do
|
||||||
|
formula = Testball.new
|
||||||
|
path = formula.plist_path
|
||||||
|
formula.prefix.mkpath
|
||||||
|
|
||||||
|
expect(formula).to receive(:plist).twice.and_return("PLIST")
|
||||||
|
expect(formula).to receive(:plist_path).and_call_original
|
||||||
|
|
||||||
|
installer = described_class.new(formula)
|
||||||
|
expect {
|
||||||
|
installer.install_service
|
||||||
|
}.not_to output(/Error: Failed to install plist file/).to_stderr
|
||||||
|
|
||||||
|
expect(path).to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it "works if service is set" do
|
||||||
|
formula = Testball.new
|
||||||
|
path = formula.plist_path
|
||||||
|
service = Homebrew::Service.new(formula)
|
||||||
|
formula.prefix.mkpath
|
||||||
|
|
||||||
|
expect(formula).to receive(:plist).and_return(nil)
|
||||||
|
expect(formula).to receive(:service?).twice.and_return(true)
|
||||||
|
expect(formula).to receive(:service).and_return(service)
|
||||||
|
expect(formula).to receive(:plist_path).and_call_original
|
||||||
|
|
||||||
|
expect(service).to receive(:to_plist).and_return("plist")
|
||||||
|
|
||||||
|
installer = described_class.new(formula)
|
||||||
|
expect {
|
||||||
|
installer.install_service
|
||||||
|
}.not_to output(/Error: Failed to install plist file/).to_stderr
|
||||||
|
|
||||||
|
expect(path).to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns without definition" do
|
||||||
|
formula = Testball.new
|
||||||
|
path = formula.plist_path
|
||||||
|
formula.prefix.mkpath
|
||||||
|
|
||||||
|
expect(formula).to receive(:plist).and_return(nil)
|
||||||
|
expect(formula).to receive(:service?).twice.and_return(nil)
|
||||||
|
expect(formula).not_to receive(:plist_path)
|
||||||
|
|
||||||
|
installer = described_class.new(formula)
|
||||||
|
expect {
|
||||||
|
installer.install_service
|
||||||
|
}.not_to output(/Error: Failed to install plist file/).to_stderr
|
||||||
|
|
||||||
|
expect(path).not_to exist
|
||||||
|
end
|
||||||
|
|
||||||
|
it "errors with duplicate definition" do
|
||||||
|
formula = Testball.new
|
||||||
|
path = formula.plist_path
|
||||||
|
formula.prefix.mkpath
|
||||||
|
|
||||||
|
expect(formula).to receive(:plist).and_return("plist")
|
||||||
|
expect(formula).to receive(:service?).and_return(true)
|
||||||
|
expect(formula).not_to receive(:service)
|
||||||
|
expect(formula).not_to receive(:plist_path)
|
||||||
|
|
||||||
|
installer = described_class.new(formula)
|
||||||
|
expect {
|
||||||
|
installer.install_service
|
||||||
|
}.to output("Error: Formula specified both service and plist\n").to_stderr
|
||||||
|
|
||||||
|
expect(Homebrew).to have_failed
|
||||||
|
expect(path).not_to exist
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -701,6 +701,38 @@ describe Formula do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
specify "#service" do
|
||||||
|
f = formula do
|
||||||
|
url "https://brew.sh/test-1.0.tbz"
|
||||||
|
service do
|
||||||
|
run [opt_bin/"beanstalkd"]
|
||||||
|
run_type :immediate
|
||||||
|
error_log_path var/"log/beanstalkd.error.log"
|
||||||
|
log_path var/"log/beanstalkd.log"
|
||||||
|
working_dir var
|
||||||
|
keep_alive true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(f.service.run).to eq([HOMEBREW_PREFIX/"opt/bin/beanstalkd"])
|
||||||
|
expect(f.service.error_log_path).to eq("#{HOMEBREW_PREFIX}/var/log/beanstalkd.error.log")
|
||||||
|
expect(f.service.log_path).to eq("#{HOMEBREW_PREFIX}/var/log/beanstalkd.log")
|
||||||
|
expect(f.service.working_dir).to eq("#{HOMEBREW_PREFIX}/var")
|
||||||
|
expect(f.service.keep_alive).to eq(true)
|
||||||
|
expect(f.service.run_type).to eq("immediate")
|
||||||
|
end
|
||||||
|
|
||||||
|
specify "service uses simple run" do
|
||||||
|
f = formula do
|
||||||
|
url "https://brew.sh/test-1.0.tbz"
|
||||||
|
service do
|
||||||
|
run opt_bin/"beanstalkd"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(f.service.run).to eq([HOMEBREW_PREFIX/"opt/bin/beanstalkd"])
|
||||||
|
end
|
||||||
|
|
||||||
specify "dependencies" do
|
specify "dependencies" do
|
||||||
f1 = formula "f1" do
|
f1 = formula "f1" do
|
||||||
url "f1-1.0"
|
url "f1-1.0"
|
||||||
|
|||||||
68
Library/Homebrew/test/service_spec.rb
Normal file
68
Library/Homebrew/test/service_spec.rb
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# typed: false
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "formula"
|
||||||
|
require "service"
|
||||||
|
|
||||||
|
describe Homebrew::Service do
|
||||||
|
let(:klass) do
|
||||||
|
Class.new(Formula) do
|
||||||
|
url "https://brew.sh/test-1.0.tbz"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
let(:name) { "formula_name" }
|
||||||
|
let(:path) { Formulary.core_path(name) }
|
||||||
|
let(:spec) { :stable }
|
||||||
|
let(:f) { klass.new(name, path, spec) }
|
||||||
|
|
||||||
|
let(:service) { described_class.new(f) }
|
||||||
|
|
||||||
|
describe "#to_plist" do
|
||||||
|
it "returns valid plist" do
|
||||||
|
service.instance_eval do
|
||||||
|
run [opt_bin/"beanstalkd"]
|
||||||
|
run_type :immediate
|
||||||
|
environment_variables PATH: std_service_path_env
|
||||||
|
error_log_path var/"log/beanstalkd.error.log"
|
||||||
|
log_path var/"log/beanstalkd.log"
|
||||||
|
working_dir var
|
||||||
|
keep_alive true
|
||||||
|
end
|
||||||
|
|
||||||
|
plist = service.to_plist
|
||||||
|
expect(plist).to include("<key>Label</key>")
|
||||||
|
expect(plist).to include("<string>homebrew.mxcl.#{name}</string>")
|
||||||
|
expect(plist).to include("<key>KeepAlive</key>")
|
||||||
|
expect(plist).to include("<key>RunAtLoad</key>")
|
||||||
|
expect(plist).to include("<key>ProgramArguments</key>")
|
||||||
|
expect(plist).to include("<string>#{HOMEBREW_PREFIX}/opt/#{name}/bin/beanstalkd</string>")
|
||||||
|
expect(plist).to include("<key>WorkingDirectory</key>")
|
||||||
|
expect(plist).to include("<string>#{HOMEBREW_PREFIX}/var</string>")
|
||||||
|
expect(plist).to include("<key>StandardOutPath</key>")
|
||||||
|
expect(plist).to include("<string>#{HOMEBREW_PREFIX}/var/log/beanstalkd.log</string>")
|
||||||
|
expect(plist).to include("<key>StandardErrorPath</key>")
|
||||||
|
expect(plist).to include("<string>#{HOMEBREW_PREFIX}/var/log/beanstalkd.error.log</string>")
|
||||||
|
expect(plist).to include("<key>EnvironmentVariables</key>")
|
||||||
|
expect(plist).to include("<key>PATH</key>")
|
||||||
|
expect(plist).to include("<string>#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns valid partial plist" do
|
||||||
|
service.instance_eval do
|
||||||
|
run ["#{HOMEBREW_PREFIX}/bin/beanstalkd"]
|
||||||
|
run_type :immediate
|
||||||
|
end
|
||||||
|
|
||||||
|
plist = service.to_plist
|
||||||
|
expect(plist).to include("<string>homebrew.mxcl.#{name}</string>")
|
||||||
|
expect(plist).to include("<key>Label</key>")
|
||||||
|
expect(plist).not_to include("<key>KeepAlive</key>")
|
||||||
|
expect(plist).to include("<key>RunAtLoad</key>")
|
||||||
|
expect(plist).to include("<key>ProgramArguments</key>")
|
||||||
|
expect(plist).not_to include("<key>WorkingDirectory</key>")
|
||||||
|
expect(plist).not_to include("<key>StandardOutPath</key>")
|
||||||
|
expect(plist).not_to include("<key>StandardErrorPath</key>")
|
||||||
|
expect(plist).not_to include("<key>EnvironmentVariables</key>")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
x
Reference in New Issue
Block a user