diff --git a/Library/Homebrew/bundle_version.rb b/Library/Homebrew/bundle_version.rb new file mode 100644 index 0000000000..d6d357008d --- /dev/null +++ b/Library/Homebrew/bundle_version.rb @@ -0,0 +1,89 @@ +# typed: true +# frozen_string_literal: true + +require "system_command" + +module Homebrew + # Representation of a macOS bundle version, commonly found in `Info.plist` files. + # + # @api private + class BundleVersion + extend T::Sig + + extend SystemCommand::Mixin + + sig { params(info_plist_path: Pathname).returns(T.nilable(String)) } + def self.from_info_plist(info_plist_path) + plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist + + short_version = plist["CFBundleShortVersionString"].presence + version = plist["CFBundleVersion"].presence + + new(short_version, version) if short_version || version + end + + sig { params(package_info_path: Pathname).returns(T.nilable(String)) } + def self.from_package_info(package_info_path) + Homebrew.install_bundler_gems! + require "nokogiri" + + xml = Nokogiri::XML(package_info_path.read) + + bundle_id = xml.xpath("//pkg-info//bundle-version//bundle").first&.attr("id") + return unless bundle_id + + bundle = xml.xpath("//pkg-info//bundle").find { |b| b["id"] == bundle_id } + return unless bundle + + short_version = bundle["CFBundleShortVersionString"] + version = bundle["CFBundleVersion"] + + new(short_version, version) if short_version || version + end + + sig { returns(T.nilable(String)) } + attr_reader :short_version, :version + + sig { params(short_version: T.nilable(String), version: T.nilable(String)).void } + def initialize(short_version, version) + @short_version = short_version.presence + @version = version.presence + + return if @short_version || @version + + raise ArgumentError, "`short_version` and `version` cannot both be `nil` or empty" + end + + def <=>(other) + [short_version, version].map { |v| Version.new(v) } <=> + [other.short_version, other.version].map { |v| Version.new(v) } + end + + # Create a nicely formatted version (on a best effor basis). + sig { returns(String) } + def nice_version + nice_parts.join(",") + end + + sig { returns(T::Array[String]) } + def nice_parts + return [short_version] if short_version == version + + if short_version && version + return [version] if version.match?(/\A\d+(\.\d+)+\Z/) && version.start_with?("#{short_version}.") + return [short_version] if short_version.match?(/\A\d+(\.\d+)+\Z/) && short_version.start_with?("#{version}.") + + if short_version.match?(/\A\d+(\.\d+)*\Z/) && version.match?(/\A\d+\Z/) + return [short_version] if short_version.start_with?("#{version}.") || short_version.end_with?(".#{version}") + + return [short_version, version] + end + end + + fallback = (short_version || version).sub(/\A[^\d]+/, "") + + [fallback] + end + private :nice_parts + end +end diff --git a/Library/Homebrew/test/bundle_version_spec.rb b/Library/Homebrew/test/bundle_version_spec.rb new file mode 100644 index 0000000000..4d653b98de --- /dev/null +++ b/Library/Homebrew/test/bundle_version_spec.rb @@ -0,0 +1,29 @@ +# typed: false +# frozen_string_literal: true + +require "bundle_version" + +describe Homebrew::BundleVersion do + describe "#nice_version" do + expected_mappings = { + ["1.2", nil] => "1.2", + [nil, "1.2.3"] => "1.2.3", + ["1.2", "1.2.3"] => "1.2.3", + ["1.2.3", "1.2"] => "1.2.3", + ["1.2.3", "8312"] => "1.2.3,8312", + ["2021", "2006"] => "2021,2006", + ["1.0", "1"] => "1.0", + ["1.0", "0"] => "1.0", + ["1.2.3.4000", "4000"] => "1.2.3.4000", + ["5", "5.0.45"] => "5.0.45", + ["XQuartz-2.7.11", "2.7.112"] => "2.7.11", + } + + expected_mappings.each do |(short_version, version), expected_version| + it "maps (#{short_version.inspect}, #{version.inspect}) to #{expected_version.inspect}" do + expect(described_class.new(short_version, version).nice_version) + .to eq expected_version + end + end + end +end diff --git a/Library/Homebrew/test/unversioned_cask_checker_spec.rb b/Library/Homebrew/test/unversioned_cask_checker_spec.rb deleted file mode 100644 index dc4ddbfef9..0000000000 --- a/Library/Homebrew/test/unversioned_cask_checker_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "unversioned_cask_checker" - -describe Homebrew::UnversionedCaskChecker do - describe "::decide_between_versions" do - expected_mappings = { - [nil, nil] => nil, - ["1.2", nil] => "1.2", - [nil, "1.2.3"] => "1.2.3", - ["1.2", "1.2.3"] => "1.2.3", - ["1.2.3", "1.2"] => "1.2.3", - ["1.2.3", "8312"] => "1.2.3,8312", - ["2021", "2006"] => "2021,2006", - ["1.0", "1"] => "1.0", - ["1.0", "0"] => "1.0", - ["1.2.3.4000", "4000"] => "1.2.3.4000", - ["5", "5.0.45"] => "5.0.45", - } - - expected_mappings.each do |(short_version, version), expected_version| - it "maps (#{short_version}, #{version}) to #{expected_version}" do - expect(described_class.decide_between_versions(short_version, version)) - .to eq expected_version - end - end - end -end diff --git a/Library/Homebrew/unversioned_cask_checker.rb b/Library/Homebrew/unversioned_cask_checker.rb index d6e18c6e37..679782d6a7 100644 --- a/Library/Homebrew/unversioned_cask_checker.rb +++ b/Library/Homebrew/unversioned_cask_checker.rb @@ -1,6 +1,7 @@ # typed: true # frozen_string_literal: true +require "bundle_version" require "cask/cask" require "cask/installer" @@ -45,56 +46,6 @@ module Homebrew pkgs.count == 1 end - sig { params(info_plist_path: Pathname).returns(T.nilable(String)) } - def self.version_from_info_plist(info_plist_path) - plist = system_command!("plutil", args: ["-convert", "xml1", "-o", "-", info_plist_path]).plist - - short_version = plist["CFBundleShortVersionString"].presence - version = plist["CFBundleVersion"].presence - - return decide_between_versions(short_version, version) if short_version && version - end - - sig { params(package_info_path: Pathname).returns(T.nilable(String)) } - def self.version_from_package_info(package_info_path) - Homebrew.install_bundler_gems! - require "nokogiri" - - xml = Nokogiri::XML(package_info_path.read) - - bundle_id = xml.xpath("//pkg-info//bundle-version//bundle").first&.attr("id") - return unless bundle_id - - bundle = xml.xpath("//pkg-info//bundle").find { |b| b["id"] == bundle_id } - return unless bundle - - short_version = bundle["CFBundleShortVersionString"] - version = bundle["CFBundleVersion"] - - return decide_between_versions(short_version, version) if short_version && version - end - - sig do - params(short_version: T.nilable(String), version: T.nilable(String)) - .returns(T.nilable(String)) - end - def self.decide_between_versions(short_version, version) - return short_version if short_version == version - - if short_version && version - return version if version.match?(/\A\d+(\.\d+)+\Z/) && version.start_with?("#{short_version}.") - return short_version if short_version.match?(/\A\d+(\.\d+)+\Z/) && short_version.start_with?("#{version}.") - - if short_version.match?(/\A\d+(\.\d+)*\Z/) && version.match?(/\A\d+\Z/) - return short_version if short_version.start_with?("#{version}.") || short_version.end_with?(".#{version}") - - return "#{short_version},#{version}" - end - end - - short_version || version - end - sig { returns(T.nilable(String)) } def guess_cask_version if apps.empty? && pkgs.empty? @@ -120,7 +71,7 @@ module Homebrew end info_plist_paths.each do |info_plist_path| - if (version = self.class.version_from_info_plist(info_plist_path)) + if (version = BundleVersion.from_info_plist(info_plist_path)&.nice_version) return version end end @@ -149,7 +100,7 @@ module Homebrew package_info_path = extract_dir/"PackageInfo" if package_info_path.exist? - if (version = self.class.version_from_package_info(package_info_path)) + if (version = BundleVersion.from_package_info(package_info_path)&.nice_version) return version end elsif packages.count == 1