Files
brew/Library/Homebrew/cask/artifact/generated_completion.rb
T
Mike McQuaid f8fcbd88e0 Sandbox structured cask operations
- Run each complete cask step block in one isolated subprocess and all
  generated completions in another phase-scoped sandbox.
- Share sandbox selection, fork fallback, install-hook rules and child
  error reporting with formula build, post-install and test processes.
- Restrict home, network and filesystem access while preserving `brew`
  and supporting explicit command write paths.
- Keep JSON payloads compact and independent of cask Ruby files.
- Remove the completed official-tap migration plan.
2026-08-07 08:37:09 +01:00

179 lines
6.2 KiB
Ruby

# typed: strict
# frozen_string_literal: true
require "cask/artifact/abstract_artifact"
require "cask/artifact/bashcompletion"
require "cask/artifact/fishcompletion"
require "cask/artifact/zshcompletion"
require "extend/hash/keys"
require "utils/shell_completion"
module Cask
module Artifact
# Artifact corresponding to the `generate_completions_from_executable` stanza.
class GeneratedCompletion < AbstractArtifact
SUPPORTED_SHELLS = [:bash, :zsh, :fish, :pwsh].freeze
sig { override.returns(Symbol) }
def self.dsl_key
:generate_completions_from_executable
end
sig {
params(
cask: Cask,
args: T.any(Pathname, String),
base_name: T.nilable(String),
shell_parameter_format: T.nilable(T.any(Symbol, String)),
shells: T.nilable(T::Array[T.any(Symbol, String)]),
).returns(T.attached_class)
}
def self.from_args(cask, *args, base_name: nil, shell_parameter_format: nil, shells: nil)
raise CaskInvalidError.new(cask.token, "'#{dsl_key}' requires at least one command") if args.empty?
commands = args.to_a
resolved_shells = (shells || ::Utils::ShellCompletion.default_completion_shells(shell_parameter_format))
.map(&:to_sym)
unsupported_shells = resolved_shells - SUPPORTED_SHELLS
unless unsupported_shells.empty?
raise CaskInvalidError.new(
cask.token,
"'#{dsl_key}' does not support shell(s): #{unsupported_shells.join(", ")}",
)
end
new(
cask,
commands,
base_name:,
shell_parameter_format:,
shells: resolved_shells,
)
end
sig {
params(
cask: Cask,
commands: T::Array[T.any(Pathname, String)],
base_name: T.nilable(String),
shell_parameter_format: T.nilable(T.any(Symbol, String)),
shells: T::Array[Symbol],
).void
}
def initialize(cask, commands, base_name:, shell_parameter_format:, shells:)
super(cask, *commands, base_name:, shell_parameter_format:, shells:)
@commands = commands
@base_name = base_name
@shell_parameter_format = shell_parameter_format
@shells = shells
@resolved_base_name = T.let(nil, T.nilable(String))
end
sig { returns(T::Array[T.any(Pathname, String)]) }
attr_reader :commands
sig { returns(T.nilable(String)) }
attr_reader :base_name
sig { returns(T.nilable(T.any(Symbol, String))) }
attr_reader :shell_parameter_format
sig { returns(T::Array[Symbol]) }
attr_reader :shells
sig { override.returns(String) }
def summarize
"#{commands.join(" ")} (base_name: #{resolved_base_name}, shells: #{shells.join(", ")})"
end
sig { params(_options: T.untyped).void }
def install_phase(**_options)
executable = staged_path_join_executable(commands.fetch(0))
completion_commands = [executable, *commands[1..]]
completions = shells.map do |shell|
popen_read_env = { "SHELL" => shell.to_s }
{
"shell" => shell.to_s,
"commands" => completion_commands.map(&:to_s),
"shell_parameter" => ::Utils::ShellCompletion.completion_shell_parameter(
shell_parameter_format, shell, executable.to_s, popen_read_env
),
"env" => popen_read_env,
"output_path" => completion_script_path(shell).to_s,
}
end
if (sandbox = cask_sandbox)
completions.map { |completion| Pathname(completion.fetch("output_path")).dirname }.uniq.each do |directory|
sandbox.allow_write_path directory
end
begin
run_cask_sandbox(sandbox, { "action" => "generated_completions", "completions" => completions })
rescue => e
opoo e
end
return
end
completions.each { |completion| write_completion(completion, executable) }
end
sig { params(command: T.class_of(SystemCommand), _options: T.untyped).void }
def uninstall_phase(command: SystemCommand, **_options)
shells.each do |shell|
path = completion_script_path(shell)
next unless path.exist?
Utils.gain_permissions_remove(path, command:)
rescue => e
opoo "Failed to remove #{shell} generated completions: #{e}"
end
end
private
sig { params(completion: T::Hash[String, T.untyped], executable: Pathname).void }
def write_completion(completion, executable)
output_path = Pathname(completion.fetch("output_path"))
output_path.dirname.mkpath
output_path.write(
::Utils::ShellCompletion.generate_completion_output(
completion.fetch("commands"), completion["shell_parameter"], completion.fetch("env")
),
)
rescue => e
opoo "Failed to generate #{completion.fetch("shell")} completions from #{executable}: #{e}"
end
sig { returns(String) }
def resolved_base_name
@resolved_base_name ||= T.let(begin
executable = staged_path_join_executable(commands.fetch(0))
name = base_name || File.basename(executable.to_s)
name = cask.token if name.empty?
name
end, T.nilable(String))
@resolved_base_name
end
sig { params(shell: Symbol).returns(Pathname) }
def completion_script_path(shell)
case shell
when :bash
BashCompletion.new(cask, resolved_base_name).resolve_target(resolved_base_name)
when :zsh
ZshCompletion.new(cask, resolved_base_name).resolve_target(resolved_base_name)
when :fish
FishCompletion.new(cask, resolved_base_name).resolve_target(resolved_base_name)
when :pwsh
HOMEBREW_PREFIX/"share/pwsh/completions"/"_#{resolved_base_name}.ps1"
else
raise ArgumentError, "unsupported shell: #{shell}"
end
end
end
end
end