Merge pull request #15396 from apainintheneck/custom-service-name

Custom service name
This commit is contained in:
Kevin 2023-05-17 22:09:31 -07:00 committed by GitHub
commit 946478aed4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 288 additions and 88 deletions

View File

@ -2,6 +2,7 @@
# frozen_string_literal: true
require "language/python"
require "utils/service"
# A formula's caveats.
#
@ -153,33 +154,32 @@ class Caveats
end
def service_caveats
return if !formula.plist && !formula.service? && !keg&.plist_installed?
return if formula.service? && formula.service.command.blank?
return if !formula.plist && !formula.service? && !Utils::Service.installed?(formula) && !keg&.plist_installed?
return if formula.service? && !formula.service.command? && !Utils::Service.installed?(formula)
s = []
command = if formula.service?
command = if formula.service.command?
formula.service.manual_command
else
formula.plist_manual
end
return <<~EOS if !which("launchctl") && formula.plist
return <<~EOS if !Utils::Service.launchctl? && formula.plist
#{Formatter.warning("Warning:")} #{formula.name} provides a launchd plist which can only be used on macOS!
You can manually execute the service instead with:
#{command}
EOS
# Brew services only works with these two tools
return <<~EOS if !which("systemctl") && !which("launchctl") && formula.service?
return <<~EOS if !Utils::Service.systemctl? && !Utils::Service.launchctl? && formula.service.command?
#{Formatter.warning("Warning:")} #{formula.name} provides a service which can only be used on macOS or systemd!
You can manually execute the service instead with:
#{command}
EOS
is_running_service = formula.service? && quiet_system("ps aux | grep #{formula.service.command&.first}")
startup = formula.service&.requires_root? || formula.plist_startup
if is_running_service || (formula.plist && quiet_system("/bin/launchctl list #{formula.plist_name} &>/dev/null"))
startup = formula.service.requires_root? || formula.plist_startup
if Utils::Service.running?(formula)
s << "To restart #{formula.full_name} after an upgrade:"
s << " #{startup ? "sudo " : ""}brew services restart #{formula.full_name}"
elsif startup
@ -190,7 +190,7 @@ class Caveats
s << " brew services start #{formula.full_name}"
end
if formula.plist_manual || formula.service?
if formula.plist_manual || formula.service.command?
s << "Or, if you don't want/need a background service you can just run:"
s << " #{command}"
end

View File

@ -1022,13 +1022,13 @@ class Formula
# The generated launchd {.plist} service name.
sig { returns(String) }
def plist_name
"homebrew.mxcl.#{name}"
service.plist_name
end
# The generated service name.
sig { returns(String) }
def service_name
"homebrew.#{name}"
service.service_name
end
# The generated launchd {.plist} file path.
@ -1058,8 +1058,6 @@ class Formula
# The service specification of the software.
def service
return unless service?
@service ||= Homebrew::Service.new(self, &self.class.service)
end
@ -2176,7 +2174,7 @@ class Formula
"disabled" => disabled?,
"disable_date" => disable_date,
"disable_reason" => disable_reason,
"service" => service&.serialize,
"service" => (service.serialize if service?),
"tap_git_head" => tap_git_head,
"ruby_source_path" => ruby_source_path,
"ruby_source_checksum" => {},

View File

@ -288,7 +288,7 @@ module FormulaCellarChecks
def check_service_command(formula)
return unless formula.prefix.directory?
return unless formula.service?
return if formula.service.command.blank?
return unless formula.service.command?
"Service command does not exist" unless File.exist?(formula.service.command.first)
end

View File

@ -1040,7 +1040,7 @@ on_request: installed_on_request?, options: options)
return
end
if formula.service? && formula.service.command.present?
if formula.service? && formula.service.command?
service_path = formula.systemd_service_path
service_path.atomic_write(formula.service.to_systemd_unit)
service_path.chmod 0644
@ -1052,7 +1052,7 @@ on_request: installed_on_request?, options: options)
end
end
service = if formula.service? && formula.service.command.present?
service = if formula.service? && formula.service.command?
formula.service.to_plist
elsif formula.plist
formula.plist

