mirror of
https://github.com/Homebrew/brew.git
synced 2026-08-12 22:29:27 +04:00
- 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.
179 lines
6.2 KiB
Ruby
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
|