diff options
Diffstat (limited to 'Library/Homebrew/cask/developer/bin/generate_cask_token')
| -rwxr-xr-x | Library/Homebrew/cask/developer/bin/generate_cask_token | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/Library/Homebrew/cask/developer/bin/generate_cask_token b/Library/Homebrew/cask/developer/bin/generate_cask_token new file mode 100755 index 000000000..48e933136 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/generate_cask_token @@ -0,0 +1,418 @@ +#!/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby +# +# generate_cask_token +# +# todo: +# +# remove Ruby 2.0 dependency and change shebang line +# +# detect Cask files which differ only by the placement of hyphens. +# +# merge entirely into "brew cask create" command +# + +### +### dependencies +### + +require "pathname" +require "open3" + +begin + # not available by default + require "active_support/inflector" +rescue LoadError +end + +### +### configurable constants +### + +EXPANDED_SYMBOLS = { + "+" => "plus", + "@" => "at", + }.freeze + +CASK_FILE_EXTENSION = ".rb".freeze + +# Hardcode App names that cannot be transformed automatically. +# Example: in "x48.app", "x48" is not a version number. +# The value in the hash should be a valid Cask token. +APP_EXCEPTION_PATS = { + # looks like a trailing version, but is not. + %r{\Aiterm\Z}i => "iterm2", + %r{\Aiterm2\Z}i => "iterm2", + %r{\Apgadmin3\Z}i => "pgadmin3", + %r{\Ax48\Z}i => "x48", + %r{\Avitamin-r[\s\d\.]*\Z}i => "vitamin-r", + %r{\Aimagealpha\Z}i => "imagealpha", + # upstream is in the midst of changing branding + %r{\Abitcoin-?qt\Z}i => "bitcoin-core", + # "mac" cannot be separated from the name because it is in an English phrase + %r{\Aplayonmac\Z}i => "playonmac", + %r{\Acleanmymac[\s\d\.]*\Z}i => "cleanmymac", + # arguably we should not have kept these two exceptions + %r{\Akismac\Z}i => "kismac", + %r{\Avoicemac\Z}i => "voicemac", + }.freeze + +# Preserve trailing patterns on App names that could be mistaken +# for version numbers, etc +PRESERVE_TRAILING_PATS = [ + %r{id3}i, + %r{mp3}i, + %r{3[\s-]*d}i, + %r{diff3}i, + %r{\A[^\d]+\+\Z}i, + ].freeze + +# The code that employs these patterns against App names +# - hacks a \b (word-break) between CamelCase and snake_case transitions +# - anchors the pattern to end-of-string +# - applies the patterns repeatedly until there is no match +REMOVE_TRAILING_PATS = [ + # spaces + %r{\s+}i, + + # generic terms + %r{\bapp}i, + %r{\b(?:quick[\s-]*)?launcher}i, + + # "mac", "for mac", "for OS X", "macOS", "for macOS". + %r{\b(?:for)?[\s-]*mac(?:intosh|OS)?}i, + %r{\b(?:for)?[\s-]*os[\s-]*x}i, + + # hardware designations such as "for x86", "32-bit", "ppc" + %r{(?:\bfor\s*)?x.?86}i, + %r{(?:\bfor\s*)?\bppc}i, + %r{(?:\bfor\s*)?\d+.?bits?}i, + + # frameworks + %r{\b(?:for)?[\s-]*(?:oracle|apple|sun)*[\s-]*(?:jvm|java|jre)}i, + %r{\bgtk}i, + %r{\bqt}i, + %r{\bwx}i, + %r{\bcocoa}i, + + # localizations + %r{en\s*-\s*us}i, + + # version numbers + %r{[^a-z0-9]+}i, + %r{\b(?:version|alpha|beta|gamma|release|release.?candidate)(?:[\s\.\d-]*\d[\s\.\d-]*)?}i, + %r{\b(?:v|ver|vsn|r|rc)[\s\.\d-]*\d[\s\.\d-]*}i, + %r{\d+(?:[a-z\.]\d+)*}i, + %r{\b\d+\s*[a-z]}i, + %r{\d+\s*[a-c]}i, # constrained to a-c b/c of false positives + ].freeze + +# Patterns which are permitted (undisturbed) following an interior version number +AFTER_INTERIOR_VERSION_PATS = [ + %r{ce}i, + %r{pro}i, + %r{professional}i, + %r{client}i, + %r{server}i, + %r{host}i, + %r{viewer}i, + %r{launcher}i, + %r{installer}i, + ].freeze + +### +### classes +### + +class AppName < String + def self.remove_trailing_pat + @@remove_trailing_pat ||= %r{(?<=.)(?:#{REMOVE_TRAILING_PATS.join('|')})\Z}i + end + + def self.preserve_trailing_pat + @@preserve_trailing_pat ||= %r{(?:#{PRESERVE_TRAILING_PATS.join('|')})\Z}i + end + + def self.after_interior_version_pat + @@after_interior_version_pat ||= %r{(?:#{AFTER_INTERIOR_VERSION_PATS.join('|')})}i + end + + def english_from_app_bundle + return self if ascii_only? + return self unless File.exist?(self) + + # check Info.plist CFBundleDisplayName + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleDisplayName", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + # check Info.plist CFBundleName + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleName", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + # check localization strings + local_strings_file = Pathname.new(self).join("Contents", "Resources", "en.lproj", "InfoPlist.strings") + local_strings_file = Pathname.new(self).join("Contents", "Resources", "English.lproj", "InfoPlist.strings") unless local_strings_file.exist? + if local_strings_file.exist? + bundle_name = File.open(local_strings_file, "r:UTF-16LE:UTF-8") do |fh| + %r{\ACFBundle(?:Display)?Name\s*=\s*"(.*)";\Z}.match(fh.readlines.grep(%r{^CFBundle(?:Display)?Name\s*=\s*}).first) do |match| + match.captures.first + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + end + + # check Info.plist CFBundleExecutable + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleExecutable", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + self + end + + def basename + if Pathname.new(self).exist? + AppName.new(Pathname.new(self).basename.to_s) + else + self + end + end + + def remove_extension + sub(%r{\.app\Z}i, "") + end + + def decompose_to_ascii + # crudely (and incorrectly) decompose extended latin characters to ASCII + return self if ascii_only? + return self unless respond_to?(:mb_chars) + AppName.new(mb_chars.normalize(:kd).each_char.select(&:ascii_only?).join) + end + + def hardcoded_exception + APP_EXCEPTION_PATS.each do |regexp, exception| + return AppName.new(exception) if regexp.match(self) + end + nil + end + + def insert_vertical_tabs_for_camel_case + app_name = AppName.new(self) + if app_name.sub!(%r{(#{self.class.preserve_trailing_pat})\Z}i, "") + trailing = Regexp.last_match(1) + end + app_name.gsub!(%r{([^A-Z])([A-Z])}, "\\1\v\\2") + app_name.sub!(%r{\Z}, trailing) if trailing + app_name + end + + def insert_vertical_tabs_for_snake_case + gsub(%r{_}, "\v") + end + + def clean_up_vertical_tabs + gsub(%r{\v}, "") + end + + def remove_interior_versions! + # done separately from REMOVE_TRAILING_PATS because this + # requires a substitution with a backreference + sub!(%r{(?<=.)[\.\d]+(#{self.class.after_interior_version_pat})\Z}i, '\1') + sub!(%r{(?<=.)[\s\.\d-]*\d[\s\.\d-]*(#{self.class.after_interior_version_pat})\Z}i, '-\1') + end + + def remove_trailing_strings_and_versions + app_name = insert_vertical_tabs_for_camel_case + .insert_vertical_tabs_for_snake_case + while self.class.remove_trailing_pat.match(app_name) && + !self.class.preserve_trailing_pat.match(app_name) + app_name.sub!(self.class.remove_trailing_pat, "") + end + app_name.remove_interior_versions! + app_name.clean_up_vertical_tabs + end + + def simplified + return @simplified if @simplified + @simplified = english_from_app_bundle + .basename + .decompose_to_ascii + .remove_extension + @simplified = @simplified.hardcoded_exception || @simplified.remove_trailing_strings_and_versions + @simplified + end +end + +class CaskFileName < String + def spaces_to_hyphens + gsub(%r{ +}, "-") + end + + def delete_invalid_chars + gsub(%r{[^a-z0-9-]+}, "") + end + + def collapse_multiple_hyphens + gsub(%r{--+}, "-") + end + + def delete_leading_hyphens + gsub(%r{^--+}, "") + end + + def delete_hyphens_before_numbers + gsub(%r{-([0-9])}, '\1') + end + + def spell_out_symbols + cask_file_name = self + EXPANDED_SYMBOLS.each do |k, v| + cask_file_name.gsub!(k, " #{v} ") + end + cask_file_name.sub(%r{ +\Z}, "") + end + + def add_extension + sub(%r{(?:#{escaped_cask_file_extension})?\Z}i, CASK_FILE_EXTENSION) + end + + def remove_extension + sub(%r{#{escaped_cask_file_extension}\Z}i, "") + end + + def from_simplified_app_name + return @from_simplified_app_name if @from_simplified_app_name + @from_simplified_app_name = if APP_EXCEPTION_PATS.rassoc(remove_extension) + remove_extension + else + remove_extension + .downcase + .spell_out_symbols + .spaces_to_hyphens + .delete_invalid_chars + .collapse_multiple_hyphens + .delete_leading_hyphens + .delete_hyphens_before_numbers + end + raise "Could not determine Simplified App name" if @from_simplified_app_name.empty? + @from_simplified_app_name.add_extension + end +end + +### +### methods +### + +def project_root + Dir.chdir File.dirname(File.expand_path(__FILE__)) + @git_root ||= Open3.popen3(*%w[ + git rev-parse --show-toplevel + ]) do |_stdin, stdout, _stderr| + begin + Pathname.new(stdout.gets.chomp) + rescue + raise "could not find project root" + end + end + raise "could not find project root" unless @git_root.exist? + @git_root +end + +def escaped_cask_file_extension + @escaped_cask_file_extension ||= Regexp.escape(CASK_FILE_EXTENSION) +end + +def simplified_app_name + @simplified_app_name ||= AppName.new(ARGV.first.dup.force_encoding("UTF-8")).simplified +end + +def cask_file_name + @cask_file_name ||= CaskFileName.new(simplified_app_name).from_simplified_app_name +end + +def cask_token + @cask_token ||= cask_file_name.remove_extension +end + +def warnings + return @warnings if @warnings + @warnings = [] + unless APP_EXCEPTION_PATS.rassoc(cask_token) + if %r{\d} =~ cask_token + @warnings.push "WARNING: '#{cask_token}' contains digits. Digits which are version numbers should be removed." + end + end + filename = project_root.join("Casks", cask_file_name) + if filename.exist? + @warnings.push "WARNING: the file '#{filename}' already exists. Prepend the vendor name if this is not a duplicate." + end + @warnings +end + +def report + puts "Proposed Simplified App name: #{simplified_app_name}" if $debug + puts "Proposed token: #{cask_token}" + puts "Proposed file name: #{cask_file_name}" + puts "Cask Header Line: cask '#{cask_token}' do" + unless warnings.empty? + $stderr.puts "\n" + $stderr.puts warnings + $stderr.puts "\n" + exit 1 + end +end + +### +### main +### + +usage = <<-EOS +Usage: generate_cask_token [ -debug ] <application.app> + +Given an Application name or a path to an Application, propose a +Cask token, filename, and header line. + +With -debug, also provide the internal "Simplified App Name". + +EOS + +if ARGV.first =~ %r{^-+h(elp)?$}i + puts usage + exit 0 +end + +if ARGV.first =~ %r{^-+debug?$}i + $debug = 1 + ARGV.shift +end + +unless ARGV.length == 1 + puts usage + exit 1 +end + +report |