View File

@ -260,14 +260,22 @@ module Formulary
if (service_hash = json_formula["service"])
service_hash = Homebrew::Service.deserialize(service_hash)
run_params = service_hash.delete(:run)
service do
T.bind(self, Homebrew::Service)
if run_params.is_a?(Hash)
if (run_params = service_hash.delete(:run))
case run_params
when Hash
run(**run_params)
else
when Array, String
run run_params
end
end
if (name_params = service_hash.delete(:name))
name(**name_params)
end
service_hash.each do |key, arg|
public_send(key, arg)
end

View File

@ -21,15 +21,35 @@ module RuboCop
share: :opt_share,
}.freeze
# At least one of these methods must be defined in a service block.
REQUIRED_METHOD_CALLS = [:run, :name].freeze
def audit_formula(_node, _class_node, _parent_class_node, body_node)
service_node = find_block(body_node, :service)
return if service_node.blank?
method_calls = service_node.each_descendant(:send).group_by(&:method_name)
method_calls.delete(:service)
# NOTE: Solving the first problem here might solve the second one too
# so we don't show both of them at the same time.
if (method_calls.keys & REQUIRED_METHOD_CALLS).empty?
offending_node(service_node)
problem "Service blocks require `run` or `name` to be defined."
elsif !method_calls.key?(:run)
other_method_calls = method_calls.keys - [:name]
if other_method_calls.any?
offending_node(service_node)
problem "`run` must be defined to use methods other than `name` like #{other_method_calls}."
end
end
# This check ensures that cellar paths like `bin` are not referenced
# because their `opt_` variants are more portable and work with the
# API.
# because their `opt_` variants are more portable and work with the API.
CELLAR_PATH_AUDIT_CORRECTIONS.each do |path, opt_path|
find_every_method_call_by_name(service_node, path).each do |node|
next unless method_calls.key?(path)
method_calls.fetch(path).each do |node|
offending_node(node)
problem "Use `#{opt_path}` instead of `#{path}` in service blocks." do |corrector|
corrector.replace(node.source_range, opt_path)

View File

