aboutsummaryrefslogtreecommitdiffstats
path: root/Library/Homebrew/cask/developer/bin/generate_cask_token
diff options
context:
space:
mode:
Diffstat (limited to 'Library/Homebrew/cask/developer/bin/generate_cask_token')
-rwxr-xr-xLibrary/Homebrew/cask/developer/bin/generate_cask_token418
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