Browse Source
this is a first draft of an idea i've had kicking around for awhile pushing out some code so i can get a conversation started
commit
5a0d1d5556
14 changed files with 608 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||
|
class Alfred < Cask |
||||
|
url 'http://rwc.cachefly.net/alfred_1.1_189.dmg' |
||||
|
homepage 'http://www.alfredapp.com/' |
||||
|
version '1.1_189' |
||||
|
end |
@ -0,0 +1,5 @@ |
|||||
|
class Dropbox < Cask |
||||
|
url 'http://dl-web.dropbox.com/u/17/Dropbox%201.2.52.dmg' |
||||
|
homepage 'http://www.dropbox.com/' |
||||
|
version '1.2.52' |
||||
|
end |
@ -0,0 +1,5 @@ |
|||||
|
class GoogleChrome < Cask |
||||
|
url 'https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg' |
||||
|
homepage 'https://www.google.com/chrome/' |
||||
|
version '17.0.963.56' |
||||
|
end |
@ -0,0 +1,5 @@ |
|||||
|
class KeepassX < Cask |
||||
|
url 'http://downloads.sourceforge.net/keepassx/KeePassX-0.4.3.dmg' |
||||
|
homepage 'http://www.keepassx.org' |
||||
|
version '0.4.3' |
||||
|
end |
@ -0,0 +1,5 @@ |
|||||
|
class NvAlt < Cask |
||||
|
url 'http://brettterpstra.com/downloads/nvalt2.1.zip?9d7bd4' |
||||
|
homepage 'http://brettterpstra.com/project/nvalt/' |
||||
|
version '2.1' |
||||
|
end |
@ -0,0 +1,101 @@ |
|||||
|
# "To install, drag this icon..." no more! |
||||
|
|
||||
|
Let's see if we can get the elegance, simplicity, and speed of Homebrew for the |
||||
|
installation and management GUI Mac applications like Google Chrome and Adium. |
||||
|
|
||||
|
`brew-cask` provides a friendly homebrew-style CLI workflow for the |
||||
|
administration of Mac applications distributed as binaries. |
||||
|
|
||||
|
It's implemented as a `homebrew` "[external |
||||
|
command](https://github.com/mxcl/homebrew/wiki/External-Commands)" called |
||||
|
`cask`. |
||||
|
|
||||
|
# Let's try it! |
||||
|
|
||||
|
## Install and set up brew-cask |
||||
|
|
||||
|
This is still a little ornery. You'll probably want to also see "Known |
||||
|
Ugliness" below. |
||||
|
|
||||
|
# be sure you have Homebrew installed first |
||||
|
|
||||
|
$ git clone https://github.com/phinze/brew-cask |
||||
|
$ ln -s brew-cask/bin/brew-cask.rb ~/bin # or anywhere in your $PATH |
||||
|
$ ln -s brew-cask/Casks /usr/local/Library/ |
||||
|
|
||||
|
## Now let's install something |
||||
|
|
||||
|
Let's see if there's a Cask for Chrome: |
||||
|
|
||||
|
$ brew cask search chrome |
||||
|
google-chrome |
||||
|
|
||||
|
Cool, there it is. Let's install it. |
||||
|
|
||||
|
$ brew cask install google-chrome |
||||
|
Downloading... |
||||
|
Success! google-chrome installed to /usr/local/Cellar/google-chrome/17.0.963.56 |
||||
|
|
||||
|
Now we have `Google Chrome.app` in our Cellar, let's get it linked somewhere useful: |
||||
|
|
||||
|
$ brew cask linkapps |
||||
|
/Users/phinze/Applications/Google Chrome.app -> /usr/local/Cellar/google-chrome/17.0.963.56/Google Chrome.app |
||||
|
|
||||
|
And there we have it. Google Chrome installed with a few quick commands; no clicking, no dragging, no dropping. |
||||
|
|
||||
|
open "~/Applications/Google Chrome.app" |
||||
|
|
||||
|
# What is a Cask? |
||||
|
|
||||
|
A `Cask` is like a `Formula` in Homebrew except it describes how to download |
||||
|
and install a binary application. |
||||
|
|
||||
|
Casks have two important fields: |
||||
|
|
||||
|
* __url__: (required) points to binary distribution of the application |
||||
|
* __version__: (required) describes the version of the application available at the URL |
||||
|
|
||||
|
# What Casks are available? |
||||
|
|
||||
|
Just run `brew cask search` with no arguments to get a list. |
||||
|
|
||||
|
Here's the current list: |
||||
|
|
||||
|
<pre> |
||||
|
alfred |
||||
|
dropbox |
||||
|
google-chrome |
||||
|
keepass-x |
||||
|
nv-alt |
||||
|
</pre> |
||||
|
|
||||
|
# What's the status of this project? Where's it headed? |
||||
|
|
||||
|
It's really just a start at this point, but it works, and I've got big plans! |
||||
|
|
||||
|
`brew-cask` currently understands how to install `dmg` and `zip` files that |
||||
|
contain a `app` file. I'd like to extend it to be able to handle `pkg` files |
||||
|
as well as the numerous other permutations of compression and distribution in |
||||
|
the wild (`app` inside `dmg` inside `zip`; folder inside `dmg`; etc.). |
||||
|
|
||||
|
I plan to use the `Cask` model to allow per-project customization of behavior, |
||||
|
like Homebrew does with `Formula`. This would allow weirdo applications like |
||||
|
Eclipse ("really you want me to drag that whole folder to `Applications`?") to |
||||
|
contain their complexity. |
||||
|
|
||||
|
Each Cask will then encapsulate and automate the story of how a given |
||||
|
application should be installed. If all goes well - I'm hoping to build up a |
||||
|
community-maintained collection of Casks that becomes the standard way that |
||||
|
hackers install Mac apps. |
||||
|
|
||||
|
# Known Ugliness |
||||
|
|
||||
|
The interactions with Old Uncle Homebrew are a little funky at this point. I'm |
||||
|
still playing with sharing Homebrew's Cellar (which we do in the current |
||||
|
implementation). This means that `cask` applications show up in regular old |
||||
|
`brew list`, and can be `unlink`ed and `uninstall`ed by `brew`. But `cask` |
||||
|
apps are not regular formula, so they won't show up in `brew search` and `brew |
||||
|
info` will not return anything for you. |
||||
|
|
||||
|
So there's some coolness out of playing in @mxcl's playground, but also some |
||||
|
confusing behavior. We'll see how it plays out. |
@ -0,0 +1,4 @@ |
|||||
|
$LOAD_PATH << File.expand_path('../../lib', Pathname.new(__FILE__).realpath) |
||||
|
require 'cask' |
||||
|
|
||||
|
Cask::CLI.process(ARGV) |
@ -0,0 +1,162 @@ |
|||||
|
require 'download_strategy' |
||||
|
require 'plist/parser' |
||||
|
require 'uri' |
||||
|
|
||||
|
HOME_APPS = Pathname.new(File.expand_path("~/Applications")) |
||||
|
|
||||
|
class Cask; end |
||||
|
|
||||
|
require 'cask/cli' |
||||
|
require 'cask/cli/install' |
||||
|
require 'cask/cli/linkapps' |
||||
|
require 'cask/cli/list' |
||||
|
require 'cask/cli/search' |
||||
|
require 'plist/parser' |
||||
|
|
||||
|
class Cask |
||||
|
def self.path |
||||
|
HOMEBREW_PREFIX.join("Library", "Casks") |
||||
|
end |
||||
|
|
||||
|
def self.cellarpath |
||||
|
HOMEBREW_CELLAR |
||||
|
end |
||||
|
|
||||
|
def self.all |
||||
|
cask_titles = path.entries.map(&:to_s).grep(/.rb$/).map { |p| p.to_s.split('.').first } |
||||
|
cask_titles.map { |c| self.load(c) } |
||||
|
end |
||||
|
|
||||
|
def self.init |
||||
|
path.mkpath |
||||
|
HOMEBREW_CACHE.mkpath |
||||
|
HOME_APPS.mkpath |
||||
|
end |
||||
|
|
||||
|
def self.homepage(homepage=nil) |
||||
|
@homepage ||= homepage |
||||
|
end |
||||
|
def homepage; self.class.homepage; end |
||||
|
|
||||
|
def self.installed |
||||
|
self.all.select(&:installed?) |
||||
|
end |
||||
|
|
||||
|
def self.load(cask_title) |
||||
|
require path.join(cask_title) |
||||
|
const_get(cask_title.split('-').map(&:capitalize).join).new |
||||
|
end |
||||
|
|
||||
|
def self.title |
||||
|
self.name.gsub(/([a-z\d])([A-Z])/,'\1-\2').downcase |
||||
|
end |
||||
|
|
||||
|
def self.url(url=nil) |
||||
|
@url ||= URI.parse(url) |
||||
|
end |
||||
|
def url; self.class.url; end |
||||
|
|
||||
|
attr_reader :title |
||||
|
def initialize(title=self.class.title) |
||||
|
@title = title |
||||
|
end |
||||
|
|
||||
|
def self.version(version=nil) |
||||
|
@version ||= version |
||||
|
end |
||||
|
def version; self.class.version; end |
||||
|
# def version |
||||
|
# Pathname.new(self.url.path.to_s).version |
||||
|
# end |
||||
|
|
||||
|
VALID_SUFFIXES = ['dmg', 'pkg', 'app'] |
||||
|
|
||||
|
def destination_path |
||||
|
HOMEBREW_CELLAR.join(self.title).join(self.version) |
||||
|
end |
||||
|
|
||||
|
def install |
||||
|
downloader = CurlDownloadStrategy.new(self.url.to_s, self.title, self.version, {}) |
||||
|
downloaded_path = downloader.fetch |
||||
|
|
||||
|
FileUtils.mkdir_p destination_path |
||||
|
|
||||
|
_with_extracted_mountpoints(downloaded_path) do |mountpoint| |
||||
|
puts `ditto #{mountpoint} #{destination_path}` |
||||
|
end |
||||
|
|
||||
|
puts "Success! #{self} installed to #{destination_path}" |
||||
|
end |
||||
|
|
||||
|
def linkapps |
||||
|
destination_path.entries.select { |f| f.basename.to_s =~ /.app$/ }.each do |app| |
||||
|
symlink_destination = HOME_APPS.join(app.basename) |
||||
|
symlink_target = destination_path.join(app) |
||||
|
if symlink_destination.directory? || symlink_destination.file? |
||||
|
puts "#{symlink_destination} already exists and is not a symlink, not linking #{self}" |
||||
|
elsif symlink_destination.symlink? |
||||
|
puts "#{symlink_destination} exists but is symlink; removing and relinking" |
||||
|
puts "#{symlink_destination} -> #{symlink_target}" |
||||
|
symlink_destination.delete |
||||
|
symlink_destination.make_symlink(symlink_destination) |
||||
|
else |
||||
|
puts "#{symlink_destination} -> #{symlink_target}" |
||||
|
symlink_destination.make_symlink(symlink_destination) |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def installed? |
||||
|
return false unless destination_path.exist? |
||||
|
destination_path.entries.any? do |f| |
||||
|
f.basename.to_s =~ /.app$/ |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def _with_extracted_mountpoints(path) |
||||
|
if _dmg?(path) |
||||
|
File.open(path) do |dmg| |
||||
|
xml_str = `hdiutil mount -plist -nobrowse -readonly -noidme -mountrandom /tmp #{dmg.path}` |
||||
|
hdiutil_info = Plist::parse_xml(xml_str) |
||||
|
raise Exception.new("No disk entities returned by mount at #{dmg.path}") unless hdiutil_info.has_key?("system-entities") |
||||
|
mounts = hdiutil_info["system-entities"].collect { |entity| |
||||
|
entity["mount-point"] |
||||
|
}.compact |
||||
|
begin |
||||
|
mounts.each do |mountpoint| |
||||
|
yield Pathname.new(mountpoint) |
||||
|
end |
||||
|
ensure |
||||
|
mounts.each do |mountpoint| |
||||
|
`hdiutil eject #{mountpoint}` |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
elsif _zip?(path) |
||||
|
destdir = "/tmp/brewcask_#{@title}_extracted" |
||||
|
`mkdir -p #{destdir}` |
||||
|
`unzip -d '#{destdir}' '#{path}'` |
||||
|
begin |
||||
|
yield destdir |
||||
|
ensure |
||||
|
`rm -rf '#{destdir}'` |
||||
|
end |
||||
|
else |
||||
|
raise "uh oh, could not identify type of #{path}" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def _dmg?(path) |
||||
|
output = `hdiutil imageinfo #{path} 2>/dev/null` |
||||
|
output != '' |
||||
|
end |
||||
|
|
||||
|
def _zip?(path) |
||||
|
output = `file -Izb #{path}` |
||||
|
output.chomp == 'application/x-empty compressed-encoding=application/zip; charset=binary; charset=binary' |
||||
|
end |
||||
|
|
||||
|
def to_s |
||||
|
@title |
||||
|
end |
||||
|
end |
@ -0,0 +1,52 @@ |
|||||
|
class Cask::CLI |
||||
|
def self.commands |
||||
|
Cask::CLI.constants - ["NullCommand"] |
||||
|
end |
||||
|
|
||||
|
def self.lookup_command(command) |
||||
|
if command && Cask::CLI.const_defined?(command.capitalize) |
||||
|
Cask::CLI.const_get(command.capitalize) |
||||
|
else |
||||
|
Cask::CLI::NullCommand.new(command) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
def self.process(arguments) |
||||
|
Cask.init |
||||
|
command, *rest = *arguments |
||||
|
lookup_command(command).run(*rest) |
||||
|
end |
||||
|
|
||||
|
class NullCommand |
||||
|
def initialize(attempted_name) |
||||
|
@attempted_name = attempted_name |
||||
|
end |
||||
|
|
||||
|
def run(*args) |
||||
|
purpose |
||||
|
if @attempted_name |
||||
|
puts "!! " |
||||
|
puts "!! no command with name: #{@attempted_name}" |
||||
|
puts "!! " |
||||
|
end |
||||
|
usage |
||||
|
end |
||||
|
|
||||
|
def purpose |
||||
|
puts <<-PURPOSE.gsub(/^ {6}/, '') |
||||
|
{{ brew-cask }} |
||||
|
brew-cask provides a friendly homebrew-style CLI workflow for the |
||||
|
administration Mac applications distributed as binaries |
||||
|
PURPOSE |
||||
|
end |
||||
|
|
||||
|
def usage |
||||
|
puts "available commands: " |
||||
|
puts Cask::CLI.commands.map {|c| " - #{c.downcase}: #{_help_for(c)}"}.join("\n") |
||||
|
end |
||||
|
|
||||
|
def _help_for(command) |
||||
|
Cask::CLI.lookup_command(command).help |
||||
|
end |
||||
|
end |
||||
|
end |
@ -0,0 +1,11 @@ |
|||||
|
class Cask::CLI::Install |
||||
|
def self.run(*arguments) |
||||
|
cask_name, *rest = *arguments |
||||
|
cask = Cask.load(cask_name) |
||||
|
cask.install |
||||
|
end |
||||
|
|
||||
|
def self.help |
||||
|
"installs the cask of the given name" |
||||
|
end |
||||
|
end |
@ -0,0 +1,9 @@ |
|||||
|
class Cask::CLI::Linkapps |
||||
|
def self.run(*arguments) |
||||
|
Cask.installed.map(&:linkapps) |
||||
|
end |
||||
|
|
||||
|
def self.help |
||||
|
"makes a symlink from all cask-installed .app files into ~/Applications" |
||||
|
end |
||||
|
end |
@ -0,0 +1,9 @@ |
|||||
|
class Cask::CLI::List |
||||
|
def self.run(*arguments) |
||||
|
puts Cask.installed.map(&:to_s).join("\n") |
||||
|
end |
||||
|
|
||||
|
def self.help |
||||
|
"lists installed casks" |
||||
|
end |
||||
|
end |
@ -0,0 +1,10 @@ |
|||||
|
class Cask::CLI::Search |
||||
|
def self.run(*arguments) |
||||
|
search_term, *rest = *arguments |
||||
|
puts Cask.all.map(&:to_s).grep(/#{search_term}/).join("\n") |
||||
|
end |
||||
|
|
||||
|
def self.help |
||||
|
"searches all known casks" |
||||
|
end |
||||
|
end |
@ -0,0 +1,225 @@ |
|||||
|
# |
||||
|
# = plist |
||||
|
# |
||||
|
# Copyright 2006-2010 Ben Bleything and Patrick May |
||||
|
# Distributed under the MIT License |
||||
|
# |
||||
|
|
||||
|
# Plist parses Mac OS X xml property list files into ruby data structures. |
||||
|
# |
||||
|
# === Load a plist file |
||||
|
# This is the main point of the library: |
||||
|
# |
||||
|
# r = Plist::parse_xml( filename_or_xml ) |
||||
|
module Plist |
||||
|
# Note that I don't use these two elements much: |
||||
|
# |
||||
|
# + Date elements are returned as DateTime objects. |
||||
|
# + Data elements are implemented as Tempfiles |
||||
|
# |
||||
|
# Plist::parse_xml will blow up if it encounters a data element. |
||||
|
# If you encounter such an error, or if you have a Date element which |
||||
|
# can't be parsed into a Time object, please send your plist file to |
||||
|
# plist@hexane.org so that I can implement the proper support. |
||||
|
def Plist::parse_xml( filename_or_xml ) |
||||
|
listener = Listener.new |
||||
|
#parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) |
||||
|
parser = StreamParser.new(filename_or_xml, listener) |
||||
|
parser.parse |
||||
|
listener.result |
||||
|
end |
||||
|
|
||||
|
class Listener |
||||
|
#include REXML::StreamListener |
||||
|
|
||||
|
attr_accessor :result, :open |
||||
|
|
||||
|
def initialize |
||||
|
@result = nil |
||||
|
@open = Array.new |
||||
|
end |
||||
|
|
||||
|
|
||||
|
def tag_start(name, attributes) |
||||
|
@open.push PTag::mappings[name].new |
||||
|
end |
||||
|
|
||||
|
def text( contents ) |
||||
|
@open.last.text = contents if @open.last |
||||
|
end |
||||
|
|
||||
|
def tag_end(name) |
||||
|
last = @open.pop |
||||
|
if @open.empty? |
||||
|
@result = last.to_ruby |
||||
|
else |
||||
|
@open.last.children.push last |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class StreamParser |
||||
|
def initialize( plist_data_or_file, listener ) |
||||
|
if plist_data_or_file.respond_to? :read |
||||
|
@xml = plist_data_or_file.read |
||||
|
elsif File.exists? plist_data_or_file |
||||
|
@xml = File.read( plist_data_or_file ) |
||||
|
else |
||||
|
@xml = plist_data_or_file |
||||
|
end |
||||
|
|
||||
|
@listener = listener |
||||
|
end |
||||
|
|
||||
|
TEXT = /([^<]+)/ |
||||
|
XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um |
||||
|
DOCTYPE_PATTERN = /\s*<!DOCTYPE\s+(.*?)(\[|>)/um |
||||
|
COMMENT_START = /\A<!--/u |
||||
|
COMMENT_END = /.*?-->/um |
||||
|
|
||||
|
|
||||
|
def parse |
||||
|
plist_tags = PTag::mappings.keys.join('|') |
||||
|
start_tag = /<(#{plist_tags})([^>]*)>/i |
||||
|
end_tag = /<\/(#{plist_tags})[^>]*>/i |
||||
|
|
||||
|
require 'strscan' |
||||
|
|
||||
|
@scanner = StringScanner.new( @xml ) |
||||
|
until @scanner.eos? |
||||
|
if @scanner.scan(COMMENT_START) |
||||
|
@scanner.scan(COMMENT_END) |
||||
|
elsif @scanner.scan(XMLDECL_PATTERN) |
||||
|
elsif @scanner.scan(DOCTYPE_PATTERN) |
||||
|
elsif @scanner.scan(start_tag) |
||||
|
@listener.tag_start(@scanner[1], nil) |
||||
|
if (@scanner[2] =~ /\/$/) |
||||
|
@listener.tag_end(@scanner[1]) |
||||
|
end |
||||
|
elsif @scanner.scan(TEXT) |
||||
|
@listener.text(@scanner[1]) |
||||
|
elsif @scanner.scan(end_tag) |
||||
|
@listener.tag_end(@scanner[1]) |
||||
|
else |
||||
|
raise "Unimplemented element" |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PTag |
||||
|
@@mappings = { } |
||||
|
def PTag::mappings |
||||
|
@@mappings |
||||
|
end |
||||
|
|
||||
|
def PTag::inherited( sub_class ) |
||||
|
key = sub_class.to_s.downcase |
||||
|
key.gsub!(/^plist::/, '' ) |
||||
|
key.gsub!(/^p/, '') unless key == "plist" |
||||
|
|
||||
|
@@mappings[key] = sub_class |
||||
|
end |
||||
|
|
||||
|
attr_accessor :text, :children |
||||
|
def initialize |
||||
|
@children = Array.new |
||||
|
end |
||||
|
|
||||
|
def to_ruby |
||||
|
raise "Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}" |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PList < PTag |
||||
|
def to_ruby |
||||
|
children.first.to_ruby if children.first |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PDict < PTag |
||||
|
def to_ruby |
||||
|
dict = Hash.new |
||||
|
key = nil |
||||
|
|
||||
|
children.each do |c| |
||||
|
if key.nil? |
||||
|
key = c.to_ruby |
||||
|
else |
||||
|
dict[key] = c.to_ruby |
||||
|
key = nil |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
dict |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
require 'cgi' |
||||
|
class PKey < PTag |
||||
|
def to_ruby |
||||
|
CGI::unescapeHTML(text || '') |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PString < PTag |
||||
|
def to_ruby |
||||
|
CGI::unescapeHTML(text || '') |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PArray < PTag |
||||
|
def to_ruby |
||||
|
children.collect do |c| |
||||
|
c.to_ruby |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PInteger < PTag |
||||
|
def to_ruby |
||||
|
text.to_i |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PTrue < PTag |
||||
|
def to_ruby |
||||
|
true |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PFalse < PTag |
||||
|
def to_ruby |
||||
|
false |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
class PReal < PTag |
||||
|
def to_ruby |
||||
|
text.to_f |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
require 'date' |
||||
|
class PDate < PTag |
||||
|
def to_ruby |
||||
|
DateTime.parse(text) |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
require 'base64' |
||||
|
class PData < PTag |
||||
|
def to_ruby |
||||
|
data = Base64.decode64(text.gsub(/\s+/, '')) |
||||
|
|
||||
|
begin |
||||
|
return Marshal.load(data) |
||||
|
rescue Exception => e |
||||
|
io = StringIO.new |
||||
|
io.write data |
||||
|
io.rewind |
||||
|
return io |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
Loading…
Reference in new issue