@ -28,7 +28,7 @@ module Homebrew
@run_type = RUN_TYPE_IMMEDIATE
@run_at_load = true
@environment_variables = {}
@service_block = block
instance_eval(&block) if block
end
sig { returns(Formula) }
@ -36,6 +36,34 @@ module Homebrew
@formula
end
sig { returns(String) }
def default_plist_name
"homebrew.mxcl.#{@formula.name}"
end
sig { returns(String) }
def plist_name
@plist_name ||= default_plist_name
end
sig { returns(String) }
def default_service_name
"homebrew.#{@formula.name}"
end
sig { returns(String) }
def service_name
@service_name ||= default_service_name
end
sig { params(macos: T.nilable(String), linux: T.nilable(String)).void }
def name(macos: nil, linux: nil)
raise TypeError, "Service#name expects at least one String" if [macos, linux].none?(String)
@plist_name = macos if macos
@service_name = linux if linux
end
sig {
params(
command: T.nilable(T.any(T::Array[String], String, Pathname)),
@ -162,7 +190,6 @@ module Homebrew
# @return [Boolean]
sig { returns(T::Boolean) }
def requires_root?
eval_service_block
@require_root.present? && @require_root == true
end
@ -198,7 +225,6 @@ module Homebrew
# @return [Boolean]
sig { returns(T::Boolean) }
def keep_alive?
eval_service_block
@keep_alive.present? && @keep_alive[:always] != false
end
@ -357,20 +383,22 @@ module Homebrew
sig { returns(T.nilable(T::Array[String])) }
def command
eval_service_block
@run&.map(&:to_s)
end
sig { returns(T::Boolean) }
def command?
@run.present?
end
# Returns the `String` command to run manually instead of the service.
# @return [String]
sig { returns(String) }
def manual_command
eval_service_block
vars = @environment_variables.except(:PATH)
.map { |k, v| "#{k}=\"#{v}\"" }
cmd = command
out = vars + cmd if cmd.present?
out = vars + command if command?
out.join(" ")
end
@ -378,7 +406,6 @@ module Homebrew
# @return [Boolean]
sig { returns(T::Boolean) }
def timed?
eval_service_block
@run_type == RUN_TYPE_CRON || @run_type == RUN_TYPE_INTERVAL
end
@ -388,7 +415,7 @@ module Homebrew
def to_plist
# command needs to be first because it initializes all other values
base = {
Label: @formula.plist_name,
Label: plist_name,
ProgramArguments: command,
RunAtLoad: @run_at_load == true,
}
@ -487,10 +514,9 @@ module Homebrew
WantedBy=timers.target
[Timer]
Unit=#{@formula.service_name}
Unit=#{service_name}
EOS
eval_service_block
options = []
options << "Persistent=true" if @run_type == RUN_TYPE_CRON
options << "OnUnitActiveSec=#{@interval}" if @run_type == RUN_TYPE_INTERVAL
@ -504,19 +530,17 @@ 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
name_params = {
macos: (plist_name if plist_name != default_plist_name),
linux: (service_name if service_name != default_service_name),
}.compact
unless command?
return name_params.blank? ? {} : { name: name_params }
end
cron_string = if @cron.present?
[:Minute, :Hour, :Day, :Month, :Weekday]
@ -527,6 +551,7 @@ module Homebrew
sockets_string = "#{@sockets[:type]}://#{@sockets[:host]}:#{@sockets[:port]}" if @sockets.present?
{
name: name_params.presence,
run: @run_params,
run_type: @run_type,
interval: @interval,
@ -551,6 +576,12 @@ module Homebrew
sig { params(api_hash: Hash).returns(Hash) }
def self.deserialize(api_hash)
hash = {}
hash[:name] = api_hash["name"].transform_keys(&:to_sym) if api_hash.key?("name")
# The service hash might not have a "run" command if it only documents
# an existing service file with the "name" command.
return hash unless api_hash.key?("run")
hash[:run] =
case api_hash["run"]
when String

View File

@ -32,6 +32,10 @@ describe Caveats do
describe "#caveats" do
context "when f.plist is not nil", :needs_macos do
before do
allow(Utils::Service).to receive(:launchctl?).and_return(true)
end
it "prints error when no launchd is present" do
f = formula do
url "foo-1.0"
@ -39,7 +43,7 @@ describe Caveats do
"plist_test.plist"
end
end
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(nil)
expect(Utils::Service).to receive(:launchctl?).once.and_return(false)
expect(described_class.new(f).caveats).to include("provides a launchd plist which can only be used on macOS!")
end
@ -50,7 +54,7 @@ describe Caveats do
"plist_test.plist"
end
end
expect(described_class.new(f).caveats).to include("login")
expect(described_class.new(f).caveats).to include("restart at login")
end
it "gives information about service" do
@ -82,12 +86,25 @@ describe Caveats do
expect(caveats).to include("WARNING:")
expect(caveats).to include("tmux")
end
# @todo This should get deprecated and the service block `plist_name` method should get used instead.
it "prints info when there are custom service files" do
f = formula do
url "foo-1.0"
def plist_name
"custom.mxcl.foo"
end
end
expect(Utils::Service).to receive(:installed?).with(f).once.and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("restart at login")
end
end
context "when f.service is not nil" do
context "when service block is defined" do
before do
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(true)
allow_any_instance_of(Object).to receive(:which).with("systemctl").and_return(true)
allow(Utils::Service).to receive(:launchctl?).and_return(true)
allow(Utils::Service).to receive(:systemctl?).and_return(true)
end
it "prints warning when no service deamon is found" do
@ -97,9 +114,8 @@ describe Caveats do
run [bin/"cmd"]
end
end
allow_any_instance_of(Object).to receive(:which).with("launchctl").and_return(nil)
allow_any_instance_of(Object).to receive(:which).with("systemctl").and_return(nil)
expect(Utils::Service).to receive(:launchctl?).twice.and_return(false)
expect(Utils::Service).to receive(:systemctl?).once.and_return(false)
expect(described_class.new(f).caveats).to include("service which can only be used on macOS or systemd!")
end
@ -111,9 +127,7 @@ describe Caveats do
require_root true
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
allow(Homebrew).to receive(:_system).and_return(true)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("startup")
end
@ -124,10 +138,8 @@ describe Caveats do
run [bin/"cmd"]
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
allow(Homebrew).to receive(:_system).and_return(true)
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(described_class.new(f).caveats).to include("login")
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("restart at login")
end
it "gives information about require_root restarting services after upgrade" do
@ -138,10 +150,8 @@ describe Caveats do
require_root true
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true)
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(true)
expect(f_obj.caveats).to include(" sudo brew services restart #{f.full_name}")
end
@ -152,10 +162,8 @@ describe Caveats do
run [bin/"cmd"]
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true)
expect(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(true)
expect(f_obj.caveats).to include(" brew services restart #{f.full_name}")
end
@ -167,10 +175,8 @@ describe Caveats do
require_root true
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(f_obj.caveats).to include(" sudo brew services start #{f.full_name}")
end
@ -181,10 +187,8 @@ describe Caveats do
run [bin/"cmd"]
end
end
cmd = "#{HOMEBREW_CELLAR}/formula_name/1.0/bin/cmd"
f_obj = described_class.new(f)
allow(Homebrew).to receive(:_system).and_return(true)
allow(Homebrew).to receive(:_system).with("ps aux | grep #{cmd}").and_return(false)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(f_obj.caveats).to include(" brew services start #{f.full_name}")
end
@ -202,6 +206,18 @@ describe Caveats do
expect(caveats).to include("if you don't want/need a background service")
expect(caveats).to include("VAR=\"foo\" #{cmd} start")
end
it "prints info when there are custom service files" do
f = formula do
url "foo-1.0"
service do
name macos: "custom.mxcl.foo", linux: "custom.foo"
end
end
expect(Utils::Service).to receive(:installed?).with(f).once.and_return(true)
expect(Utils::Service).to receive(:running?).with(f).once.and_return(false)
expect(described_class.new(f).caveats).to include("restart at login")
end
end
context "when f.keg_only is not nil" do

View File

@ -218,21 +218,21 @@ describe FormulaInstaller do
it "works if service is set" do
formula = Testball.new
service = Homebrew::Service.new(formula)
launchd_service_path = formula.launchd_service_path
service_path = formula.systemd_service_path
service = Homebrew::Service.new(formula)
formula.opt_prefix.mkpath
expect(formula).to receive(:plist).and_return(nil)
expect(formula).to receive(:service?).exactly(3).and_return(true)
expect(formula).to receive(:service).exactly(5).and_return(service)
expect(formula).to receive(:service).exactly(7).and_return(service)
expect(formula).to receive(:launchd_service_path).and_call_original
expect(formula).to receive(:systemd_service_path).and_call_original
expect(service).to receive(:timed?).and_return(false)
expect(service).to receive(:command?).exactly(2).and_return(true)
expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:to_systemd_unit).and_return("unit")
expect(service).to receive(:command).exactly(2).and_return("/bin/sh")
installer = described_class.new(formula)
expect do
@ -245,24 +245,24 @@ describe FormulaInstaller do
it "works if timed service is set" do
formula = Testball.new
service = Homebrew::Service.new(formula)
launchd_service_path = formula.launchd_service_path
service_path = formula.systemd_service_path
timer_path = formula.systemd_timer_path
service = Homebrew::Service.new(formula)
formula.opt_prefix.mkpath
expect(formula).to receive(:plist).and_return(nil)
expect(formula).to receive(:service?).exactly(3).and_return(true)
expect(formula).to receive(:service).exactly(6).and_return(service)
expect(formula).to receive(:service).exactly(9).and_return(service)
expect(formula).to receive(:launchd_service_path).and_call_original
expect(formula).to receive(:systemd_service_path).and_call_original
expect(formula).to receive(:systemd_timer_path).and_call_original
expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:timed?).and_return(true)
expect(service).to receive(:command?).exactly(2).and_return(true)
expect(service).to receive(:to_plist).and_return("plist")
expect(service).to receive(:to_systemd_unit).and_return("unit")
expect(service).to receive(:to_systemd_timer).and_return("timer")
expect(service).to receive(:command).exactly(2).and_return("/bin/sh")
installer = described_class.new(formula)
expect do

