Merge pull request #9517 from SMillerDev/plist_generator
formula: allow plists to be generated
This commit is contained in:
		
						commit
						576ec2c9a3
					
				@ -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.
 | 
			
		||||
    #
 | 
			
		||||
 | 
			
		||||
@ -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
									
								
							
							
						
						
									
										145
									
								
								Library/Homebrew/service.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										66
									
								
								Library/Homebrew/test/service_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Library/Homebrew/test/service_spec.rb
									
									
									
									
									
										Normal 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
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user