Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Library/Homebrew/cask/artifact/install_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def run_steps(command, phase: :install)
"context" => {
"name" => cask.name,
"token" => cask.token,
"arch" => cask.arch,
"version" => cask.version.to_s,
"staged_path" => cask.staged_path.to_s,
"caskroom_path" => cask.caskroom_path.to_s,
Expand Down
4 changes: 4 additions & 0 deletions Library/Homebrew/cask_artifact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ class InstallStepsContext
sig { returns(String) }
attr_reader :token

sig { returns(T.nilable(String)) }
attr_reader :arch

sig { returns(String) }
attr_reader :version

Expand All @@ -42,6 +45,7 @@ class InstallStepsContext
def initialize(context)
@name = T.let(context.fetch("name"), T.any(String, T::Array[String]))
@token = T.let(context.fetch("token"), String)
@arch = T.let(context["arch"], T.nilable(String))
@version = T.let(context.fetch("version"), String)
@staged_path = T.let(Pathname(context.fetch("staged_path")), Pathname)
@caskroom_path = T.let(Pathname(context.fetch("caskroom_path")), Pathname)
Expand Down
16 changes: 16 additions & 0 deletions Library/Homebrew/install_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ def token
"{{token}}"
end

sig { returns(String) }
def arch
"{{arch}}"
end

sig { returns(TemplateVersion) }
def version
TEMPLATE_VERSION
Expand Down Expand Up @@ -1412,6 +1417,8 @@ def template_token_value(token)
context_value(:name)&.to_s
when "name"
context_name
when "arch"
context_arch
when "token"
context_value(:token)&.to_s
when "user"
Expand Down Expand Up @@ -1496,6 +1503,15 @@ def context_name
value&.to_s
end

sig { returns(T.nilable(String)) }
def context_arch
# Casks without a matching `arch` stanza value expand to an empty
# string, matching the deprecated `postflight` block.
return unless @context.respond_to?(:arch)

context_value(:arch).to_s
end

sig { returns(T.nilable(String)) }
def context_version
context_value(:version)&.to_s
Expand Down
10 changes: 6 additions & 4 deletions Library/Homebrew/rubocops/shared/install_steps_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,9 @@ def allowed_step_template_node?(node)

send_node = T.cast(node, RuboCop::AST::SendNode)
return false if send_node.arguments.present?
return [:formula_name, :name, :token, :version].include?(send_node.method_name) if send_node.receiver.nil?
if send_node.receiver.nil?
return [:arch, :formula_name, :name, :token, :version].include?(send_node.method_name)
end

return false unless (receiver = send_node.receiver)&.send_type?

Expand Down Expand Up @@ -749,9 +751,9 @@ def dstr_permission_path(node)
source << content.dump.delete_prefix('"').delete_suffix('"')
true
elsif child.begin_type? && allowed_step_template_node?(child)
interpolation = "\#{#{child.source}}"
path << interpolation
source << interpolation
# `child.source` already carries the `#{}` delimiters.
path << child.source
source << child.source
true
else
false
Expand Down
32 changes: 32 additions & 0 deletions Library/Homebrew/test/install_steps_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,38 @@ def create_executable(path)
expect(written).to include("literal = {{unknown}} {single}")
end

specify "expands the cask arch token" do
context.define_singleton_method(:arch) { "-m1" }
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
write_file "arch", "example#{arch}.app", append_newline: true
end

Homebrew::InstallSteps::Runner.new(context:).run(steps)

expect((root/"var/arch").read).to eq("example-m1.app\n")
end

specify "expands the cask arch token to an empty string without a matching value" do
context.define_singleton_method(:arch) { nil }
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
write_file "arch", "example{{arch}}.app", append_newline: true
end

Homebrew::InstallSteps::Runner.new(context:).run(steps)

expect((root/"var/arch").read).to eq("example.app\n")
end

specify "leaves the arch token verbatim for a context without an arch" do
steps = Homebrew::InstallSteps::DSL.build(default_base: :var) do
write_file "arch", "example{{arch}}.app", append_newline: true
end

Homebrew::InstallSteps::Runner.new(context:).run(steps)

expect((root/"var/arch").read).to eq("example{{arch}}.app\n")
end

specify "expands formula and cask identity tokens" do
context.define_singleton_method(:name) { "example-formula" }
context.define_singleton_method(:token) { "example-cask" }
Expand Down
27 changes: 26 additions & 1 deletion Library/Homebrew/test/rubocops/cask/install_steps_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,31 @@
CASK
end

it "autocorrects permission work using the arch template helper" do
expect_offense <<~'CASK'
cask "foo" do
version :latest
sha256 :no_check

postflight do
^^^^^^^^^^^^^ Use `postflight_steps` for simple file preparation.
set_ownership "#{staged_path}/foo-#{arch}"
end
end
CASK

expect_correction <<~'CASK'
cask "foo" do
version :latest
sha256 :no_check

postflight_steps do
set_ownership "foo-#{arch}"
end
end
CASK
end

it "does not autocorrect dynamic, unsupported or mixed permission work" do
expect_offense <<~'CASK'
cask "foo" do
Expand All @@ -500,7 +525,7 @@

postflight do
^^^^^^^^^^^^^ Casks must use `postflight_steps` instead of `postflight`.
set_ownership "#{staged_path}/foo-#{arch}"
set_ownership "#{staged_path}/foo-#{language}"
end
end
CASK
Expand Down
4 changes: 2 additions & 2 deletions docs/Cask-Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,9 +670,9 @@ Ruby `#{...}` interpolation is normally evaluated before structured steps are se

`{{...}}` is not Ruby interpolation. It remains literal in the JSON API and the install-step runner expands supported tokens at install time. Use this form for install-time values inside `preflight_steps`, `postflight_steps`, `uninstall_preflight_steps` and `uninstall_postflight_steps`. When a path argument supports `base:`, `source_base:` or `target_base:`, prefer those options to embedding a path token.

The runtime steps DSL retains compatibility helpers for `token`, `name`, `version`, `version.major` and `version.major_minor`. Interpolating these helpers is safe and permitted by RuboCop because they return the corresponding `{{...}}` token text rather than a concrete value. Other Ruby interpolation is rejected. Prefer explicit `{{...}}` tokens in new steps so it is clear that expansion is deferred until installation.
The runtime steps DSL retains compatibility helpers for `token`, `name`, `arch`, `version`, `version.major` and `version.major_minor`. Interpolating these helpers is safe and permitted by RuboCop because they return the corresponding `{{...}}` token text rather than a concrete value. Other Ruby interpolation is rejected. Prefer explicit `{{...}}` tokens in new steps so it is clear that expansion is deferred until installation.

Content, replacements, command arguments and command environments may use fixed install-time tokens. These include `{{HOMEBREW_BREW_FILE}}`, `{{HOMEBREW_CELLAR}}`, `{{HOMEBREW_PREFIX}}`, `{{token}}`, `{{name}}`, `{{user}}`, `{{staged_path}}`, `{{appdir}}`, `{{caskroom_path}}`, `{{temp}}`, `{{version}}`, `{{version.major}}` and `{{version.major_minor}}`. `{{name}}` is retained for compatibility; prefer `{{token}}`. Any other `{{...}}` is left verbatim. For example: `write_file "settings.conf", "application = {{appdir}}/Example.app"`.
Content, replacements, command arguments and command environments may use fixed install-time tokens. These include `{{HOMEBREW_BREW_FILE}}`, `{{HOMEBREW_CELLAR}}`, `{{HOMEBREW_PREFIX}}`, `{{token}}`, `{{name}}`, `{{arch}}`, `{{user}}`, `{{staged_path}}`, `{{appdir}}`, `{{caskroom_path}}`, `{{temp}}`, `{{version}}`, `{{version.major}}` and `{{version.major_minor}}`. `{{name}}` is retained for compatibility; prefer `{{token}}`. `{{arch}}` expands to the value the cask's `arch` stanza selected for the current architecture, and to an empty string when the cask has no matching value. Any other `{{...}}` is left verbatim. For example: `write_file "settings.conf", "application = {{appdir}}/Example.app"`.

{% endraw %}

Expand Down
Loading