Browse Source

quick sketchy first release of code/ideas

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
  1. 5
      Casks/alfred.rb
  2. 5
      Casks/dropbox.rb
  3. 5
      Casks/google-chrome.rb
  4. 5
      Casks/keepass-x.rb
  5. 5
      Casks/nv-alt.rb
  6. 101
      README.md
  7. 4
      bin/brew-cask.rb
  8. 162
      lib/cask.rb
  9. 52
      lib/cask/cli.rb
  10. 11
      lib/cask/cli/install.rb
  11. 9
      lib/cask/cli/linkapps.rb
  12. 9
      lib/cask/cli/list.rb
  13. 10
      lib/cask/cli/search.rb
  14. 225
      lib/plist/parser.rb

5
Casks/alfred.rb

@ -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

5
Casks/dropbox.rb

@ -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

5
Casks/google-chrome.rb

@ -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

5
Casks/keepass-x.rb

@ -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

5
Casks/nv-alt.rb

@ -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

101
README.md

@ -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.

4
bin/brew-cask.rb

@ -0,0 +1,4 @@
$LOAD_PATH << File.expand_path('../../lib', Pathname.new(__FILE__).realpath)
require 'cask'
Cask::CLI.process(ARGV)

162
lib/cask.rb

@ -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

52
lib/cask/cli.rb

@ -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

11
lib/cask/cli/install.rb

@ -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

9
lib/cask/cli/linkapps.rb

@ -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

9
lib/cask/cli/list.rb

@ -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

10
lib/cask/cli/search.rb

@ -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

225
lib/plist/parser.rb

@ -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…
Cancel
Save