tap_dance 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MmRkZjkyMDQ0NWQ0NGE1MmNjMDRhYTBhY2EwMDQ5ZmUyMDMxNDk2MA==
5
+ data.tar.gz: !binary |-
6
+ YzM1YTQzODBjYTk4OTFiZmRmMjE2ODAzODcwM2U2MTdhNWFlMmUxZQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NDg0NGJhZjE0OTA3ZTYyN2E1NzFkOTA3NWE2YjNiOGVlNTY1YmRlZmIxOGIw
10
+ ZmFmMTNmZDU0NTdjYjY3MjdmMzI5YjcxNjdkMWMzZWNkYjY0M2VkMzlkODQx
11
+ N2NkZGQzYzRhMzQ5MjJkZDU4NWJiYTE5OWExNmVkMWRjMjEyZjA=
12
+ data.tar.gz: !binary |-
13
+ NWJhMTcwNWNlMjU0ODc0ZDYwMWMwZWRiNDQ5MDJjYzAwZGMyYjUzNzZhYzRj
14
+ MDA4MDUyZWNmMzRhNzNjMTNhMDhmZmI3NDczYjEzZjU5NmNiNjJjNmYyMjlm
15
+ ZTZkZTA1MDAwYzVjMzQyMDNmNmIyOGRmMDI4MDFkYzZlNDU4YWQ=
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tap_dance.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+
2
+ guard :rspec do
3
+ watch(%r{^spec/.+_spec\.rb$})
4
+ watch(%r{^bin/(.+)$}) { |m| [ "spec/bin/#{m[1]}_spec.rb" ] }
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| [ "spec" ] }
6
+ # "spec/lib/#{m[1]}_spec.rb"
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jonathan Martin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,72 @@
1
+ [![Gem Version](https://badge.fury.io/rb/tap_dance.png)](http://badge.fury.io/rb/tap_dance)
2
+ [![Dependency Status](https://gemnasium.com/nybblr/tap_dance.png)](https://gemnasium.com/nybblr/tap_dance)
3
+ [![Code Climate](https://codeclimate.com/github/nybblr/tap_dance.png)](https://codeclimate.com/github/nybblr/tap_dance)
4
+
5
+ Tap Dance: Homebrew meets Bundler
6
+ ===============================
7
+
8
+ Homebrew rocks, but managing system level dependencies can become a pain to track across multiple machines. Bundler was designed to make explicit declaration of Ruby dependencies consistent and painless. Tap Dance aims to do the same for system binaries by alleviating such issues as:
9
+ - "What did I `brew install` to make my vim plugins work?"
10
+ - "With what extra arguments did I run my python installation?"
11
+ - "Did I install that item in `brew list` for a reason I don't remember, or is it an automatically installed dependency?"
12
+ - "Ugh, I just want to pull my latest dotfiles and have everything work."
13
+
14
+ Installation
15
+ ------------
16
+
17
+ The "best" way to install tap dance is by including it in your system-wide Gemfile (e.g. a Gemfile in your dotfiles).
18
+
19
+ Add this line to your Gemfile:
20
+
21
+ gem 'tap_dance'
22
+
23
+ And then execute:
24
+
25
+ $ bundle
26
+
27
+ If you don't version-control your dots (which you should), install it yourself as:
28
+
29
+ $ gem install tap_dance
30
+
31
+ Usage
32
+ -----
33
+
34
+ TODO: Write usage instructions here
35
+
36
+ What's up with the name?
37
+ ------------------------
38
+
39
+ Because declaring system and project deps with an awesome ruby gem makes me want to dance. Although I actually prefer swing and waltz.
40
+
41
+ Features
42
+ --------
43
+
44
+ Limit by:
45
+ - matching computer name
46
+ - environment variable
47
+ - shell script
48
+ - if installed
49
+
50
+ Groups
51
+
52
+ Credit where due!
53
+ -----------------
54
+
55
+ Huge thanks to Yehuda Katz for his awesome work on Bundler. I shamelessly stole a lot of the feature ideas and Gemfile evaluation magic from it. Oh, and obviously since the gem is built around Homebrew, a big thanks to Max and the community around brew: it has redefined my machine setup workflow.
56
+
57
+ Contributing
58
+ ------------
59
+
60
+ I love contributions: give me a hand by including an in-depth description of the problem fixed or feature implemented, with usage information if applicable.
61
+
62
+ Most importantly, please include tests: if the request fixes a bug, include one or more failing tests which cover the issue. For new features, try to cover the basic functionalities, making sure the tests can pass on any properly configured OSX workstation.
63
+
64
+ **Steps to contribution awesomeness:**
65
+
66
+ 1. Fork it
67
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
68
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
69
+ 4. Push to the branch (`git push origin my-new-feature`)
70
+ 5. Create new Pull Request
71
+
72
+ Although you could just make your commits on the master branch, it's easier to inspect merge histories with actual feature branches. It also makes the bounds of your contribution obvious. Did I mention it looks better in `git log --oneline --graph`
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/dance ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Add lib, ext to load path
4
+ root = File.expand_path '../..', __FILE__
5
+ [:lib, :ext].each do |dir|
6
+ path = File.join root, dir.to_s
7
+ $:.unshift(path) unless $:.include?(path)
8
+ end
9
+
10
+ require 'rubygems'
11
+ require 'thor'
12
+
13
+ require "tap_dance"
14
+
15
+ TapDance::CLI.start
@@ -0,0 +1,7 @@
1
+ tap :dev, "nybblr/dev"
2
+ # tap :dart, "dartsim/dart" do
3
+ # brew "dartsim"
4
+ # end
5
+
6
+ brew "sack", :tap => :dev
7
+ brew "vim", :flags => { :with => [:ruby, :perl, :python] }
data/ext/array.rb ADDED
@@ -0,0 +1,18 @@
1
+ class Array
2
+ # Simple implementation of hash
3
+ # squash for Hash#squash. See the
4
+ # Hash notes for more explanation.
5
+ #
6
+ # For Array, we simply call squash
7
+ # on each item.
8
+
9
+ def squash(separator=" ")
10
+ map do |m|
11
+ if m.respond_to? :squash
12
+ m.squash(separator)
13
+ else
14
+ m.to_s
15
+ end
16
+ end.flatten
17
+ end
18
+ end
data/ext/hash.rb ADDED
@@ -0,0 +1,42 @@
1
+ class Hash
2
+ # Converts a deeply nested hash
3
+ # of stringish keys and values
4
+ # to an array of flattened branches
5
+ #
6
+ # For example, consider:
7
+ #
8
+ # hash = {
9
+ # :with => {
10
+ # :b => [1, 2]
11
+ # },
12
+ # :without => "c"
13
+ # }
14
+ #
15
+ # hash.squash("_") =>
16
+ # [
17
+ # "with_b_1",
18
+ # "with_b_2",
19
+ # "without_c"
20
+ # ]
21
+ #
22
+ # This is useful for readable hash options, like:
23
+ #
24
+ # magic :with => [ "Jack", "Giant" => { :in => "sky", :on => [ "ground", "harp" ] } ]
25
+
26
+ def squash(separator=" ")
27
+ inject([]) do |memo, (k, v)|
28
+ key = if k.respond_to?(:squash) then k.squash else [ k.to_s ] end
29
+ val = if v.respond_to?(:squash) then v.squash else [ v.to_s ] end
30
+
31
+ # Loop over all combos
32
+ for sk in key do
33
+ for sv in val do
34
+ memo << (sk + separator + sv)
35
+ end
36
+ end
37
+
38
+ memo
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,109 @@
1
+ require 'tap_dance/ui'
2
+ require 'tap_dance/brew_cli'
3
+
4
+ module TapDance
5
+ class Brew
6
+ attr_accessor :name
7
+ attr_accessor :tap
8
+
9
+ def initialize(name, opts={})
10
+ @opts = opts.dup
11
+ @name = name.to_s
12
+ @tap = @opts[:tap]
13
+ @flags = @opts[:flags]
14
+ @flags ||= []
15
+
16
+ # Get actual tap object
17
+ unless @tap.nil?
18
+ case @tap
19
+ when String, Symbol
20
+ @tap = @opts[:definition].tap_named @tap.to_s
21
+ when TapDance::Tap
22
+ # Good to go!
23
+ end
24
+ @opts[:tap] = @tap
25
+ end
26
+
27
+ end
28
+
29
+ def install
30
+ if brewable?
31
+ # Do magic here
32
+ BrewCLI.install canonical, @flags
33
+ else
34
+ TapDance.ui.error "No available formula for \"#{canonical}\""
35
+ end
36
+ end
37
+
38
+ def upgrade
39
+ if installed?
40
+ # Do magic here
41
+ BrewCLI.upgrade canonical, @flags
42
+ else
43
+ TapDance.ui.error "You haven't installed \"#{canonical}\""
44
+ end
45
+ end
46
+
47
+ def canonical
48
+ unless @tap.nil?
49
+ "#{@tap.url}/#{@name}"
50
+ else
51
+ @name
52
+ end
53
+ end
54
+
55
+ # Does the formula exist?
56
+ def brewable?
57
+ BrewCLI.formula_info(canonical).okay?
58
+ end
59
+
60
+ def formula_version
61
+ res = BrewCLI.formula_info(canonical)
62
+ return nil unless res.okay?
63
+
64
+ res.out.split($/).first.match(/#{name}: stable ([^\s,]+)/)[1]
65
+ end
66
+
67
+ def formula_versions
68
+ # Takes a long time to get; cache return
69
+ if @formula_versions.nil?
70
+ @formula_versions = BrewCLI.formula_versions(canonical).
71
+ out.chomp.split($/).map do |v|
72
+ s = v.split(/\s+/)
73
+ version = s[0]
74
+ commit = s[3]
75
+ path = s[4]
76
+ [ version, commit, path ]
77
+ end
78
+ else
79
+ return @formula_versions
80
+ end
81
+ end
82
+
83
+ def installed_versions
84
+ # Would love to use StringScanner...but we don't need to!
85
+ versions = BrewCLI.list_versions(canonical).out.strip.split(/\s+/)
86
+ unless versions.empty?
87
+ return versions[1..-1]
88
+ else
89
+ return []
90
+ end
91
+ end
92
+
93
+ # Is a version installed?
94
+ def installed?
95
+ !installed_versions.empty?
96
+ end
97
+
98
+ def latest_version
99
+ # Works unless the versions have
100
+ # strings in them, like alpha/beta
101
+ installed_versions.sort.last if installed?
102
+ end
103
+
104
+ def to_s
105
+ return canonical
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,99 @@
1
+ require 'tap_dance'
2
+ require 'tap_dance/shell_result'
3
+
4
+ # Load in extensions
5
+ require 'hash'
6
+ require 'array'
7
+
8
+ module TapDance
9
+ module BrewCLI
10
+ class << self
11
+ # One offing terminal commands makes it easy to stub.
12
+ attr_accessor :dry_run
13
+ attr_writer :prefix
14
+
15
+ def prefix
16
+ @prefix ||= exec("--prefix").strip
17
+ end
18
+
19
+ def update
20
+ exec "update", true
21
+ end
22
+
23
+ def install(name, flags="")
24
+ exec "install #{name} #{flag_string flags}", true
25
+ end
26
+
27
+ def upgrade(name, flags="")
28
+ exec "upgrade #{name} #{flag_string flags}", true
29
+ end
30
+
31
+ def list_versions(name)
32
+ exec "list --versions #{name}"
33
+ end
34
+
35
+ def formula_info(name)
36
+ exec "info #{name}"
37
+ end
38
+
39
+ def formula_versions(name)
40
+ exec "versions #{name}"
41
+ end
42
+
43
+ def tap(url)
44
+ exec "tap #{url}", true
45
+ end
46
+
47
+ def untap(url)
48
+ exec "untap #{url}", true
49
+ end
50
+
51
+ def tap_list
52
+ exec "tap"
53
+ end
54
+
55
+ private
56
+
57
+ # Private because we want
58
+ # all brew commands wrapped
59
+ # for testing.
60
+ def exec(cmd, volatile=false)
61
+ # If a command is volatile (causes system changes),
62
+ # we want to offer a dry-run option for testing
63
+ # and the CLI.
64
+ cmd.strip!
65
+
66
+ if volatile and @dry_run
67
+ TapDance.ui.warn "Would run: brew #{cmd}"
68
+ return nil
69
+ else
70
+ ShellResult.of "brew #{cmd}"
71
+ end
72
+ end
73
+
74
+ # Sanitize flags with standard unix
75
+ # conventions and generate a string
76
+ def flag_string(flags)
77
+ case flags
78
+ when String
79
+ return flags
80
+ else
81
+ return flags.squash("-").map {|m|
82
+ case m
83
+ when /^--/
84
+ m
85
+ when /^-[^\-]/
86
+ m
87
+ else
88
+ if m =~ /^.[ =]/
89
+ "-#{m}"
90
+ else
91
+ "--#{m}"
92
+ end
93
+ end
94
+ }.join(" ")
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,83 @@
1
+ require 'tap_dance'
2
+ require 'tap_dance/brew'
3
+ require 'tap_dance/tap'
4
+ require 'tap_dance/dsl'
5
+
6
+ require 'thor'
7
+
8
+ module TapDance
9
+ class CLI < Thor
10
+ package_name "TapDance"
11
+ default_task "install"
12
+
13
+ class_option "verbose",
14
+ :type => :boolean,
15
+ :banner => "Enable verbose output mode",
16
+ :aliases => "-v"
17
+ class_option "brewfile",
18
+ :type => :string,
19
+ :banner => "Specify an alternate path to a Brewfile",
20
+ :aliases => "-B"
21
+ class_option "dry-run",
22
+ :type => :boolean,
23
+ :banner => "Print out what volatile commands would be run"
24
+
25
+ attr_accessor :brewfile
26
+ attr_accessor :definition
27
+
28
+ def initialize(*args, &block)
29
+ super(*args, &block)
30
+
31
+ # Activate shell output
32
+ TapDance.ui = UI::Shell.new(options)
33
+ TapDance.ui.level = "debug" if options["verbose"]
34
+
35
+ # Make sure homebrew is installed
36
+ unless command?(:brew)
37
+ TapDance.ui.error "You haven't installed homebrew, or it isn't in your path."
38
+ exit 1
39
+ end
40
+
41
+ @definition = nil
42
+
43
+ # Activate dry-run mode
44
+ TapDance::BrewCLI.dry_run = options["dry-run"] if options["dry-run"]
45
+
46
+ # Find brewfile
47
+ @brewfile = options["brewfile"]
48
+ @brewfile ||= "./Brewfile"
49
+ @brewfile = File.expand_path @brewfile
50
+
51
+ TapDance.ui.info "Brewing from #{@brewfile}"
52
+
53
+ unless File.exist? @brewfile
54
+ TapDance.ui.error "Nothing to brew! No Brewfile found."
55
+ else
56
+ TapDance.ui.info "Running `brew update` for good measure."
57
+ BrewCLI.update
58
+ end
59
+ end
60
+
61
+ desc "install", "install all the brews in your Brewfile"
62
+ def install
63
+ return unless File.exist? @brewfile
64
+ @definition = TapDance::DSL.evaluate @brewfile
65
+ @definition.execute
66
+ end
67
+
68
+ desc "update", "update all the brews in your Brewfile"
69
+ def update(name=nil)
70
+ return unless File.exist? @brewfile
71
+ @definition = TapDance::DSL.evaluate @brewfile
72
+ @definition.execute true
73
+ end
74
+
75
+ private
76
+
77
+ # Note: susceptible to code injection!
78
+ def command?(name)
79
+ system "which #{name.to_s} > /dev/null 2>&1"
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,47 @@
1
+ require 'tap_dance/brew'
2
+ require 'tap_dance/tap'
3
+ require 'tap_dance/ui'
4
+ require 'tap_dance'
5
+
6
+ module TapDance
7
+ class Definition
8
+ attr_accessor :taps
9
+ attr_accessor :brews
10
+
11
+ def initialize(taps=[], brews=[])
12
+ @taps = taps
13
+ @brews = brews
14
+ end
15
+
16
+ def tap(name, url, opts={})
17
+ @taps << Tap.new(name, url, opts.merge(:definition => self))
18
+ @taps.last
19
+ end
20
+
21
+ def brew(name, opts={})
22
+ @brews << Brew.new(name, opts.merge(:definition => self))
23
+ @brews.last
24
+ end
25
+
26
+ def tap_named(name)
27
+ @taps.find { |t| t.name.to_s == name.to_s }
28
+ end
29
+
30
+ def execute(upgrade=false)
31
+ for tap in @taps do
32
+ TapDance.ui.confirm "Tapping \"#{tap}\""
33
+ TapDance.ui.detail tap.entap
34
+ end
35
+
36
+ for brew in @brews do
37
+ unless upgrade
38
+ TapDance.ui.confirm "Installing \"#{brew}\""
39
+ TapDance.ui.detail brew.install
40
+ else
41
+ TapDance.ui.confirm "Upgrading \"#{brew}\""
42
+ TapDance.ui.detail brew.upgrade
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,55 @@
1
+ require 'tap_dance/definition'
2
+ require 'tap_dance/brew'
3
+ require 'tap_dance/tap'
4
+ require 'tap_dance/ui'
5
+
6
+ class BrewfileError < RuntimeError; end
7
+ module TapDance
8
+ class DSL
9
+ attr_reader :definition
10
+
11
+ def self.evaluate(brewfile, lockfile=nil, unlock=nil)
12
+ builder = new
13
+ builder.eval_brewfile(brewfile)
14
+ builder.definition
15
+ end
16
+
17
+ def initialize
18
+ @groups = [] # Arg/filter groups
19
+ @tap = nil # Current tap scope
20
+
21
+ @definition = Definition.new
22
+ end
23
+
24
+ def eval_brewfile(brewfile, contents = nil)
25
+ contents ||= File.read File.expand_path(brewfile.to_s)
26
+ instance_eval contents, brewfile.to_s, 1
27
+ rescue SyntaxError => e
28
+ bt = e.message.split("\n")[1..-1]
29
+ raise BrewfileError, ["Brewfile syntax error:", *bt].join("\n")
30
+ rescue ScriptError, RegexpError, NameError, ArgumentError => e
31
+ e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})"
32
+ TapDance.ui.warn e.backtrace.join("\n ")
33
+ raise BrewfileError, "There was an error in your Brewfile," \
34
+ " and TapDance cannot continue."
35
+ end
36
+
37
+ ### DSL commands
38
+ def tap(name, url, opts={})
39
+ # Can't nest taps; doesn't make sense
40
+ raise BrewfileError, "You cannot nest taps!" unless @tap.nil?
41
+ old_tap = @tap
42
+
43
+ @tap = @definition.tap name, url, opts
44
+
45
+ yield if block_given?
46
+
47
+ @tap = old_tap
48
+ end
49
+
50
+ def brew(name, opts={})
51
+ @definition.brew name, { :tap => @tap }.merge(opts)
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ require 'open3'
2
+
3
+ module TapDance
4
+ class ShellResult
5
+ attr_reader :out
6
+ attr_reader :err
7
+ attr_reader :status
8
+
9
+ def self.of(cmd)
10
+ out, err, sts = Open3.capture3 cmd
11
+ new out, err, sts.exitstatus
12
+ end
13
+
14
+ def initialize(out, err=nil, status=0)
15
+ @out = out.to_s
16
+ @err = err.to_s
17
+ @status = status
18
+ end
19
+
20
+ def okay?
21
+ !error?
22
+ end
23
+
24
+ def error?
25
+ @status != 0 || erred(@out) || erred(@err)
26
+ end
27
+
28
+ def stdout?
29
+ @out.strip != ""
30
+ end
31
+
32
+ def stderr?
33
+ @err.strip != ""
34
+ end
35
+
36
+ def to_s
37
+ if @out.strip == "" then @err else @out end
38
+ end
39
+
40
+ ### We likes, but disable for refactoring
41
+ # def method_missing(method, *args, &block)
42
+ # # Let people treat it like a string
43
+ # if String.new.respond_to? method
44
+ # to_s.send(method, *args, &block)
45
+ # else
46
+ # super
47
+ # end
48
+ # end
49
+
50
+ # def respond_to_missing?(method, include_private = false)
51
+ # String.new.respond_to?(method) || super
52
+ # end
53
+
54
+ private
55
+
56
+ def erred(string)
57
+ string[0..4] == 'Error'
58
+ end
59
+
60
+ end
61
+ end