diff --git a/Library/Homebrew/dev-cmd/bump-cask-pr.rb b/Library/Homebrew/dev-cmd/bump-cask-pr.rb index cc7c4132c1289..6ddfcc34a5bd9 100644 --- a/Library/Homebrew/dev-cmd/bump-cask-pr.rb +++ b/Library/Homebrew/dev-cmd/bump-cask-pr.rb @@ -293,7 +293,11 @@ def replace_version_and_checksum(cask, new_hash, new_version, contents) cask_sourcefile_path = cask.sourcefile_path raise "unexpected nil cask.sourcefile_path" unless cask_sourcefile_path - old_cask = Cask::CaskLoader.load(cask_sourcefile_path) + contents = split_root_version_and_checksum(new_version, contents) + + old_cask = Homebrew::SimulateSystem.with(os: default_cask_os, arch: :arm) do + Cask::CaskLoader.load(cask_sourcefile_path) + end generate_system_options(cask, new_version).each do |os, arch| tag = Utils::Bottles::Tag.new(system: os, arch:) old_cask.refresh_for_tag(tag) do @@ -307,25 +311,34 @@ def replace_version_and_checksum(cask, new_hash, new_version, contents) bump_version = new_version.send(arch) || new_version.general next unless bump_version + version_scope = cask_stanza_scope(contents, :version, arch) contents = replace_cask_stanza_value( contents, :version, old_version.latest? ? :latest : old_version.to_s, - bump_version.latest? ? :latest : bump_version.to_s + bump_version.latest? ? :latest : bump_version.to_s, + within: version_scope ) tmp_cask = Cask::CaskLoader::FromContentLoader.new(contents) .load(config: nil) old_hash = tmp_cask.sha256 + next if new_hash.is_a?(String) && old_hash.to_s == new_hash + + checksum_scope = cask_stanza_scope(contents, :sha256, arch) if tmp_cask.version.latest? || new_hash == :no_check opoo "Ignoring specified `--sha256=` argument." if new_hash.is_a?(String) if old_hash != :no_check - contents = replace_cask_stanza_value(contents, :sha256, old_hash.to_s, - :no_check) + contents = replace_cask_stanza_value(contents, :sha256, old_hash.to_s, :no_check, + within: checksum_scope) end elsif old_hash == :no_check && new_hash != :no_check - contents = replace_cask_stanza_value(contents, :sha256, :no_check, new_hash) if new_hash.is_a?(String) - elsif new_hash && !cask.on_system_blocks_exist? && cask.languages.empty? - contents = replace_cask_stanza_value(contents, :sha256, old_hash.to_s, new_hash.to_s) + if new_hash.is_a?(String) && (!arch_specific_version_bump?(new_version) || checksum_scope) + contents = replace_cask_stanza_value(contents, :sha256, :no_check, new_hash, within: checksum_scope) + end + elsif new_hash && cask.languages.empty? && + (!cask.on_system_blocks_exist? || checksum_scope || arch_specific_version_bump?(new_version)) + contents = replace_cask_stanza_value(contents, :sha256, old_hash.to_s, new_hash.to_s, + within: checksum_scope) elsif old_hash != :no_check opoo "Multiple checksum replacements required; ignoring specified `--sha256` argument." if new_hash languages = if cask.languages.empty? @@ -346,7 +359,8 @@ def replace_version_and_checksum(cask, new_hash, new_version, contents) Utils::Tar.validate_file(download) if new_cask.sha256.to_s != download.sha256 - contents = replace_cask_stanza_value(contents, :sha256, new_cask.sha256.to_s, download.sha256) + contents = replace_cask_stanza_value(contents, :sha256, new_cask.sha256.to_s, download.sha256, + within: checksum_scope) end end end @@ -355,24 +369,76 @@ def replace_version_and_checksum(cask, new_hash, new_version, contents) contents end + sig { + params( + new_version: BumpVersionParser, + contents: String, + ).returns(String) + } + def split_root_version_and_checksum(new_version, contents) + return contents unless arch_specific_version_bump?(new_version) + + cask_ast = Utils::AST::CaskAST.new(contents) + root_version = cask_ast.first_stanza_value(:version, within: :root) + if root_version && + !cask_ast.stanza_anywhere?(:version, within: :on_arm) && + !cask_ast.stanza_anywhere?(:version, within: :on_intel) + cask_ast.replace_root_stanza_with_arch_blocks(:version, root_version) + contents = cask_ast.process + end + + cask_ast = Utils::AST::CaskAST.new(contents) + root_sha256 = cask_ast.first_stanza_value(:sha256, within: :root) + if root_sha256.is_a?(String) && + !cask_ast.stanza_anywhere?(:sha256, within: :on_arm) && + !cask_ast.stanza_anywhere?(:sha256, within: :on_intel) + cask_ast.replace_root_stanza_with_arch_blocks(:sha256, root_sha256) + contents = cask_ast.process + end + + contents + end + + sig { params(new_version: BumpVersionParser).returns(T::Boolean) } + def arch_specific_version_bump?(new_version) + new_version.arm.present? || new_version.intel.present? + end + + sig { returns(Symbol) } + def default_cask_os + current_os = Homebrew::SimulateSystem.current_os + return current_os if MacOSVersion::SYMBOLS.include?(current_os) + + MacOSVersion.new(HOMEBREW_MACOS_NEWEST_SUPPORTED).to_sym + end + + sig { params(contents: String, name: Symbol, arch: Symbol).returns(T.nilable(Symbol)) } + def cask_stanza_scope(contents, name, arch) + scope = :"on_#{arch}" + return scope if Utils::AST::CaskAST.new(contents).stanza?(name, within: scope) + + nil + end + sig { params( contents: String, name: Symbol, old_value: T.any(Numeric, String, Symbol), new_value: T.any(Numeric, String, Symbol), + within: T.nilable(Symbol), ).returns(String) } - def replace_cask_stanza_value(contents, name, old_value, new_value) + def replace_cask_stanza_value(contents, name, old_value, new_value, within: nil) return contents if old_value == new_value cask_ast = Utils::AST::CaskAST.new(contents) - replacement_count = cask_ast.replace_stanza_value(name, old_value, new_value) + replacement_count = cask_ast.replace_stanza_value(name, old_value, new_value, within:) if replacement_count.zero? # Treat an already-applied replacement as a successful no-op so the # per-(os, arch) loop in `replace_version_and_checksum` can yield the # same general version more than once without raising. - return contents if cask_ast.replace_stanza_value(name, new_value, new_value).positive? + return contents if cask_ast.replace_stanza_value(name, new_value, new_value, within:).positive? raise "Could not find '#{name}' stanza with value #{old_value.inspect}!" end diff --git a/Library/Homebrew/dev-cmd/bump.rb b/Library/Homebrew/dev-cmd/bump.rb index 5b0cff6c82252..34cc2a41a642f 100644 --- a/Library/Homebrew/dev-cmd/bump.rb +++ b/Library/Homebrew/dev-cmd/bump.rb @@ -702,27 +702,7 @@ def retrieve_and_display_info_and_open_pr(formula_or_cask, name, repositories, a return if duplicate_pull_requests.present? - version_args = [] - if multiple_versions[:current] && multiple_versions[:new] - if (new_version_arm = new_version.arm) && - !message?(new_version_arm) && - current_version.arm && - new_version_arm > current_version.arm - version_args << "--version-arm=#{new_version_arm}" - end - if (new_version_intel = new_version.intel) && - !message?(new_version_intel) && - current_version.intel && - new_version_intel > current_version.intel - version_args << "--version-intel=#{new_version_intel}" - end - elsif multiple_versions[:current] - opoo "`#{name}` needs to be manually updated using one version" - elsif multiple_versions[:new] - opoo "`#{name}` needs to be manually updated using arch-specific versions" - elsif new_version.general - version_args << "--version=#{new_version.general}" - end + version_args = version_args_for_bump(current_version:, new_version:, multiple_versions:, name:) return if version_args.blank? bump_pr_args = [ @@ -753,6 +733,49 @@ def retrieve_and_display_info_and_open_pr(formula_or_cask, name, repositories, a Homebrew.failed = true unless result end + sig { + params( + current_version: BumpVersionParser, + new_version: BumpVersionParser, + multiple_versions: T::Hash[Symbol, T::Boolean], + name: String, + ).returns(T::Array[String]) + } + def version_args_for_bump(current_version:, new_version:, multiple_versions:, name:) + version_args = T.let([], T::Array[String]) + + if multiple_versions[:new] + (BumpVersionParser::VERSION_SYMBOLS - [:general]).each do |arch| + new_arch_version = new_version.send(arch) + next if new_arch_version.blank? || message?(new_arch_version) + + current_arch_version = if multiple_versions[:current] + current_version.send(arch) + else + current_version.general + end + next if current_arch_version.blank? || new_arch_version <= current_arch_version + + version_args << "--version-#{arch}=#{new_arch_version}" + end + elsif multiple_versions[:current] + if (new_general_version = new_version.general) && !message?(new_general_version) + (BumpVersionParser::VERSION_SYMBOLS - [:general]).each do |arch| + current_arch_version = current_version.send(arch) + next if current_arch_version.blank? || new_general_version <= current_arch_version + + version_args << "--version-#{arch}=#{new_general_version}" + end + end + + opoo "`#{name}` needs to be manually updated using one version" if version_args.blank? + elsif new_version.general + version_args << "--version=#{new_version.general}" + end + + version_args + end + sig { params( current_version: BumpVersionParser, diff --git a/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb b/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb index b1d0c04164286..112d1cec94f16 100644 --- a/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb +++ b/Library/Homebrew/test/dev-cmd/bump-cask-pr_spec.rb @@ -263,7 +263,7 @@ sha256 arm: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", intel: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - url "https://brew.sh/foo-\#{arch}-\#{version}.dmg" + url "https://brew.sh/foo-\#{version}.dmg" name "Foo" end RUBY @@ -287,6 +287,202 @@ end end + describe "#replace_version_and_checksum" do + let(:old_hash) { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" } + let(:new_hash) { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } + let(:intel_hash) { "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" } + + before do + Homebrew.install_bundler_gems!(groups: ["ast"]) + require "utils/ast" + end + + def cask_from_contents(contents) + path = mktmpdir/"foo.rb" + path.write(contents) + Homebrew::SimulateSystem.with(os: newest_macos, arch: :arm) do + Cask::CaskLoader.load(path) + end + end + + it "splits a root version and single checksum before replacing the ARM values" do + contents = <<~RUBY + cask "foo" do + version "1.0" + sha256 "#{old_hash}" + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + cask = Homebrew::SimulateSystem.with(os: newest_macos, arch: :arm) { cask_from_contents(contents) } + new_version = Homebrew::BumpVersionParser.new(arm: "2.0") + + expect(bump_cask_pr.send(:replace_version_and_checksum, cask, new_hash, new_version, contents)) + .to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + end + on_intel do + version "1.0" + end + on_arm do + sha256 "#{new_hash}" + end + on_intel do + sha256 "#{old_hash}" + end + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + end + + it "splits a root version and keeps top-level architecture checksums" do + contents = <<~RUBY + cask "foo" do + arch arm: "arm", intel: "intel" + + version "1.0" + sha256 arm: "#{old_hash}", + intel: "#{intel_hash}" + + url "https://brew.sh/foo-\#{arch}-\#{version}.dmg" + name "Foo" + end + RUBY + cask = Homebrew::SimulateSystem.with(os: newest_macos, arch: :arm) { cask_from_contents(contents) } + new_version = Homebrew::BumpVersionParser.new(arm: "2.0") + + expect(bump_cask_pr.send(:replace_version_and_checksum, cask, new_hash, new_version, contents)) + .to eq <<~RUBY + cask "foo" do + arch arm: "arm", intel: "intel" + + on_arm do + version "2.0" + end + on_intel do + version "1.0" + end + sha256 arm: "#{new_hash}", + intel: "#{intel_hash}" + + url "https://brew.sh/foo-\#{arch}-\#{version}.dmg" + name "Foo" + end + RUBY + end + + it "splits a root version and leaves top-level no_check checksums" do + contents = <<~RUBY + cask "foo" do + version "1.0" + sha256 :no_check + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + cask = cask_from_contents(contents) + new_version = Homebrew::BumpVersionParser.new(arm: "2.0") + + expect(bump_cask_pr.send(:replace_version_and_checksum, cask, new_hash, new_version, contents)) + .to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + end + on_intel do + version "1.0" + end + sha256 :no_check + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + end + + it "splits root version and checksum stanzas when new versions differ by architecture" do + contents = <<~RUBY + cask "foo" do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + cask = cask_from_contents(contents) + new_version = Homebrew::BumpVersionParser.new(arm: "2.0", intel: "1.5") + + expect( + bump_cask_pr.send(:replace_version_and_checksum, cask, :no_check, new_version, contents), + ).to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + end + on_intel do + version "1.5" + end + on_arm do + sha256 :no_check + end + on_intel do + sha256 :no_check + end + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + end + + it "only updates matching version and checksum stanzas inside the target architecture block" do + contents = <<~RUBY + cask "foo" do + on_arm do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + on_intel do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + cask = cask_from_contents(contents) + new_version = Homebrew::BumpVersionParser.new(arm: "2.0") + + expect( + bump_cask_pr.send(:replace_version_and_checksum, cask, :no_check, new_version, contents), + ).to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + sha256 :no_check + end + + on_intel do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + url "https://brew.sh/foo-\#{version}.dmg" + name "Foo" + end + RUBY + end + end + describe "::check_throttle" do let(:c_throttle) do Cask::Cask.new("throttle-test") do diff --git a/Library/Homebrew/test/dev-cmd/bump_spec.rb b/Library/Homebrew/test/dev-cmd/bump_spec.rb index 11295444af463..6db9e39d38236 100644 --- a/Library/Homebrew/test/dev-cmd/bump_spec.rb +++ b/Library/Homebrew/test/dev-cmd/bump_spec.rb @@ -153,6 +153,77 @@ end end + describe "::retrieve_and_display_info_and_open_pr" do + subject(:bump) { klass.new(["--open-pr", "test"]) } + + before do + allow(bump).to receive(:retrieve_pull_requests) + allow(GitHub).to receive(:too_many_open_prs?).and_return(false) + end + + it "passes arch-specific version arguments when a cask moves from one version to arch-specific versions" do + version_info = klass::VersionBumpInfo.new( + type: :cask, + deprecated: { general: false }, + multiple_versions: { current: false, new: true }, + version_name: "cask version: ", + current_version: Homebrew::BumpVersionParser.new(general: Version.new("1.2.3")), + new_version: Homebrew::BumpVersionParser.new( + arm: Version.new("1.2.5"), + intel: Version.new("1.2.4"), + ), + repology_latest: "not found", + newer_than_upstream: { general: false }, + duplicate_pull_requests: nil, + maybe_duplicate_pull_requests: nil, + ) + allow(bump).to receive(:retrieve_versions_by_arch).and_return(version_info) + + expect(bump).to receive(:system).with( + HOMEBREW_BREW_FILE, + "bump-cask-pr", + "basic-cask", + "--version-arm=1.2.5", + "--version-intel=1.2.4", + "--no-browse", + "--message=Created by `brew bump`", + ).and_return(true) + + bump.send(:retrieve_and_display_info_and_open_pr, c_basic, "basic-cask", [], ambiguous_cask: false) + end + + it "passes a single version argument when an arch-specific cask moves to one version" do + version_info = klass::VersionBumpInfo.new( + type: :cask, + deprecated: { arm: false, intel: false }, + multiple_versions: { current: true, new: false }, + version_name: "cask version: ", + current_version: Homebrew::BumpVersionParser.new( + arm: Version.new("1.2.3"), + intel: Version.new("1.2.2"), + ), + new_version: Homebrew::BumpVersionParser.new(general: Version.new("1.2.4")), + repology_latest: "not found", + newer_than_upstream: { arm: false, intel: false }, + duplicate_pull_requests: nil, + maybe_duplicate_pull_requests: nil, + ) + allow(bump).to receive(:retrieve_versions_by_arch).and_return(version_info) + + expect(bump).to receive(:system).with( + HOMEBREW_BREW_FILE, + "bump-cask-pr", + "basic-cask", + "--version-arm=1.2.4", + "--version-intel=1.2.4", + "--no-browse", + "--message=Created by `brew bump`", + ).and_return(true) + + bump.send(:retrieve_and_display_info_and_open_pr, c_basic, "basic-cask", [], ambiguous_cask: false) + end + end + describe "::message?" do let(:version) { Version.new("1.2.3") } let(:cask_version) { Cask::DSL::Version.new("1.2.3,4") } @@ -184,6 +255,83 @@ end end + describe "::version_args_for_bump" do + let(:current_general) { Homebrew::BumpVersionParser.new(general: "26.519.41501") } + let(:new_split) do + Homebrew::BumpVersionParser.new( + arm: "26.519.81530", + intel: "26.519.41501", + ) + end + let(:current_split) do + Homebrew::BumpVersionParser.new( + arm: "1.2.3", + intel: "1.2.2", + ) + end + let(:new_general) { Homebrew::BumpVersionParser.new(general: "1.2.4") } + + it "emits only changed arch arguments when a general cask version becomes arch-specific" do + expect( + bump.send(:version_args_for_bump, + current_version: current_general, + new_version: new_split, + multiple_versions: { current: false, new: true }, + name: "codex-app"), + ).to eq(["--version-arm=26.519.81530"]) + end + + it "emits arch arguments for both architectures when split cask versions merge" do + expect( + bump.send(:version_args_for_bump, + current_version: current_split, + new_version: new_general, + multiple_versions: { current: true, new: false }, + name: "foo"), + ).to eq(["--version-arm=1.2.4", "--version-intel=1.2.4"]) + end + + it "keeps existing split-to-split routing" do + new_split = Homebrew::BumpVersionParser.new( + arm: "1.2.4", + intel: "1.2.2", + ) + + expect( + bump.send(:version_args_for_bump, + current_version: current_split, + new_version: new_split, + multiple_versions: { current: true, new: true }, + name: "foo"), + ).to eq(["--version-arm=1.2.4"]) + end + + it "keeps existing general version routing" do + expect( + bump.send(:version_args_for_bump, + current_version: current_general, + new_version: new_general, + multiple_versions: { current: false, new: false }, + name: "foo"), + ).to eq(["--version=1.2.4"]) + end + + it "ignores message versions in arch-specific routing" do + new_split = Homebrew::BumpVersionParser.new( + arm: "26.519.81530", + intel: "skipped", + ) + + expect( + bump.send(:version_args_for_bump, + current_version: current_general, + new_version: new_split, + multiple_versions: { current: false, new: true }, + name: "foo"), + ).to eq(["--version-arm=26.519.81530"]) + end + end + describe "::version_with_cooldown" do it "uses RubyGems version creation times" do version_info = { diff --git a/Library/Homebrew/test/utils/ast/cask_ast_spec.rb b/Library/Homebrew/test/utils/ast/cask_ast_spec.rb index caab374f667bb..8deaf6cdeefe1 100644 --- a/Library/Homebrew/test/utils/ast/cask_ast_spec.rb +++ b/Library/Homebrew/test/utils/ast/cask_ast_spec.rb @@ -78,6 +78,121 @@ end RUBY end + + it "replaces matching stanza arguments only inside on_arm blocks" do + cask_ast = klass.new <<~RUBY + cask "foo" do + on_arm do + version "1.0" + end + on_intel do + version "1.0" + end + end + RUBY + + expect(cask_ast.replace_stanza_value(:version, "1.0", "2.0", within: :on_arm)).to eq(1) + expect(cask_ast.process).to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + end + on_intel do + version "1.0" + end + end + RUBY + end + + it "replaces matching stanza arguments only inside on_intel blocks" do + cask_ast = klass.new <<~RUBY + cask "foo" do + on_arm do + version "1.0" + end + on_intel do + version "1.0" + end + end + RUBY + + expect(cask_ast.replace_stanza_value(:version, "1.0", "2.0", within: :on_intel)).to eq(1) + expect(cask_ast.process).to eq <<~RUBY + cask "foo" do + on_arm do + version "1.0" + end + on_intel do + version "2.0" + end + end + RUBY + end + + it "keeps replacing all matching stanza arguments without a scope" do + cask_ast = klass.new <<~RUBY + cask "foo" do + on_arm do + version "1.0" + end + on_intel do + version "1.0" + end + end + RUBY + + expect(cask_ast.replace_stanza_value(:version, "1.0", "2.0")).to eq(2) + expect(cask_ast.process).to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + end + on_intel do + version "2.0" + end + end + RUBY + end + + it "replaces matching stanza values within an on-system block" do + cask_ast = klass.new <<~RUBY + cask "foo" do + on_arm do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + on_intel do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + url "https://brew.sh/foo.dmg" + end + RUBY + + expect(cask_ast.replace_stanza_value(:version, "1.0", "2.0", within: :on_arm)).to eq(1) + expect( + cask_ast.replace_stanza_value(:sha256, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + within: :on_arm), + ).to eq(1) + expect(cask_ast.process).to eq <<~RUBY + cask "foo" do + on_arm do + version "2.0" + sha256 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + end + + on_intel do + version "1.0" + sha256 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + end + + url "https://brew.sh/foo.dmg" + end + RUBY + end end describe "#depends_on_macos?" do diff --git a/Library/Homebrew/utils/ast.rb b/Library/Homebrew/utils/ast.rb index a55f722a8c68e..d65726e48872a 100644 --- a/Library/Homebrew/utils/ast.rb +++ b/Library/Homebrew/utils/ast.rb @@ -587,16 +587,42 @@ def replace_first_stanza_value(name, value) replace_stanza_argument(stanza_node, value) end + sig { params(name: Symbol, within: T.nilable(Symbol)).returns(T::Boolean) } + def stanza?(name, within: nil) + stanzas(name, within:).present? + end + + sig { params(name: Symbol, within: Symbol).returns(T::Boolean) } + def stanza_anywhere?(name, within:) + cask_block.each_node(:block).any? do |node| + block_node = T.cast(node, BlockNode) + next false if block_node.method_name != within || block_node.receiver.present? + + block_node.each_node(:send).any? do |send_node| + send_node.method_name == name && send_node.receiver.nil? && send_node.first_argument.present? + end + end + end + + sig { params(name: Symbol, within: T.nilable(Symbol)).returns(T.untyped) } + def first_stanza_value(name, within: nil) + stanza_node = stanzas(name, within:).first + return if stanza_node.blank? + + literal_value(stanza_node.first_argument) + end + sig { params( name: Symbol, old_value: T.any(Numeric, String, Symbol), new_value: T.any(Numeric, String, Symbol), + within: T.nilable(Symbol), ).returns(Integer) } - def replace_stanza_value(name, old_value, new_value) + def replace_stanza_value(name, old_value, new_value, within: nil) replacement_count = T.let(0, Integer) - stanzas(name).each do |stanza_node| + stanzas(name, within:).each do |stanza_node| if literal_value(stanza_node.first_argument) == old_value replace_stanza_argument(stanza_node, new_value) replacement_count += 1 @@ -615,6 +641,25 @@ def replace_stanza_value(name, old_value, new_value) replacement_count end + sig { params(name: Symbol, old_value: T.any(Numeric, String, Symbol)).void } + def replace_root_stanza_with_arch_blocks(name, old_value) + stanza_node = top_level_stanzas(name).find do |node| + literal_value(node.first_argument) == old_value + end + return if stanza_node.blank? + + indent = " " * stanza_node.source_range.column + replacement = [ + "#{indent}on_arm do", + "#{indent} #{name} #{ruby_literal(old_value)}", + "#{indent}end", + "#{indent}on_intel do", + "#{indent} #{name} #{ruby_literal(old_value)}", + "#{indent}end", + ].join("\n") + tree_rewriter.replace(whole_line_range(stanza_node.source_range), "#{replacement}\n") + end + sig { returns(T::Boolean) } def depends_on_macos? stanzas(:depends_on).any? do |stanza_node| @@ -641,13 +686,38 @@ def depends_on_macos? sig { returns(TreeRewriter) } attr_reader :tree_rewriter + sig { params(name: Symbol, within: T.nilable(Symbol)).returns(T::Array[SendNode]) } + def stanzas(name, within: nil) + if within == :root + nodes = body_children(cask_block.body).grep(SendNode) + elsif within + blocks = on_system_blocks(within) + return [] if blocks.blank? + + nodes = blocks.flat_map { |block| body_children(block.body).grep(SendNode) } + else + nodes = cask_block.each_node(:send) + end + + nodes.select do |node| + node.method_name == name && node.receiver.nil? && node.first_argument.present? + end + end + sig { params(name: Symbol).returns(T::Array[SendNode]) } - def stanzas(name) - cask_block.each_node(:send).select do |node| + def top_level_stanzas(name) + body_children(cask_block.body).grep(SendNode).select do |node| node.method_name == name && node.receiver.nil? && node.first_argument.present? end end + sig { params(name: Symbol).returns(T::Array[BlockNode]) } + def on_system_blocks(name) + body_children(cask_block.body).grep(BlockNode).select do |node| + node.method_name == name && node.receiver.nil? + end + end + sig { params(stanza_node: SendNode, value: T.any(Numeric, String, Symbol)).void } def replace_stanza_argument(stanza_node, value) argument = stanza_node.first_argument @@ -656,6 +726,20 @@ def replace_stanza_argument(stanza_node, value) tree_rewriter.replace(argument.source_range, ruby_literal(value)) end + sig { params(range: Parser::Source::Range).returns(Parser::Source::Range) } + def whole_line_range(range) + range.with( + begin_pos: range.begin_pos - range.column, + end_pos: line_end_pos(range.end_pos), + ) + end + + sig { params(position: Integer).returns(Integer) } + def line_end_pos(position) + newline_pos = cask_contents.index("\n", position) + newline_pos ? newline_pos + 1 : cask_contents.length + end + sig { returns([ProcessedSource, BlockNode]) } def process_cask processed_source, root_node = process_source(cask_contents)