From 8572fb538d2e14ca94b11bdc0eff3a718a93bfe4 Mon Sep 17 00:00:00 2001 From: Daeho Ro <40587651+daeho-ro@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:16:08 +0900 Subject: [PATCH] install_steps: add `{{arch}}` template token - the deprecated `postflight` block exposed the cask's `arch`, but its `postflight_steps` replacement did not, so migrating casks had to duplicate steps in `on_arm` and `on_intel` blocks - expand `{{arch}}` to the value the cask's `arch` stanza selected, and to an empty string when no value matches, as `postflight` did - leave `{{arch}}` verbatim for contexts without an `arch` - fix the step path corrector wrapping allowed template helpers in a second `#{}`, which autocorrected to `"#{#{arch}}"` --- .../Homebrew/cask/artifact/install_steps.rb | 1 + Library/Homebrew/cask_artifact.rb | 4 +++ Library/Homebrew/install_steps.rb | 16 ++++++++++ .../rubocops/shared/install_steps_helper.rb | 10 +++--- Library/Homebrew/test/install_steps_spec.rb | 32 +++++++++++++++++++ .../test/rubocops/cask/install_steps_spec.rb | 27 +++++++++++++++- docs/Cask-Cookbook.md | 4 +-- 7 files changed, 87 insertions(+), 7 deletions(-) diff --git a/Library/Homebrew/cask/artifact/install_steps.rb b/Library/Homebrew/cask/artifact/install_steps.rb index abfc86e263c42..e25f53d9b02c7 100644 --- a/Library/Homebrew/cask/artifact/install_steps.rb +++ b/Library/Homebrew/cask/artifact/install_steps.rb @@ -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, diff --git a/Library/Homebrew/cask_artifact.rb b/Library/Homebrew/cask_artifact.rb index b29b900800774..537b57791dbfd 100644 --- a/Library/Homebrew/cask_artifact.rb +++ b/Library/Homebrew/cask_artifact.rb @@ -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 @@ -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) diff --git a/Library/Homebrew/install_steps.rb b/Library/Homebrew/install_steps.rb index 04294b52a7939..e3283410257d8 100644 --- a/Library/Homebrew/install_steps.rb +++ b/Library/Homebrew/install_steps.rb @@ -104,6 +104,11 @@ def token "{{token}}" end + sig { returns(String) } + def arch + "{{arch}}" + end + sig { returns(TemplateVersion) } def version TEMPLATE_VERSION @@ -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" @@ -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 diff --git a/Library/Homebrew/rubocops/shared/install_steps_helper.rb b/Library/Homebrew/rubocops/shared/install_steps_helper.rb index 32c072c5810a8..a4f5490191e9b 100644 --- a/Library/Homebrew/rubocops/shared/install_steps_helper.rb +++ b/Library/Homebrew/rubocops/shared/install_steps_helper.rb @@ -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? @@ -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 diff --git a/Library/Homebrew/test/install_steps_spec.rb b/Library/Homebrew/test/install_steps_spec.rb index 8881684847f46..f252397cf1485 100644 --- a/Library/Homebrew/test/install_steps_spec.rb +++ b/Library/Homebrew/test/install_steps_spec.rb @@ -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" } diff --git a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb index 7c8fa1ef25132..29a4ea59b9fff 100644 --- a/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb +++ b/Library/Homebrew/test/rubocops/cask/install_steps_spec.rb @@ -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 @@ -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 diff --git a/docs/Cask-Cookbook.md b/docs/Cask-Cookbook.md index 127b000d40c6b..40ca62d4d8567 100644 --- a/docs/Cask-Cookbook.md +++ b/docs/Cask-Cookbook.md @@ -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 %}