Merge pull request #9517 from SMillerDev/plist_generator

formula: allow plists to be generated
This commit is contained in:
Mike McQuaid 2021-04-09 13:59:55 +01:00 committed by GitHub
commit 576ec2c9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 367 additions and 6 deletions

View File

@ -14,6 +14,7 @@ require "build_options"
require "formulary"
require "software_spec"
require "livecheck"
require "service"
require "install_renamed"
require "pkg_version"
require "keg"
@ -391,6 +392,11 @@ class Formula
# @see .livecheckable?
delegate livecheckable?: :"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 is autodetected from the URL and/or tag so only needs to be
# declared if it cannot be autodetected correctly.
@ -962,6 +968,13 @@ class Formula
prefix/"#{plist_name}.plist"
end
# The service specification of the software.
def service
return unless service?
Homebrew::Service.new(self, &self.class.service)
end
# @private
delegate plist_manual: :"self.class"
@ -2382,6 +2395,13 @@ class Formula
@livecheckable == true
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_block.present?
end
# The `:startup` attribute set by {.plist_options}.
# @private
attr_reader :plist_startup
@ -2846,6 +2866,21 @@ class Formula
@livecheck.instance_eval(&block)
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)
return @service_block unless block
@service_block = block
end
# Defines whether the {Formula}'s bottle can be used on the given Homebrew
# installation.
#

View File

@ -21,6 +21,7 @@ require "find"
require "utils/spdx"
require "deprecate_disable"
require "unlink"
require "service"
# Installer for a formula.
#
@ -774,7 +775,7 @@ class FormulaInstaller
ohai "Finishing up" if verbose?
install_plist
install_service
keg = Keg.new(formula.prefix)
link(keg)
@ -1009,13 +1010,25 @@ class FormulaInstaller
end
sig { void }
def install_plist
return unless formula.plist
def install_service
if formula.service? && formula.plist
ofail "Formula specified both service and plist"
return
end
formula.plist_path.atomic_write(formula.plist)
formula.plist_path.chmod 0644
plist = if formula.service?
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.mkpath if formula.plist.include? log.to_s
log.mkpath if plist.include? log.to_s
rescue Exception => e # rubocop:disable Lint/RescueException
ofail "Failed to install plist file"
odebug e, e.backtrace

145
Library/Homebrew/service.rb Normal file
View File

@ -0,0 +1,145 @@
# 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
extend Forwardable
RUN_TYPE_IMMEDIATE = "immediate"
RUN_TYPE_INTERVAL = "interval"
RUN_TYPE_CRON = "cron"
# sig { params(formula: Formula).void }
def initialize(formula, &block)
@formula = formula
@run_type = RUN_TYPE_IMMEDIATE
@environment_variables = {}
@service_block = block
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
delegate [:bin, :var, :etc, :opt_bin, :opt_sbin, :opt_prefix] => :@formula
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
instance_eval(&@service_block)
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

View File

@ -218,4 +218,79 @@ describe FormulaInstaller do
formula_installer.caveats
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

View File

@ -701,6 +701,33 @@ describe Formula do
end
end
specify "#service" do
f = formula do
url "https://brew.sh/test-1.0.tbz"
end
f.class.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
expect(f.service).not_to eq(nil)
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).not_to eq(nil)
end
specify "dependencies" do
f1 = formula "f1" do
url "f1-1.0"

View File

@ -0,0 +1,66 @@
# 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) }
describe "#to_plist" do
it "returns valid plist" do
f.class.service 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 = f.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
f.class.service do
run bin/"beanstalkd"
run_type :immediate
end
plist = f.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