View File

@ -701,7 +701,7 @@ describe Formula do
url "https://brew.sh/test-1.0.tbz"
end
expect(f.service).to be_nil
expect(f.service.serialize).to eq({})
end
specify "service complicated" do
@ -717,7 +717,8 @@ describe Formula do
keep_alive true
end
end
expect(f.service).not_to be_nil
expect(f.service.serialize.keys)
.to contain_exactly(:run, :run_type, :error_log_path, :log_path, :working_dir, :keep_alive)
end
specify "service uses simple run" do
@ -728,7 +729,20 @@ describe Formula do
end
end
expect(f.service).not_to be_nil
expect(f.service.serialize.keys).to contain_exactly(:run, :run_type)
end
specify "service with only custom names" do
f = formula do
url "https://brew.sh/test-1.0.tbz"
service do
name macos: "custom.macos.beanstalkd", linux: "custom.linux.beanstalkd"
end
end
expect(f.plist_name).to eq("custom.macos.beanstalkd")
expect(f.service_name).to eq("custom.linux.beanstalkd")
expect(f.service.serialize.keys).to contain_exactly(:name)
end
specify "service helpers return data" do

View File

@ -272,6 +272,7 @@ describe Formulary do
"link_overwrite" => ["bin/abc"],
"caveats" => "example caveat string\n/$HOME\n$HOMEBREW_PREFIX",
"service" => {
"name" => { macos: "custom.launchd.name", linux: "custom.systemd.name" },
"run" => ["$HOMEBREW_PREFIX/opt/formula_name/bin/beanstalkd", "test"],
"run_type" => "immediate",
"working_dir" => "/$HOME",
@ -362,6 +363,8 @@ describe Formulary do
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(formula.plist_name).to eq("custom.launchd.name")
expect(formula.service_name).to eq("custom.systemd.name")
expect do
formula.install

View File

@ -5,6 +5,46 @@ require "rubocops/service"
describe RuboCop::Cop::FormulaAudit::Service do
subject(:cop) { described_class.new }
it "reports offenses when a service block is missing a required command" do
expect_offense(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
^^^^^^^^^^ FormulaAudit/Service: Service blocks require `run` or `name` to be defined.
run_type :cron
working_dir "/tmp/example"
end
end
RUBY
end
it "reports no offenses when a service block only includes custom names" do
expect_no_offenses(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
name macos: "custom.mcxl.foo", linux: "custom.foo"
end
end
RUBY
end
it "reports offenses when a service block includes more than custom names and no run command" do
expect_offense(<<~RUBY)
class Foo < Formula
url "https://brew.sh/foo-1.0.tgz"
service do
^^^^^^^^^^ FormulaAudit/Service: `run` must be defined to use methods other than `name` like [:working_dir].
name macos: "custom.mcxl.foo", linux: "custom.foo"
working_dir "/tmp/example"
end
end
RUBY
end
it "reports offenses when a formula's service block uses cellar paths" do
expect_offense(<<~RUBY)
class Foo < Formula
@ -31,7 +71,7 @@ describe RuboCop::Cop::FormulaAudit::Service do
RUBY
end
it "reports no offenses when a formula's service block only uses opt paths" do
it "reports no offenses when a service block only uses opt paths" do
expect_no_offenses(<<~RUBY)
class Bin < Formula
url "https://brew.sh/foo-1.0.tgz"

View File

@ -949,6 +949,10 @@ describe Homebrew::Service do
describe ".deserialize" do
let(:serialized_hash) do
{
"name" => {
"linux" => "custom.systemd.name",
"macos" => "custom.launchd.name",
},
"environment_variables" => {
"PATH" => "$HOMEBREW_PREFIX/bin:$HOMEBREW_PREFIX/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
},
@ -961,6 +965,10 @@ describe Homebrew::Service do
let(:deserialized_hash) do
{
name: {
linux: "custom.systemd.name",
macos: "custom.launchd.name",
},
environment_variables: {
PATH: "#{HOMEBREW_PREFIX}/bin:#{HOMEBREW_PREFIX}/sbin:/usr/bin:/bin:/usr/sbin:/sbin",
},

View File

@ -0,0 +1,50 @@
# typed: true
# frozen_string_literal: true
module Utils
# Helpers for `brew services` related code.
module Service
# Check if a service is running for a specified formula.
sig { params(formula: Formula).returns(T::Boolean) }
def self.running?(formula)
if launchctl?
quiet_system(launchctl, "list", formula.plist_name)
elsif systemctl?
quiet_system(systemctl, "is-active", "--quiet", formula.service_name)
end
end
# Check if a service file is installed in the expected location.
sig { params(formula: Formula).returns(T::Boolean) }
def self.installed?(formula)
(launchctl? && formula.launchd_service_path.exist?) ||
(systemctl? && formula.systemd_service_path.exist?)
end
# Path to launchctl binary.
sig { returns(T.nilable(Pathname)) }
def self.launchctl
return @launchctl if defined? @launchctl
@launchctl = which("launchctl")
end
# Path to systemctl binary.
sig { returns(T.nilable(Pathname)) }
def self.systemctl
return @systemctl if defined? @systemctl
@systemctl = which("systemctl")
end
sig { returns(T::Boolean) }
def self.launchctl?
!launchctl.nil?
end
sig { returns(T::Boolean) }
def self.systemctl?
!systemctl.nil?
end
end
end

View File

@ -0,0 +1,7 @@
# typed: strict
module Utils
module Service
include Kernel
end
end

View File

@ -871,13 +871,17 @@ Another example would be configuration files that should not be overwritten on p
There are two ways to add `launchd` plists and `systemd` services to a formula, so that [`brew services`](https://github.com/Homebrew/homebrew-services) can pick them up:
1. If the package already provides a service file the formula can install it into the prefix:
1. If the package already provides a service file the formula can reference it by name:
```ruby
prefix.install_symlink "file.plist" => "#{plist_name}.plist"
prefix.install_symlink "file.service" => "#{service_name}.service"
service do
name macos: "custom.launchd.name",
linux: "custom.systemd.name"
end
```
To find the file we append `.plist` to the `launchd` service name and `.service` to the `systemd` service name internally.
2. If the formula does not provide a service file you can generate one using the following stanza:
```ruby
@ -900,7 +904,7 @@ There are two ways to add `launchd` plists and `systemd` services to a formula,
#### Service block methods
This table lists the options you can set within a `service` block. Only the `run` field is required which indicates what to run.
This table lists the options you can set within a `service` block. The `run` or `name` field must be defined inside the service block. The `run` field indicates what command to run and is required before using fields other than `name`.
| method | default | macOS | Linux | description |
| ----------------------- | ------------ | :---: | :---: | ----------- |
@ -921,6 +925,7 @@ This table lists the options you can set within a `service` block. Only the `run
| `process_type` | - | yes | no-op | type of process to manage: `:background`, `:standard`, `:interactive` or `:adaptive`
| `macos_legacy_timers` | - | yes | no-op | timers created by `launchd` jobs are coalesced unless this is set
| `sockets` | - | yes | no-op | socket that is created as an accesspoint to the service
| `name` | - | yes | yes | a hash with the `launchd` service name on macOS and/or the `systemd` service name on Linux
For services that are kept alive after starting you can use the default `run_type`: