mirror of
https://github.com/donl/homebrew-cask-versions.git
synced 2026-07-17 06:16:47 -06:00
Class names are now completely hidden from the user. This commit works by adding a prefix to all Cask class names, which is considered to be an ugly transitional hack on the way to representing individual Casks as instances.
419 lines
13 KiB
Ruby
Executable file
419 lines
13 KiB
Ruby
Executable file
#!/usr/bin/env ruby
|
|
#
|
|
# generate_cask_token
|
|
#
|
|
# todo:
|
|
#
|
|
# detect Cask files which differ only by the placement of hyphens.
|
|
#
|
|
|
|
###
|
|
### dependencies
|
|
###
|
|
|
|
require 'pathname'
|
|
require 'open3'
|
|
|
|
begin
|
|
# not available by default
|
|
require 'active_support/inflector'
|
|
rescue LoadError
|
|
end
|
|
|
|
###
|
|
### configurable constants
|
|
###
|
|
|
|
EXPANDED_SYMBOLS = {
|
|
'+' => 'plus',
|
|
}
|
|
|
|
CASK_FILE_EXTENSION = '.rb'
|
|
|
|
# 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',
|
|
}
|
|
|
|
# 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,
|
|
]
|
|
|
|
# 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,
|
|
# idea, but never discussed
|
|
# %r{\blauncher}i,
|
|
|
|
# "mac", "for mac", "for OS X".
|
|
%r{\b(?:for)?[\s-]*mac(?:intosh)?}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
|
|
]
|
|
|
|
# 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,
|
|
]
|
|
|
|
###
|
|
### 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 self.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 and 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 and 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(/^CFBundle(?:Display)?Name\s*=\s*/).first) do |match|
|
|
match.captures.first
|
|
end
|
|
end
|
|
return AppName.new(bundle_name) if bundle_name and 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 and bundle_name.ascii_only?
|
|
|
|
self
|
|
end
|
|
|
|
def basename
|
|
if Pathname.new(self).exist? then
|
|
AppName.new(Pathname.new(self).basename.to_s)
|
|
else
|
|
self
|
|
end
|
|
end
|
|
|
|
def remove_extension
|
|
self.sub(/\.app\Z/i, '')
|
|
end
|
|
|
|
def decompose_to_ascii
|
|
# crudely (and incorrectly) decompose extended latin characters to ASCII
|
|
return self if self.ascii_only?
|
|
return self unless self.respond_to?(:mb_chars)
|
|
AppName.new(self.mb_chars.normalize(:kd).each_char.select(&:ascii_only?).join)
|
|
end
|
|
|
|
def hardcoded_exception
|
|
APP_EXCEPTION_PATS.each do |regexp, exception|
|
|
if regexp.match(self) then
|
|
return AppName.new(exception)
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
def insert_vertical_tabs_for_camel_case
|
|
app_name = AppName.new(self)
|
|
if app_name.sub!(/(#{self.class.preserve_trailing_pat})\Z/i, '')
|
|
trailing = $1
|
|
end
|
|
app_name.gsub!(/([^A-Z])([A-Z])/, "\\1\v\\2")
|
|
app_name.sub!(/\Z/, trailing) if trailing
|
|
app_name
|
|
end
|
|
|
|
def insert_vertical_tabs_for_snake_case
|
|
self.gsub(/_/, "\v")
|
|
end
|
|
|
|
def clean_up_vertical_tabs
|
|
self.gsub(/\v/, '')
|
|
end
|
|
|
|
def remove_interior_versions!
|
|
# done separately from REMOVE_TRAILING_PATS because this
|
|
# requires a substitution with a backreference
|
|
self.sub!(%r{(?<=.)[\.\d]+(#{self.class.after_interior_version_pat})\Z}i, '\1')
|
|
self.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 = self.insert_vertical_tabs_for_camel_case
|
|
.insert_vertical_tabs_for_snake_case
|
|
while self.class.remove_trailing_pat.match(app_name) and
|
|
not 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 = self.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
|
|
self.gsub(/ +/, '-')
|
|
end
|
|
|
|
def delete_invalid_chars
|
|
self.gsub(/[^a-z0-9-]+/, '')
|
|
end
|
|
|
|
def collapse_multiple_hyphens
|
|
self.gsub(/--+/, '-')
|
|
end
|
|
|
|
def delete_leading_hyphens
|
|
self.gsub(/^--+/, '')
|
|
end
|
|
|
|
def delete_hyphens_before_numbers
|
|
self.gsub(/-([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(/ +\Z/, '')
|
|
end
|
|
|
|
def add_extension
|
|
self.sub(/(?:#{escaped_cask_file_extension})?\Z/i, CASK_FILE_EXTENSION)
|
|
end
|
|
|
|
def remove_extension
|
|
self.sub(/#{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(self.remove_extension)
|
|
self.remove_extension
|
|
else
|
|
self.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" unless @from_simplified_app_name.length > 0
|
|
@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}".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}.match(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 :v1 => '#{cask_token}' do"
|
|
if warnings.length > 0
|
|
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
|