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
phinze
13 years ago
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