├── .rspec ├── lib ├── bundle │ ├── version.rb │ ├── commands │ │ ├── dump.rb │ │ ├── install.rb │ │ ├── exec.rb │ │ ├── check.rb │ │ └── cleanup.rb │ ├── mac_app_store_dumper.rb │ ├── tap_dumper.rb │ ├── cask_dumper.rb │ ├── cask_installer.rb │ ├── tap_installer.rb │ ├── mac_app_store_installer.rb │ ├── brew_services.rb │ ├── utils.rb │ ├── dumper.rb │ ├── dsl.rb │ ├── brew_dumper.rb │ └── brew_installer.rb └── bundle.rb ├── spec ├── stub │ ├── utils.rb │ ├── extend │ │ └── ENV.rb │ └── formula.rb ├── dumper_spec.rb ├── spec_helper.rb ├── tap_dumper_spec.rb ├── tap_installer_spec.rb ├── mac_app_store_dumper_spec.rb ├── cask_installer_spec.rb ├── cask_dumper_spec.rb ├── dump_command_spec.rb ├── mac_app_store_installer_spec.rb ├── install_command_spec.rb ├── utils_spec.rb ├── brew_services_spec.rb ├── check_command_spec.rb ├── dsl_spec.rb ├── exec_command_spec.rb ├── cleanup_command_spec.rb ├── brew_installer_spec.rb └── brew_dumper_spec.rb ├── Rakefile ├── Gemfile ├── .travis.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── cmd └── brew-bundle.rb ├── CODEOFCONDUCT.md └── Readme.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /lib/bundle/version.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | VERSION = "1.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/stub/utils.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | def which(command) 4 | Pathname.new("/usr/local/bin/#{command}") 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new("spec") 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | group :test do 4 | gem "rspec" 5 | gem "rake" 6 | gem "simplecov" 7 | gem "coveralls" 8 | end 9 | -------------------------------------------------------------------------------- /lib/bundle/commands/dump.rb: -------------------------------------------------------------------------------- 1 | module Bundle::Commands 2 | class Dump 3 | def self.run 4 | Bundle::Dumper.dump_brewfile 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/bundle/commands/install.rb: -------------------------------------------------------------------------------- 1 | module Bundle::Commands 2 | class Install 3 | def self.run 4 | Bundle::Dsl.new(Bundle.brewfile).install || exit(1) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c # Force OS X 2 | rvm: 2.0.0 3 | cache: bundler 4 | before_install: 5 | - brew update 6 | script: 7 | - bundle exec rake 8 | notifications: 9 | email: 10 | on_success: never 11 | on_failure: always 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | bin/ 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | .ruby-gemset 20 | .ruby-version 21 | .envrc 22 | -------------------------------------------------------------------------------- /spec/stub/extend/ENV.rb: -------------------------------------------------------------------------------- 1 | ENV.instance_eval do 2 | def deps; @deps || []; end 3 | def deps= other; @deps = other; end 4 | def keg_only_deps; @keg_only_deps || []; end 5 | def keg_only_deps= other; @keg_only_deps = other; end 6 | 7 | def self.activate_extensions!; end 8 | def setup_build_environment; end 9 | def refurbish_args; end 10 | def prepend_path *args; end 11 | end 12 | 13 | def superenv?; true; end 14 | -------------------------------------------------------------------------------- /spec/stub/formula.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | class Formula 4 | def initialize(name) 5 | @prefix = Pathname.new("/usr/local") 6 | @name = name 7 | end 8 | 9 | def opt_prefix 10 | @prefix.join("opt").join(@name) 11 | end 12 | 13 | def opt_bin 14 | opt_prefix.join("bin") 15 | end 16 | 17 | def recursive_dependencies; []; end 18 | 19 | def keg_only?; false; end 20 | 21 | def installed?; true; end 22 | end 23 | 24 | class Formulary 25 | def self.factory(name) 26 | Formula.new(name) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/bundle/mac_app_store_dumper.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module Bundle 4 | class MacAppStoreDumper 5 | def self.reset! 6 | @apps = nil 7 | end 8 | 9 | def self.apps 10 | @apps ||= if Bundle.mas_installed? 11 | `mas list 2>/dev/null`.split("\n").map {|app| app.split(" ", 2)} 12 | else 13 | [] 14 | end 15 | end 16 | 17 | def self.app_ids 18 | apps.map {|id,_| id.to_i } 19 | end 20 | 21 | def self.dump 22 | apps.map {|id, name| "mas '#{name}', id: #{id}"}.join("\n") 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/bundle/tap_dumper.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module Bundle 4 | class TapDumper 5 | def self.reset! 6 | @taps = nil 7 | end 8 | 9 | def self.taps 10 | @taps ||= begin 11 | require "tap" 12 | Tap.map(&:to_hash) 13 | end 14 | end 15 | 16 | def self.dump 17 | taps.map do |tap| 18 | remote = ", '#{tap["remote"]}'" if tap["custom_remote"] && tap["remote"] 19 | "tap '#{tap["name"]}'#{remote}" 20 | end.join("\n") 21 | end 22 | 23 | def self.tap_names 24 | taps.map { |tap| tap["name"] } 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/bundle.rb: -------------------------------------------------------------------------------- 1 | require "bundle/version" 2 | require "bundle/utils" 3 | require "bundle/dsl" 4 | require "bundle/brew_services" 5 | require "bundle/brew_installer" 6 | require "bundle/cask_installer" 7 | require "bundle/mac_app_store_installer" 8 | require "bundle/tap_installer" 9 | require "bundle/brew_dumper" 10 | require "bundle/cask_dumper" 11 | require "bundle/mac_app_store_dumper" 12 | require "bundle/tap_dumper" 13 | require "bundle/dumper" 14 | require "bundle/commands/install" 15 | require "bundle/commands/dump" 16 | require "bundle/commands/cleanup" 17 | require "bundle/commands/check" 18 | require "bundle/commands/exec" 19 | -------------------------------------------------------------------------------- /lib/bundle/cask_dumper.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class CaskDumper 3 | def self.reset! 4 | @casks = nil 5 | end 6 | 7 | def self.casks 8 | @casks ||= if Bundle.cask_installed? 9 | `brew cask list -1 2>/dev/null`.split("\n").map { |cask| cask.chomp " (!)" } 10 | else 11 | [] 12 | end 13 | end 14 | 15 | def self.dump(casks_required_by_formulae) 16 | [ 17 | (casks & casks_required_by_formulae).map { |cask| "cask '#{cask}'" }.join("\n"), 18 | (casks - casks_required_by_formulae).map { |cask| "cask '#{cask}'" }.join("\n"), 19 | ] 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | * Fork the project. 3 | * Make your feature addition or bug fix. 4 | * Add tests for it. This is important so I don't break it in a 5 | future version unintentionally. 6 | * Add documentation if necessary. 7 | * Commit, do not mess with rakefile, version, or history. 8 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 9 | * Send a pull request. Bonus points for topic branches. 10 | 11 | ## Security 12 | Please report security issues to security@brew.sh. 13 | 14 | ## Code of Conduct 15 | 16 | Please note that this project is released with a [Code of Conduct](CODEOFCONDUCT.md). By participating in this project you agree to abide by its terms. 17 | -------------------------------------------------------------------------------- /lib/bundle/cask_installer.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class CaskInstaller 3 | def self.install(name, options = {}) 4 | if installed_casks.include? name 5 | puts "Skipping install of #{name} cask. It is already installed." if ARGV.verbose? 6 | return true 7 | end 8 | 9 | args = options.fetch(:args, []).map { |k, v| "--#{k}=#{v}" } 10 | 11 | puts "Installing #{name} cask. It is not currently installed." if ARGV.verbose? 12 | if (success = Bundle.system "brew", "cask", "install", name, *args) 13 | installed_casks << name 14 | end 15 | 16 | success 17 | end 18 | 19 | def self.installed_casks 20 | @installed_casks ||= Bundle::CaskDumper.casks 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/bundle/tap_installer.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class TapInstaller 3 | def self.install(name, clone_target) 4 | if installed_taps.include? name 5 | puts "Skipping install of #{name} tap. It is already installed." if ARGV.verbose? 6 | return true 7 | end 8 | 9 | puts "Installing #{name} tap. It is not currently installed." if ARGV.verbose? 10 | success = if clone_target 11 | Bundle.system "brew", "tap", name, clone_target 12 | else 13 | Bundle.system "brew", "tap", name 14 | end 15 | 16 | if success 17 | installed_taps << name 18 | end 19 | 20 | success 21 | end 22 | 23 | def self.installed_taps 24 | @installed_taps ||= Bundle::TapDumper.tap_names 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Dumper do 4 | before do 5 | allow(Bundle).to receive(:cask_installed?).and_return(true) 6 | allow(Bundle).to receive(:mas_installed?).and_return(false) 7 | allow(ARGV).to receive(:force?).and_return(false) 8 | allow(ARGV).to receive(:value).and_return(nil) 9 | Bundle::BrewDumper.reset! 10 | Bundle::TapDumper.reset! 11 | Bundle::CaskDumper.reset! 12 | Bundle::MacAppStoreDumper.reset! 13 | Bundle::BrewServices.reset! 14 | allow(Bundle::CaskDumper).to receive(:`).and_return("google-chrome\njava") 15 | end 16 | subject { Bundle::Dumper } 17 | 18 | it "generates output" do 19 | expect(subject).to receive(:write_file) do |file, content, _overwrite| 20 | expect(file).to eql(Pathname.new(Dir.pwd).join("Brewfile")) 21 | expect(content).to eql("cask 'google-chrome'\ncask 'java'\n") 22 | end 23 | subject.dump_brewfile 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/bundle/mac_app_store_installer.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class MacAppStoreInstaller 3 | def self.install(name, id) 4 | unless Bundle.mas_installed? 5 | puts "Installing mas. It is not currently installed." if ARGV.verbose? 6 | Bundle.system "brew", "install", "mas" 7 | unless Bundle.mas_installed? 8 | raise "Unable to install #{name} app. mas installation failed." 9 | end 10 | end 11 | 12 | if installed_app_ids.include? id 13 | puts "Skipping install of #{name} app. It is already installed." if ARGV.verbose? 14 | return true 15 | end 16 | 17 | puts "Installing #{name} app. It is not currently installed." if ARGV.verbose? 18 | if (success = Bundle.system "mas", "install", "#{id}") 19 | installed_app_ids << id 20 | end 21 | 22 | success 23 | end 24 | 25 | def self.installed_app_ids 26 | @installed_app_ids ||= Bundle::MacAppStoreDumper.app_ids 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/bundle/brew_services.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class BrewServices 3 | def self.reset! 4 | @started_services = nil 5 | end 6 | 7 | def self.stop(name) 8 | return true unless started?(name) 9 | if Bundle.system "brew", "services", "stop", name 10 | started_services.delete(name) 11 | true 12 | end 13 | end 14 | 15 | def self.restart(name) 16 | if Bundle.system "brew", "services", "restart", name 17 | started_services << name 18 | true 19 | end 20 | end 21 | 22 | def self.started?(name) 23 | started_services.include? name 24 | end 25 | 26 | def self.started_services 27 | @started_services ||= if Bundle.services_installed? 28 | `brew services list`.lines.map do |line| 29 | name, state, _plist = line.split(/\s+/) 30 | next unless state == "started" 31 | name 32 | end.compact 33 | else 34 | [] 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Homebrew maintainers and Andrew Nesbitt. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.start do 3 | add_filter "/test/" 4 | add_filter "/vendor/" 5 | minimum_coverage 100 6 | end 7 | 8 | require "coveralls" 9 | Coveralls.wear! 10 | 11 | PROJECT_ROOT ||= File.expand_path("../..", __FILE__) 12 | STUB_PATH ||= File.expand_path(File.join(__FILE__, "..", "stub")) 13 | $:.unshift(STUB_PATH) 14 | 15 | Dir.glob("#{PROJECT_ROOT}/lib/**/*.rb").each { |f| require f } 16 | 17 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 18 | Coveralls::SimpleCov::Formatter, 19 | SimpleCov::Formatter::HTMLFormatter 20 | ] 21 | 22 | require "bundle" 23 | require "bundler" 24 | 25 | RSpec.configure do |config| 26 | config.around(:each) do |example| 27 | Bundler.with_clean_env { example.run } 28 | end 29 | end 30 | 31 | # Stub out the inclusion of Homebrew's code. 32 | LIBS_TO_SKIP = ["formula", "tap"] 33 | 34 | module Kernel 35 | alias_method :old_require, :require 36 | def require(path) 37 | old_require(path) unless LIBS_TO_SKIP.include?(path) 38 | end 39 | end 40 | 41 | class Formula 42 | def self.installed 43 | [] 44 | end 45 | end 46 | 47 | module Tap 48 | def self.map 49 | [] 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/bundle/utils.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | def self.system(cmd, *args) 3 | if ARGV.verbose? 4 | return super cmd, *args 5 | end 6 | logs = [] 7 | success = nil 8 | IO.popen([cmd, *args], :err => [:child, :out]) do |pipe| 9 | while buf = pipe.gets 10 | logs << buf 11 | end 12 | Process.wait(pipe.pid) 13 | success = $?.success? 14 | pipe.close 15 | end 16 | puts logs.join unless success 17 | success 18 | end 19 | 20 | def self.mas_installed? 21 | @mas ||= begin 22 | !!which("mas") 23 | end 24 | end 25 | 26 | def self.cask_installed? 27 | @cask ||= begin 28 | which("brew-cask") || which("brew-cask.rb") 29 | end 30 | end 31 | 32 | def self.services_installed? 33 | @services ||= begin 34 | !!which("brew-services.rb") 35 | end 36 | end 37 | 38 | def self.brewfile 39 | if ARGV.include?("--global") 40 | file = Pathname.new("#{ENV["HOME"]}/.Brewfile") 41 | else 42 | filename = ARGV.value("file") 43 | filename = "/dev/stdin" if filename == "-" 44 | filename ||= "Brewfile" 45 | file = Pathname.new(filename).expand_path(Dir.pwd) 46 | end 47 | file.read 48 | rescue Errno::ENOENT 49 | raise "No Brewfile found" 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/tap_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::TapDumper do 4 | context "when there is no tap" do 5 | before do 6 | Bundle::TapDumper.reset! 7 | end 8 | subject { Bundle::TapDumper } 9 | 10 | it "returns empty list" do 11 | expect(subject.taps).to be_empty 12 | end 13 | 14 | it "dumps as empty string" do 15 | expect(subject.dump).to eql("") 16 | end 17 | end 18 | 19 | context "there are tap `homebrew/foo` and `bitbucket/bar`" do 20 | before do 21 | Bundle::TapDumper.reset! 22 | allow(Tap).to receive(:map).and_return [ 23 | { 24 | "name" => "homebrew/foo", 25 | "remote" => "https://github.com/Homebrew/homebrew-foo", 26 | "custom_remote" => false, 27 | }, 28 | { 29 | "name" => "bitbucket/bar", 30 | "remote" => "https://bitbucket.org/bitbucket/bar.git", 31 | "custom_remote" => true, 32 | }, 33 | ] 34 | end 35 | subject { Bundle::TapDumper } 36 | 37 | it "returns list of information" do 38 | expect(subject.taps).not_to be_empty 39 | end 40 | 41 | it "dumps output" do 42 | expect(subject.dump).to eql("tap 'homebrew/foo'\ntap 'bitbucket/bar', 'https://bitbucket.org/bitbucket/bar.git'") 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/bundle/dumper.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require "pathname" 3 | 4 | module Bundle 5 | class Dumper 6 | def self.dump_brewfile 7 | if ARGV.include?("--global") 8 | file = Pathname.new("#{ENV["HOME"]}/.Brewfile") 9 | else 10 | filename = ARGV.value("file") 11 | filename = "/dev/stdout" if filename == "-" 12 | filename ||= "Brewfile" 13 | file = Pathname.new(filename).expand_path(Dir.pwd) 14 | end 15 | raise "#{file} already exists" if should_not_write_file?(file, ARGV.force?) 16 | content = [] 17 | content << TapDumper.dump 18 | casks_required_by_formulae = BrewDumper.cask_requirements 19 | cask_before_formula, cask_after_formula = CaskDumper.dump(casks_required_by_formulae) 20 | content << cask_before_formula 21 | content << BrewDumper.dump 22 | content << cask_after_formula 23 | content << MacAppStoreDumper.dump 24 | content = content.reject(&:empty?).join("\n") + "\n" 25 | write_file file, content 26 | end 27 | 28 | private 29 | 30 | def self.should_not_write_file?(file, overwrite = false) 31 | file.exist? && !overwrite && file.to_s != "/dev/stdout" 32 | end 33 | 34 | def self.write_file(file, content) 35 | file.open("w") { |io| io.write content } 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/tap_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::TapInstaller do 4 | def do_install(clone_target = nil) 5 | Bundle::TapInstaller.install("phinze/cask", clone_target) 6 | end 7 | 8 | context ".installed_taps" do 9 | it "calls Homebrew" do 10 | Bundle::TapInstaller.installed_taps 11 | end 12 | end 13 | 14 | context "when tap is installed" do 15 | before do 16 | allow(Bundle::TapInstaller).to receive(:installed_taps).and_return(["phinze/cask"]) 17 | allow(ARGV).to receive(:verbose?).and_return(false) 18 | end 19 | 20 | it "skips" do 21 | expect(Bundle).not_to receive(:system) 22 | expect(do_install).to eql(true) 23 | end 24 | end 25 | 26 | context "when tap is not installed" do 27 | before do 28 | allow(Bundle::TapInstaller).to receive(:installed_taps).and_return([]) 29 | allow(ARGV).to receive(:verbose?).and_return(false) 30 | end 31 | 32 | it "taps" do 33 | expect(Bundle).to receive(:system).with("brew", "tap", "phinze/cask").and_return(true) 34 | expect(do_install).to eql(true) 35 | end 36 | 37 | context "with clone target" do 38 | it "taps" do 39 | expect(Bundle).to receive(:system).with("brew", "tap", "phinze/cask", "clone_target_path").and_return(true) 40 | expect(do_install("clone_target_path")).to eql(true) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/mac_app_store_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::MacAppStoreDumper do 4 | context "when mas is not installed" do 5 | before do 6 | Bundle::MacAppStoreDumper.reset! 7 | allow(Bundle).to receive(:mas_installed?).and_return(false) 8 | end 9 | subject { Bundle::MacAppStoreDumper } 10 | 11 | it "returns empty list" do 12 | expect(subject.apps).to be_empty 13 | end 14 | 15 | it "dumps as empty string" do 16 | expect(subject.dump).to eql("") 17 | end 18 | end 19 | 20 | context "when there is no apps" do 21 | before do 22 | Bundle::MacAppStoreDumper.reset! 23 | allow(Bundle).to receive(:mas_installed?).and_return(true) 24 | allow(Bundle::MacAppStoreDumper).to receive(:`).and_return("") 25 | end 26 | subject { Bundle::MacAppStoreDumper } 27 | 28 | it "returns empty list" do 29 | expect(subject.apps).to be_empty 30 | end 31 | 32 | it "dumps as empty string" do 33 | expect(subject.dump).to eql("") 34 | end 35 | end 36 | 37 | context "apps `foo`, `bar` and `baz` are installed" do 38 | before do 39 | Bundle::MacAppStoreDumper.reset! 40 | allow(Bundle).to receive(:mas_installed?).and_return(true) 41 | allow(Bundle::MacAppStoreDumper).to receive(:`).and_return("foo 123\nbar 456\nbaz 789") 42 | end 43 | subject { Bundle::MacAppStoreDumper } 44 | 45 | it "returns list %w[foo bar baz]" do 46 | expect(subject.apps).to eql([["foo", "123"], ["bar", "456"], ["baz", "789"]]) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/cask_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::CaskInstaller do 4 | def do_install 5 | Bundle::CaskInstaller.install("google-chrome") 6 | end 7 | 8 | context ".installed_casks" do 9 | it "shells out" do 10 | Bundle::CaskInstaller.installed_casks 11 | end 12 | end 13 | 14 | context "when brew-cask is installed" do 15 | before do 16 | allow(Bundle).to receive(:cask_installed?).and_return(true) 17 | allow(ARGV).to receive(:verbose?).and_return(false) 18 | end 19 | 20 | context "when cask is installed" do 21 | before do 22 | allow(Bundle::CaskInstaller).to receive(:installed_casks).and_return(["google-chrome"]) 23 | end 24 | 25 | it "skips" do 26 | expect(Bundle).not_to receive(:system) 27 | expect(do_install).to eql(true) 28 | end 29 | end 30 | 31 | context "when cask is not installed" do 32 | before do 33 | allow(Bundle::CaskInstaller).to receive(:installed_casks).and_return([]) 34 | end 35 | 36 | it "installs cask" do 37 | expect(Bundle).to receive(:system).with("brew", "cask", "install", "google-chrome").and_return(true) 38 | expect(do_install).to eql(true) 39 | end 40 | 41 | it "installs cask with arguments" do 42 | expect(Bundle).to receive(:system).with("brew", "cask", "install", "firefox", "--appdir=/Applications").and_return(true) 43 | expect(Bundle::CaskInstaller.install("firefox", :args => { :appdir => "/Applications" })).to eq(true) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/cask_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::CaskDumper do 4 | context "when brew-cask is not installed" do 5 | before do 6 | Bundle::CaskDumper.reset! 7 | allow(Bundle).to receive(:cask_installed?).and_return(false) 8 | end 9 | subject { Bundle::CaskDumper } 10 | 11 | it "returns empty list" do 12 | expect(subject.casks).to be_empty 13 | end 14 | 15 | it "dumps as empty string" do 16 | expect(subject.dump([])).to eql(["", ""]) 17 | end 18 | end 19 | 20 | context "when there is no cask" do 21 | before do 22 | Bundle::CaskDumper.reset! 23 | allow(Bundle).to receive(:cask_installed?).and_return(true) 24 | allow(Bundle::CaskDumper).to receive(:`).and_return("") 25 | end 26 | subject { Bundle::CaskDumper } 27 | 28 | it "returns empty list" do 29 | expect(subject.casks).to be_empty 30 | end 31 | 32 | it "dumps as empty string" do 33 | expect(subject.dump([])).to eql(["", ""]) 34 | end 35 | end 36 | 37 | context "cask `foo`, `bar` and `baz` are installed, while `baz` is required by formula" do 38 | before do 39 | Bundle::CaskDumper.reset! 40 | allow(Bundle).to receive(:cask_installed?).and_return(true) 41 | allow(Bundle::CaskDumper).to receive(:`).and_return("foo\nbar\nbaz") 42 | end 43 | subject { Bundle::CaskDumper } 44 | 45 | it "returns list %w[foo bar baz]" do 46 | expect(subject.casks).to eql(%w[foo bar baz]) 47 | end 48 | 49 | it "dumps as `cask 'baz'` and `cask 'foo' cask 'bar'`" do 50 | expect(subject.dump(%w[baz])).to eql ["cask 'baz'", "cask 'foo'\ncask 'bar'"] 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/dump_command_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Commands::Dump do 4 | context "when files existed" do 5 | before do 6 | allow_any_instance_of(Pathname).to receive(:exist?).and_return(true) 7 | allow(ARGV).to receive(:include?).and_return(true) 8 | allow(ARGV).to receive(:force?).and_return(false) 9 | allow(ARGV).to receive(:value).and_return(nil) 10 | allow(Bundle).to receive(:cask_installed?).and_return(true) 11 | end 12 | 13 | it "raises error" do 14 | expect do 15 | Bundle::Commands::Dump.run 16 | end.to raise_error(RuntimeError) 17 | end 18 | 19 | it "should exit before doing any work" do 20 | expect(Bundle::TapDumper).not_to receive(:dump) 21 | expect(Bundle::BrewDumper).not_to receive(:dump) 22 | expect(Bundle::CaskDumper).not_to receive(:dump) 23 | expect do 24 | Bundle::Commands::Dump.run 25 | end.to raise_error(RuntimeError) 26 | end 27 | end 28 | 29 | context "when files existed and `--force` is passed" do 30 | before do 31 | allow_any_instance_of(Pathname).to receive(:exist?).and_return(true) 32 | allow(ARGV).to receive(:force?).and_return(true) 33 | allow(ARGV).to receive(:value).and_return(nil) 34 | allow(Bundle).to receive(:cask_installed?).and_return(true) 35 | end 36 | 37 | it "doesn't raise error" do 38 | io = double("File", :write => true) 39 | expect_any_instance_of(Pathname).to receive(:open).with("w") { |&block| block.call io } 40 | expect(io).to receive(:write) 41 | expect do 42 | Bundle::Commands::Dump.run 43 | end.to_not raise_error 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/mac_app_store_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::MacAppStoreInstaller do 4 | def do_install 5 | Bundle::MacAppStoreInstaller.install("foo", 123) 6 | end 7 | 8 | context ".installed_app_ids" do 9 | it "shells out" do 10 | Bundle::MacAppStoreInstaller.installed_app_ids 11 | end 12 | end 13 | 14 | context "when mas is not installed" do 15 | before do 16 | allow(Bundle).to receive(:mas_installed?).and_return(false) 17 | allow(ARGV).to receive(:verbose?).and_return(false) 18 | end 19 | 20 | it "tries to install mas" do 21 | expect(Bundle).to receive(:system).with("brew", "install", "mas").and_return(true) 22 | expect { do_install }.to raise_error(RuntimeError) 23 | end 24 | end 25 | 26 | context "when mas is installed" do 27 | before do 28 | allow(Bundle).to receive(:mas_installed?).and_return(true) 29 | allow(ARGV).to receive(:verbose?).and_return(false) 30 | end 31 | 32 | context "when app is installed" do 33 | before do 34 | allow(Bundle::MacAppStoreInstaller).to receive(:installed_app_ids).and_return([123]) 35 | end 36 | 37 | it "skips" do 38 | expect(Bundle).not_to receive(:system) 39 | expect(do_install).to eql(true) 40 | end 41 | end 42 | 43 | context "when app is not installed" do 44 | before do 45 | allow(Bundle::MacAppStoreInstaller).to receive(:installed_app_ids).and_return([]) 46 | end 47 | 48 | it "installs app" do 49 | expect(Bundle).to receive(:system).with("mas", "install", "123").and_return(true) 50 | expect(do_install).to eql(true) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/bundle/commands/exec.rb: -------------------------------------------------------------------------------- 1 | require "extend/ENV" 2 | require "formula" 3 | require "utils" 4 | 5 | module Bundle::Commands 6 | class Exec 7 | def self.run 8 | args = [] 9 | ARGV.each_with_index do |arg, i| 10 | if arg == "--" 11 | args = ARGV.slice!(i+1..-1) 12 | break 13 | elsif !arg.start_with?("-") 14 | args = ARGV.slice!(i..-1) 15 | break 16 | end 17 | end 18 | 19 | # Setup Homebrew's ENV extensions 20 | ENV.activate_extensions! 21 | if args.empty? 22 | raise RuntimeError, "No command to execute was specified!" 23 | end 24 | 25 | command = args[0] 26 | 27 | # Save the command path, since this will be blown away by superenv 28 | command_path = which(command) 29 | raise RuntimeError, "Error: #{command} was not found on your PATH!" if command_path.nil? 30 | command_path = command_path.dirname.to_s 31 | 32 | brewfile = Bundle::Dsl.new(Bundle.brewfile) 33 | ENV.deps = brewfile.entries.map do |entry| 34 | next unless entry.type == :brew 35 | f = Formulary.factory(entry.name) 36 | [f, f.recursive_dependencies.map(&:to_formula)] 37 | end.flatten.compact 38 | ENV.keg_only_deps = ENV.deps.select(&:keg_only?) 39 | ENV.setup_build_environment 40 | 41 | # Enable compiler flag filtering 42 | ENV.refurbish_args 43 | 44 | # Setup pkg-config, if present, to help locate packages 45 | pkgconfig = Formulary.factory("pkg-config") 46 | ENV.prepend_path "PATH", pkgconfig.opt_bin.to_s if pkgconfig.installed? 47 | 48 | # Ensure the Ruby path we saved goes before anything else 49 | ENV.prepend_path "PATH", command_path 50 | 51 | exec *args 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/install_command_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Commands::Install do 4 | context "when a Brewfile is not found" do 5 | it "raises an error" do 6 | allow(ARGV).to receive(:value).and_return(nil) 7 | expect { Bundle::Commands::Install.run }.to raise_error(RuntimeError) 8 | end 9 | end 10 | 11 | context "when a Brewfile is found" do 12 | it "does not raise an error" do 13 | allow(Bundle::BrewInstaller).to receive(:install).and_return(true) 14 | allow(Bundle::CaskInstaller).to receive(:install).and_return(true) 15 | allow(Bundle::MacAppStoreInstaller).to receive(:install).and_return(true) 16 | allow(Bundle::TapInstaller).to receive(:install).and_return(true) 17 | 18 | allow(ARGV).to receive(:value).and_return(nil) 19 | allow_any_instance_of(Pathname).to receive(:read). 20 | and_return("tap 'phinze/cask'\nbrew 'mysql', conflicts_with: ['mysql56']\ncask 'google-chrome'\nmas '1Password', id: 443987910") 21 | expect { Bundle::Commands::Install.run }.to_not raise_error 22 | end 23 | 24 | it "exits on failures" do 25 | allow(Bundle::BrewInstaller).to receive(:install).and_return(false) 26 | allow(Bundle::CaskInstaller).to receive(:install).and_return(false) 27 | allow(Bundle::MacAppStoreInstaller).to receive(:install).and_return(false) 28 | allow(Bundle::TapInstaller).to receive(:install).and_return(false) 29 | 30 | allow(ARGV).to receive(:value).and_return(nil) 31 | allow_any_instance_of(Pathname).to receive(:read). 32 | and_return("tap 'phinze/cask'\nbrew 'mysql', conflicts_with: ['mysql56']\ncask 'google-chrome'\n\nmas '1Password', id: 443987910") 33 | expect { Bundle::Commands::Install.run }.to raise_error(SystemExit) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle do 4 | context "system call succeed" do 5 | it "omits all stdout output if ARGV.verbose? is false" do 6 | allow(ARGV).to receive(:verbose?).and_return(false) 7 | expect { Bundle.system "echo", "foo" }.to_not output.to_stdout_from_any_process 8 | end 9 | 10 | it "emits all stdout output if ARGV.verbose? is true" do 11 | allow(ARGV).to receive(:verbose?).and_return(true) 12 | expect { Bundle.system "echo", "foo" }.to output("foo\n").to_stdout_from_any_process 13 | end 14 | end 15 | 16 | context "system call failed" do 17 | before do 18 | allow_any_instance_of(Process::Status).to receive(:success?).and_return(false) 19 | end 20 | 21 | it "emits all stdout output even if ARGV.verbose? is false" do 22 | allow(ARGV).to receive(:verbose?).and_return(false) 23 | expect { Bundle.system "echo", "foo" }.to output("foo\n").to_stdout_from_any_process 24 | end 25 | 26 | it "emits all stdout output only once if ARGV.verbose? is true" do 27 | allow(ARGV).to receive(:verbose?).and_return(true) 28 | expect { Bundle.system "echo", "foo" }.to output("foo\n").to_stdout_from_any_process 29 | end 30 | end 31 | 32 | context "check for brew cask" do 33 | it "finds it when present" do 34 | allow(Bundle).to receive(:which).and_return(true) 35 | expect(Bundle.cask_installed?).to eql(true) 36 | end 37 | end 38 | 39 | context "check for brew services" do 40 | it "finds it when present" do 41 | allow(Bundle).to receive(:which).and_return(true) 42 | expect(Bundle.services_installed?).to eql(true) 43 | end 44 | end 45 | 46 | context "check for mas" do 47 | it "finds it when present" do 48 | allow(Bundle).to receive(:which).and_return(true) 49 | expect(Bundle.mas_installed?).to eql(true) 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/bundle/commands/check.rb: -------------------------------------------------------------------------------- 1 | module Bundle::Commands 2 | class Check 3 | def self.reset! 4 | @dsl = nil 5 | Bundle::CaskDumper.reset! 6 | Bundle::BrewDumper.reset! 7 | Bundle::MacAppStoreDumper.reset! 8 | Bundle::TapDumper.reset! 9 | Bundle::BrewServices.reset! 10 | end 11 | 12 | def self.run 13 | if any_taps_to_tap? || any_casks_to_install? || any_apps_to_install? || any_formulae_to_install? 14 | puts "brew bundle can't satisfy your Brewfile's dependencies." 15 | puts "Install missing dependencies with `brew bundle install`." 16 | exit 1 17 | else 18 | puts "The Brewfile's dependencies are satisfied." 19 | end 20 | end 21 | 22 | private 23 | 24 | def self.any_casks_to_install? 25 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 26 | requested_casks = @dsl.entries.select { |e| e.type == :cask }.map(&:name) 27 | return false if requested_casks.empty? 28 | current_casks = Bundle::CaskDumper.casks 29 | (requested_casks - current_casks).any? 30 | end 31 | 32 | def self.any_formulae_to_install? 33 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 34 | requested_formulae = @dsl.entries.select { |e| e.type == :brew }.map(&:name) 35 | requested_formulae.any? do |f| 36 | !Bundle::BrewInstaller.formula_installed_and_up_to_date?(f) 37 | end 38 | end 39 | 40 | def self.any_taps_to_tap? 41 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 42 | requested_taps = @dsl.entries.select { |e| e.type == :tap }.map(&:name) 43 | return false if requested_taps.empty? 44 | current_taps = Bundle::TapDumper.tap_names 45 | (requested_taps - current_taps).any? 46 | end 47 | 48 | def self.any_apps_to_install? 49 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 50 | requested_apps = @dsl.entries.select { |e| e.type == :mac_app_store }.map {|e| e.options[:id] } 51 | return false if requested_apps.empty? 52 | current_apps = Bundle::MacAppStoreDumper.app_ids 53 | (requested_apps - current_apps).any? 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/brew_services_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::BrewServices do 4 | context ".started_services" do 5 | before do 6 | Bundle::BrewServices.reset! 7 | end 8 | 9 | it "is empty when brew servies not installed" do 10 | allow(Bundle).to receive(:services_installed?).and_return(false) 11 | expect(Bundle::BrewServices.started_services).to be_empty 12 | end 13 | 14 | it "returns started services" do 15 | allow(Bundle).to receive(:services_installed?).and_return(true) 16 | allow(Bundle::BrewServices).to receive(:`).and_return <<-EOS 17 | nginx started homebrew.mxcl.nginx.plist 18 | apache stopped homebrew.mxcl.apache.plist 19 | mysql started homebrew.mxcl.mysql.plist 20 | EOS 21 | expect(Bundle::BrewServices.started_services).to contain_exactly("nginx", "mysql") 22 | end 23 | end 24 | 25 | context "when brew-services is installed" do 26 | before do 27 | allow(ARGV).to receive(:verbose?).and_return(false) 28 | end 29 | 30 | context "stops the service" do 31 | it "when the service is started" do 32 | allow(Bundle::BrewServices).to receive(:started_services).and_return(%w[nginx]) 33 | expect(Bundle).to receive(:system).with("brew", "services", "stop", "nginx").and_return(true) 34 | expect(Bundle::BrewServices.stop("nginx")).to eql(true) 35 | expect(Bundle::BrewServices.started_services).not_to include("nginx") 36 | end 37 | 38 | it "when the service is already stopped" do 39 | allow(Bundle::BrewServices).to receive(:started_services).and_return(%w[]) 40 | expect(Bundle).to_not receive(:system).with("brew", "services", "stop", "nginx") 41 | expect(Bundle::BrewServices.stop("nginx")).to eql(true) 42 | expect(Bundle::BrewServices.started_services).not_to include("nginx") 43 | end 44 | end 45 | 46 | it "restarts the service" do 47 | allow(Bundle::BrewServices).to receive(:started_services).and_return([]) 48 | expect(Bundle).to receive(:system).with("brew", "services", "restart", "nginx").and_return(true) 49 | expect(Bundle::BrewServices.restart("nginx")).to eql(true) 50 | expect(Bundle::BrewServices.started_services).to include("nginx") 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/bundle/commands/cleanup.rb: -------------------------------------------------------------------------------- 1 | module Bundle::Commands 2 | class Cleanup 3 | def self.reset! 4 | @dsl = nil 5 | Bundle::CaskDumper.reset! 6 | Bundle::BrewDumper.reset! 7 | Bundle::TapDumper.reset! 8 | Bundle::BrewServices.reset! 9 | end 10 | 11 | def self.run 12 | casks = casks_to_uninstall 13 | formulae = formulae_to_uninstall 14 | taps = taps_to_untap 15 | unless ARGV.force? 16 | if casks.any? 17 | puts "Would uninstall casks:" 18 | puts_columns casks 19 | end 20 | 21 | if formulae.any? 22 | puts "Would uninstall formulae:" 23 | puts_columns formulae 24 | end 25 | 26 | if taps.any? 27 | puts "Would untap:" 28 | puts_columns taps 29 | end 30 | else 31 | if casks.any? 32 | Kernel.system "brew", "cask", "uninstall", "--force", *casks 33 | puts "Uninstalled #{casks.size} cask#{casks.size == 1 ? "" : "s"}" 34 | end 35 | 36 | if formulae.any? 37 | Kernel.system "brew", "uninstall", "--force", *formulae 38 | puts "Uninstalled #{formulae.size} formula#{formulae.size == 1 ? "" : "e"}" 39 | end 40 | 41 | if taps.any? 42 | Kernel.system "brew", "untap", *taps 43 | end 44 | end 45 | end 46 | 47 | private 48 | 49 | def self.casks_to_uninstall 50 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 51 | kept_casks = @dsl.entries.select { |e| e.type == :cask }.map(&:name) 52 | current_casks = Bundle::CaskDumper.casks 53 | current_casks - kept_casks 54 | end 55 | 56 | def self.formulae_to_uninstall 57 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 58 | kept_formulae = @dsl.entries.select { |e| e.type == :brew }.map(&:name) 59 | kept_formulae.map! { |f| Bundle::BrewDumper.formula_aliases[f] || f } 60 | current_formulae = Bundle::BrewDumper.formulae 61 | current_formulae.reject do |f| 62 | Bundle::BrewInstaller.formula_in_array?(f[:full_name], kept_formulae) 63 | end.map { |f| f[:full_name] } 64 | end 65 | 66 | IGNORED_TAPS = %w[homebrew/core homebrew/bundle].freeze 67 | 68 | def self.taps_to_untap 69 | @dsl ||= Bundle::Dsl.new(Bundle.brewfile) 70 | kept_taps = @dsl.entries.select { |e| e.type == :tap }.map(&:name) 71 | current_taps = Bundle::TapDumper.tap_names 72 | current_taps - kept_taps - IGNORED_TAPS 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/check_command_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Commands::Check do 4 | def do_check 5 | Bundle::Commands::Check.run 6 | end 7 | 8 | context "when dependencies are satisfied" do 9 | it "does not raise an error" do 10 | allow(Bundle::Commands::Check).to receive(:any_casks_to_install?).and_return(false) 11 | allow(Bundle::Commands::Check).to receive(:any_formulae_to_install?).and_return(false) 12 | allow(Bundle::Commands::Check).to receive(:any_taps_to_tap?).and_return(false) 13 | allow(Bundle::Commands::Check).to receive(:any_apps_to_install?).and_return(false) 14 | expect { do_check }.to_not raise_error 15 | end 16 | end 17 | 18 | context "when casks are not installed" do 19 | before do 20 | Bundle::Commands::Check.reset! 21 | end 22 | 23 | it "raises an error" do 24 | allow(Bundle).to receive(:cask_installed?).and_return(true) 25 | allow_any_instance_of(Bundle::CaskDumper).to receive(:casks).and_return([]) 26 | allow(Bundle::BrewInstaller).to receive(:upgradable_formulae).and_return([]) 27 | allow(ARGV).to receive(:include?).and_return(true) 28 | allow_any_instance_of(Pathname).to receive(:read).and_return("cask 'abc'") 29 | expect { do_check }.to raise_error(SystemExit) 30 | end 31 | end 32 | 33 | context "when formulae are not installed" do 34 | before do 35 | Bundle::Commands::Check.reset! 36 | end 37 | 38 | it "raises an error" do 39 | allow_any_instance_of(Bundle::CaskDumper).to receive(:casks).and_return([]) 40 | allow(Bundle::BrewInstaller).to receive(:upgradable_formulae).and_return([]) 41 | allow(ARGV).to receive(:include?).and_return(true) 42 | allow_any_instance_of(Pathname).to receive(:read).and_return("brew 'abc'") 43 | expect { do_check }.to raise_error(SystemExit) 44 | end 45 | end 46 | 47 | context "when taps are not tapped" do 48 | before do 49 | Bundle::Commands::Check.reset! 50 | end 51 | 52 | it "raises an error" do 53 | allow_any_instance_of(Bundle::CaskDumper).to receive(:casks).and_return([]) 54 | allow(Bundle::BrewInstaller).to receive(:upgradable_formulae).and_return([]) 55 | allow(ARGV).to receive(:include?).and_return(true) 56 | allow_any_instance_of(Pathname).to receive(:read).and_return("tap 'abc/def'") 57 | expect { do_check }.to raise_error(SystemExit) 58 | end 59 | end 60 | 61 | context "when apps are not installed" do 62 | before do 63 | Bundle::Commands::Check.reset! 64 | end 65 | 66 | it "raises an error" do 67 | allow_any_instance_of(Bundle::MacAppStoreDumper).to receive(:app_ids).and_return([]) 68 | allow(Bundle::BrewInstaller).to receive(:upgradable_formulae).and_return([]) 69 | allow(ARGV).to receive(:include?).and_return(true) 70 | allow_any_instance_of(Pathname).to receive(:read).and_return("mas 'foo', id: 123") 71 | expect { do_check }.to raise_error(SystemExit) 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /cmd/brew-bundle.rb: -------------------------------------------------------------------------------- 1 | # Homebrew version check 2 | # commit cf71e30180d44219836ef129d5e5f00325210dfb 3 | MIN_HOMEBREW_COMMIT_DATE = Time.parse "Wed Aug 17 11:07:17 2016 +0100" 4 | HOMEBREW_REPOSITORY.cd do 5 | if MIN_HOMEBREW_COMMIT_DATE > Time.parse(`git show -s --format=%cD`) 6 | odie "Your Homebrew is outdated. Please run `brew update`." 7 | end 8 | end 9 | 10 | BUNDLE_ROOT = File.expand_path "#{File.dirname(__FILE__)}/.." 11 | BUNDLE_LIB = Pathname.new(BUNDLE_ROOT)/"lib" 12 | 13 | $LOAD_PATH.unshift(BUNDLE_LIB) 14 | 15 | require "bundle" 16 | 17 | usage = <<-EOS.undent 18 | brew bundle [-v|--verbose] [--file=|--global] 19 | brew bundle dump [--force] [--file=|--global] 20 | brew bundle cleanup [--force] [--file=|--global] 21 | brew bundle check [--file=|--global] 22 | brew bundle exec [command] 23 | brew bundle [--version] 24 | brew bundle [-h|--help] 25 | 26 | Usage: 27 | Bundler for non-Ruby dependencies from Homebrew 28 | 29 | brew bundle install or upgrade all dependencies in a Brewfile 30 | brew bundle dump write all installed casks/formulae/taps into a Brewfile 31 | brew bundle cleanup uninstall all dependencies not listed in a Brewfile 32 | brew bundle check check if all dependencies are installed in a Brewfile 33 | brew bundle exec run an external command in an isolated build environment 34 | 35 | Options: 36 | -v, --verbose print verbose output 37 | --force uninstall dependencies or overwrite existing Brewfile 38 | --file= set Brewfile path (use --file=- to output to console) 39 | --global set Brewfile path to $HOME/.Brewfile 40 | -h, --help show this help message and exit 41 | --version show the version of homebrew-bundle 42 | EOS 43 | 44 | if ARGV.include?("--version") 45 | puts Bundle::VERSION 46 | exit 0 47 | end 48 | 49 | if ARGV.flag?("--help") 50 | puts usage 51 | exit 0 52 | end 53 | 54 | # Pop the named command from ARGV, leaving everything else in place 55 | command = ARGV.named.first 56 | ARGV.delete_at(ARGV.index(command)) unless command.nil? 57 | begin 58 | case command 59 | when nil, "install" 60 | Bundle::Commands::Install.run 61 | when "dump" 62 | Bundle::Commands::Dump.run 63 | when "cleanup" 64 | Bundle::Commands::Cleanup.run 65 | when "check" 66 | Bundle::Commands::Check.run 67 | when "exec" 68 | Bundle::Commands::Exec.run 69 | else 70 | abort usage 71 | end 72 | rescue SystemExit 73 | puts "Kernel.exit" if ARGV.verbose? 74 | raise 75 | rescue Interrupt 76 | puts # seemingly a newline is typical 77 | exit 130 78 | rescue RuntimeError, SystemCallError => e 79 | raise if e.message.empty? 80 | onoe e 81 | puts e.backtrace if ARGV.debug? 82 | exit 1 83 | rescue Exception => e 84 | onoe e 85 | puts "#{Tty.white}Please report this bug:" 86 | puts " #{Tty.em}https://github.com/Homebrew/homebrew-bundle/issues/#{Tty.reset}" 87 | puts e.backtrace 88 | exit 1 89 | end 90 | -------------------------------------------------------------------------------- /CODEOFCONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | The Homebrew community is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences great successes and continued growth. When you're working with members of the community, we encourage you to follow these guidelines which help steer our interactions and strive to keep Homebrew a positive, successful, and growing community. 3 | 4 | A member of the Homebrew community is: 5 | 6 | ## Open 7 | Members of the community are open to collaboration, whether it's on GitHub, email, IRC or otherwise. We're receptive to constructive comment and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts. We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference. 8 | 9 | ## Considerate 10 | Members of the community are considerate of their peers - other Homebrew users. We're thoughtful when addressing the efforts of others, keeping in mind that oftentimes their labor was completed simply for the good of the community. We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views. 11 | 12 | ## Respectful 13 | Members of the community are respectful. We're respectful of others, their positions, their skills, their commitments, and their efforts. We're respectful of the volunteer efforts that permeate the Homebrew community. We're respectful of the processes set forth in the community, and we work within them. When we disagree, we are courteous in raising our issues. 14 | 15 | Overall, we're good to each other. We contribute to this community not because we have to, but because we want to. If we remember that, these guidelines will come naturally. 16 | 17 | # Diversity 18 | The Homebrew community welcomes and encourages participation by everyone. Our community is based on mutual respect, tolerance, and encouragement, and we are working to help each other live up to these principles. We want our community to be more diverse: whoever you are, and whatever your background, we welcome you. 19 | 20 | We have created this diversity statement because we believe that a diverse Homebrew community is stronger and more vibrant. A diverse community where people treat each other with respect has more potential contributors and more sources for ideas. 21 | 22 | Although we have phrased the formal diversity statement generically to make it all-inclusive, we recognise that there are specific attributes that are used to discriminate against people. In alphabetical order, some of these attributes include (but are not limited to): age, culture, ethnicity, gender identity or expression, national origin, physical or mental difference, politics, race, religion, sex, sexual orientation, socio-economic status, and subculture. We welcome people regardless of the values of these or other attributes. 23 | 24 | # Attribution 25 | This code of conduct is heavily based on the [Python Community Code of Conduct](https://www.python.org/psf/codeofconduct/) and the [Python Diversity Statement](https://www.python.org/community/diversity/). 26 | -------------------------------------------------------------------------------- /spec/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Dsl do 4 | it "processes input" do 5 | allow_any_instance_of(Bundle::Dsl).to receive(:system).with("/usr/libexec/java_home --failfast").and_return(false) 6 | allow(ARGV).to receive(:verbose?).and_return(true) 7 | # Keep in sync with the README 8 | dsl = Bundle::Dsl.new <<-EOS 9 | cask_args appdir: '/Applications' 10 | tap 'caskroom/cask' 11 | tap 'telemachus/brew', 'https://telemachus@bitbucket.org/telemachus/brew.git' 12 | brew 'imagemagick' 13 | brew 'mysql', restart_service: true, conflicts_with: ['homebrew/versions/mysql56'] 14 | brew 'emacs', args: ['with-cocoa', 'with-gnutls'] 15 | cask 'google-chrome' 16 | cask 'java' unless system '/usr/libexec/java_home --failfast' 17 | cask 'firefox', args: { appdir: '~/my-apps/Applications' } 18 | mas '1Password', id: 443987910 19 | EOS 20 | expect(dsl.cask_arguments).to eql(:appdir => "/Applications") 21 | expect(dsl.entries[0].name).to eql("caskroom/cask") 22 | expect(dsl.entries[1].name).to eql("telemachus/brew") 23 | expect(dsl.entries[1].options).to eql(:clone_target => "https://telemachus@bitbucket.org/telemachus/brew.git") 24 | expect(dsl.entries[2].name).to eql("imagemagick") 25 | expect(dsl.entries[3].name).to eql("mysql") 26 | expect(dsl.entries[3].options).to eql(:restart_service => true, :conflicts_with => ["homebrew/versions/mysql56"]) 27 | expect(dsl.entries[4].name).to eql("emacs") 28 | expect(dsl.entries[4].options).to eql(:args => ["with-cocoa", "with-gnutls"]) 29 | expect(dsl.entries[5].name).to eql("google-chrome") 30 | expect(dsl.entries[6].name).to eql("java") 31 | expect(dsl.entries[7].name).to eql("firefox") 32 | expect(dsl.entries[7].options).to eql(:args => { :appdir=>"~/my-apps/Applications" }) 33 | expect(dsl.entries[8].name).to eql("1Password") 34 | expect(dsl.entries[8].options).to eql(:id => 443987910) 35 | end 36 | 37 | it "handles invalid input" do 38 | allow(ARGV).to receive(:verbose?).and_return(true) 39 | expect { Bundle::Dsl.new "abcdef" }.to raise_error(RuntimeError) 40 | expect { Bundle::Dsl.new "cask_args ''" }.to raise_error(RuntimeError) 41 | expect { Bundle::Dsl.new "brew 1" }.to raise_error(RuntimeError) 42 | expect { Bundle::Dsl.new "brew 'foo', ['bad_option']" }.to raise_error(RuntimeError) 43 | expect { Bundle::Dsl.new "cask 1" }.to raise_error(RuntimeError) 44 | expect { Bundle::Dsl.new "cask 'foo', ['bad_option']" }.to raise_error(RuntimeError) 45 | expect { Bundle::Dsl.new "tap 1" }.to raise_error(RuntimeError) 46 | expect { Bundle::Dsl.new "tap 'foo', ['bad_clone_target']" }.to raise_error(RuntimeError) 47 | end 48 | 49 | it ".sanitize_brew_name" do 50 | expect(Bundle::Dsl.send(:sanitize_brew_name, "homebrew/homebrew/foo")).to eql("foo") 51 | expect(Bundle::Dsl.send(:sanitize_brew_name, "homebrew/homebrew-bar/foo")).to eql("homebrew/bar/foo") 52 | expect(Bundle::Dsl.send(:sanitize_brew_name, "homebrew/bar/foo")).to eql("homebrew/bar/foo") 53 | expect(Bundle::Dsl.send(:sanitize_brew_name, "foo")).to eql("foo") 54 | end 55 | 56 | it ".sanitize_tap_name" do 57 | expect(Bundle::Dsl.send(:sanitize_tap_name, "homebrew/homebrew-foo")).to eql("homebrew/foo") 58 | expect(Bundle::Dsl.send(:sanitize_tap_name, "homebrew/foo")).to eql("homebrew/foo") 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/exec_command_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Commands::Exec do 4 | context "when a Brewfile is not found" do 5 | it "raises an error" do 6 | allow(ARGV).to receive(:value).and_return(nil) 7 | expect { Bundle::Commands::Exec.run }.to raise_error(RuntimeError) 8 | end 9 | end 10 | 11 | context "when a Brewfile is found" do 12 | it "does not raise an error" do 13 | stub_const("ARGV", ["bundle", "install"]) 14 | allow(Object).to receive(:exec).and_return(nil) 15 | allow(ARGV).to receive(:value).and_return(nil) 16 | allow_any_instance_of(Pathname).to receive(:read). 17 | and_return("brew 'openssl'") 18 | 19 | expect { Bundle::Commands::Exec.run }.to_not raise_error 20 | end 21 | 22 | it "should be able to run without bundle arguments" do 23 | stub_const("ARGV", ["bundle", "install"]) 24 | allow(Object).to receive(:exec).with("bundle", "install").and_return(nil) 25 | allow(ARGV).to receive(:value).and_return(nil) 26 | allow(ARGV).to receive(:verbose?).and_return(true) 27 | allow_any_instance_of(Pathname).to receive(:read). 28 | and_return("brew 'openssl'") 29 | 30 | expect { Bundle::Commands::Exec.run }.to_not raise_error 31 | end 32 | 33 | it "should be able to accept arguments passed prior to the command" do 34 | stub_const("ARGV", ["--verbose", "--", "bundle", "install"]) 35 | allow(Object).to receive(:exec).with("bundle", "install").and_return(nil) 36 | allow(ARGV).to receive(:value).and_return(nil) 37 | allow(ARGV).to receive(:verbose?).and_return(true) 38 | allow_any_instance_of(Pathname).to receive(:read). 39 | and_return("brew 'openssl'") 40 | 41 | expect { Bundle::Commands::Exec.run }.to_not raise_error 42 | end 43 | 44 | it "should raise an exception if called without a command" do 45 | stub_const("ARGV", []) 46 | allow(Object).to receive(:exec).and_return(nil) 47 | allow(ARGV).to receive(:value).and_return(nil) 48 | allow(ARGV).to receive(:verbose?).and_return(true) 49 | allow_any_instance_of(Pathname).to receive(:read). 50 | and_return("brew 'openssl'") 51 | 52 | expect { Bundle::Commands::Exec.run }.to raise_error(RuntimeError) 53 | end 54 | 55 | it "should raise if called with a command that's not on the PATH" do 56 | stub_const("ARGV", ["bundle", "install"]) 57 | allow(Object).to receive(:exec).and_return(nil) 58 | allow(Object).to receive(:which).and_return(nil) 59 | allow(ARGV).to receive(:value).and_return(nil) 60 | allow(ARGV).to receive(:verbose?).and_return(true) 61 | allow_any_instance_of(Pathname).to receive(:read). 62 | and_return("brew 'openssl'") 63 | 64 | expect { Bundle::Commands::Exec.run }.to raise_error(RuntimeError) 65 | end 66 | 67 | it "should prepend the path of the requested command to PATH before running" do 68 | stub_const("ARGV", ["bundle", "install"]) 69 | allow(Object).to receive(:exec).with("bundle", "install").and_return(nil) 70 | allow(Object).to receive(:which).and_return(Pathname("/usr/local/bin/bundle")) 71 | allow(ARGV).to receive(:value).and_return(nil) 72 | allow(ARGV).to receive(:verbose?).and_return(true) 73 | allow(ENV).to receive(:prepend_path).with("/usr/local/bin").and_return(nil) 74 | allow_any_instance_of(Pathname).to receive(:read). 75 | and_return("brew 'openssl'") 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/bundle/dsl.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class Dsl 3 | class Entry 4 | attr_reader :type, :name, :options 5 | 6 | def initialize(type, name, options = {}) 7 | @type = type 8 | @name = name 9 | @options = options 10 | end 11 | end 12 | 13 | attr_reader :entries, :cask_arguments 14 | 15 | def initialize(input) 16 | @input = input 17 | @entries = [] 18 | @cask_arguments = {} 19 | 20 | begin 21 | process 22 | rescue => e 23 | error_msg = "Invalid Brewfile: #{e.message}" 24 | raise RuntimeError, error_msg, e.backtrace 25 | end 26 | end 27 | 28 | def process 29 | instance_eval(@input) 30 | end 31 | 32 | def install 33 | success = 0 34 | failure = 0 35 | 36 | @entries.each do |entry| 37 | arg = [entry.name] 38 | verb = "installing" 39 | cls = case entry.type 40 | when :brew 41 | arg << entry.options 42 | Bundle::BrewInstaller 43 | when :cask 44 | arg << entry.options 45 | Bundle::CaskInstaller 46 | when :mac_app_store 47 | arg << entry.options[:id] 48 | Bundle::MacAppStoreInstaller 49 | when :tap 50 | verb = "tapping" 51 | arg << entry.options[:clone_target] 52 | Bundle::TapInstaller 53 | end 54 | if cls.install(*arg) 55 | puts "Succeeded in #{verb} #{entry.name}" 56 | success += 1 57 | else 58 | puts "Failed in #{verb} #{entry.name}" 59 | failure += 1 60 | end 61 | end 62 | puts "\nSuccess: #{success} Fail: #{failure}" 63 | 64 | failure.zero? 65 | end 66 | 67 | def cask_args(args) 68 | raise "cask_args(#{args.inspect}) should be a Hash object" unless args.is_a? Hash 69 | @cask_arguments = args 70 | end 71 | 72 | def brew(name, options = {}) 73 | raise "name(#{name.inspect}) should be a String object" unless name.is_a? String 74 | raise "options(#{options.inspect}) should be a Hash object" unless options.is_a? Hash 75 | name = Bundle::Dsl.sanitize_brew_name(name) 76 | @entries << Entry.new(:brew, name, options) 77 | end 78 | 79 | def cask(name, options = {}) 80 | raise "name(#{name.inspect}) should be a String object" unless name.is_a? String 81 | raise "options(#{options.inspect}) should be a Hash object" unless options.is_a? Hash 82 | name = Bundle::Dsl.sanitize_cask_name(name) 83 | options[:args] = @cask_arguments.merge options.fetch(:args, {}) 84 | @entries << Entry.new(:cask, name, options) 85 | end 86 | 87 | def mas(name, options = {}) 88 | id = options[:id] 89 | raise "name(#{name.inspect}) should be a String object" unless name.is_a? String 90 | raise "options[:id](#{id}) should be an Integer object" unless id.is_a? Integer 91 | @entries << Entry.new(:mac_app_store, name, :id => id) 92 | end 93 | 94 | def tap(name, clone_target = nil) 95 | raise "name(#{name.inspect}) should be a String object" unless name.is_a? String 96 | raise "clone_target(#{clone_target.inspect}) should be nil or a String object" if clone_target && !clone_target.is_a?(String) 97 | name = Bundle::Dsl.sanitize_tap_name(name) 98 | @entries << Entry.new(:tap, name, :clone_target => clone_target) 99 | end 100 | 101 | private 102 | 103 | HOMEBREW_TAP_ARGS_REGEX = %r{^([\w-]+)/(homebrew-)?([\w-]+)$}.freeze 104 | HOMEBREW_CORE_FORMULA_REGEX = %r{^homebrew/homebrew/([\w+-.]+)$}i.freeze 105 | HOMEBREW_TAP_FORMULA_REGEX = %r{^([\w-]+)/([\w-]+)/([\w+-.]+)$}.freeze 106 | 107 | def self.sanitize_brew_name(name) 108 | name.downcase! 109 | if name =~ HOMEBREW_CORE_FORMULA_REGEX 110 | $1 111 | elsif name =~ HOMEBREW_TAP_FORMULA_REGEX 112 | user = $1 113 | repo = $2 114 | name = $3 115 | "#{user}/#{repo.sub(/homebrew-/, "")}/#{name}" 116 | else 117 | name 118 | end 119 | end 120 | 121 | def self.sanitize_tap_name(name) 122 | name.downcase! 123 | if name =~ HOMEBREW_TAP_ARGS_REGEX 124 | "#{$1}/#{$3}" 125 | else 126 | name 127 | end 128 | end 129 | 130 | def self.sanitize_cask_name(name) 131 | name.downcase 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/bundle/brew_dumper.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "tsort" 3 | 4 | module Bundle 5 | class BrewDumper 6 | def self.reset! 7 | Bundle::BrewServices.reset! 8 | @formulae = nil 9 | @formula_aliases = nil 10 | end 11 | 12 | def self.formulae 13 | @formulae ||= begin 14 | @formulae = formulae_info 15 | sort! 16 | end 17 | end 18 | 19 | def self.dump 20 | formulae.map do |f| 21 | brewline = "brew '#{f[:full_name]}'" 22 | args = f[:args].map { |arg| "'#{arg}'" }.sort.join(", ") 23 | brewline += ", args: [#{args}]" unless f[:args].empty? 24 | brewline += ", service_restart: true" if BrewServices.started?(f[:full_name]) 25 | brewline 26 | end.join("\n") 27 | end 28 | 29 | def self.cask_requirements 30 | formulae.map { |f| f[:requirements].map { |req| req["cask"] } }.flatten.compact.uniq 31 | end 32 | 33 | def self.formula_names 34 | formulae.map { |f| f[:name] } 35 | end 36 | 37 | def self.formula_aliases 38 | return @formula_aliases if @formula_aliases 39 | @formula_aliases = {} 40 | formulae.each do |f| 41 | aliases = f[:aliases] 42 | next if !aliases || aliases.empty? 43 | if f[:full_name].include? "/" # tap formula 44 | aliases.each do |a| 45 | @formula_aliases[a] = f[:full_name] 46 | @formula_aliases["#{f[:full_name].rpartition("/").first}/#{a}"] = f[:full_name] 47 | end 48 | else 49 | aliases.each { |a| @formula_aliases[a] = f[:full_name] } 50 | end 51 | end 52 | @formula_aliases 53 | end 54 | 55 | def self.formula_info(name) 56 | @formula_info_name ||= {} 57 | @formula_info_name[name] ||= begin 58 | require "formula" 59 | formula = Formula[name] 60 | return {} unless formula 61 | formula_inspector formula.to_hash 62 | end 63 | end 64 | 65 | private 66 | 67 | def self.formulae_info 68 | require "formula" 69 | Formula.installed.map { |f| formula_inspector f.to_hash } 70 | end 71 | 72 | def self.formula_inspector(f) 73 | installed = f["installed"] 74 | if f["linked_keg"].nil? 75 | keg = installed.last 76 | else 77 | keg = installed.detect { |k| f["linked_keg"] == k["version"] } 78 | end 79 | 80 | if keg 81 | args = keg["used_options"].to_a.map { |option| option.gsub(/^--/, "") } 82 | args << "HEAD" if keg["version"].to_s.start_with?("HEAD") 83 | args << "devel" if keg["version"].to_s.gsub(/_\d+$/, "") == f["versions"]["devel"] 84 | args.uniq! 85 | version = keg["version"] 86 | else 87 | args = [] 88 | version = nil 89 | end 90 | 91 | { 92 | :name => f["name"], 93 | :full_name => f["full_name"], 94 | :aliases => f["aliases"], 95 | :args => args, 96 | :version => version, 97 | :dependencies => f["dependencies"], 98 | :requirements => f["requirements"], 99 | :conflicts_with => f["conflicts_with"], 100 | :pinned? => !!f["pinned"], 101 | :outdated? => !!f["outdated"], 102 | } 103 | end 104 | 105 | class Topo < Hash 106 | include TSort 107 | alias_method :tsort_each_node, :each_key 108 | def tsort_each_child(node, &block) 109 | fetch(node).sort.each(&block) 110 | end 111 | end 112 | 113 | def self.sort! 114 | # Step 1: Sort by formula full name while putting tap formulae behind core formulae. 115 | # So we can have a nicer output. 116 | @formulae.sort! do |a, b| 117 | if !a[:full_name].include?("/") && b[:full_name].include?("/") 118 | -1 119 | elsif a[:full_name].include?("/") && !b[:full_name].include?("/") 120 | 1 121 | else 122 | a[:full_name] <=> b[:full_name] 123 | end 124 | end 125 | 126 | # Step 2: Sort by formula dependency topology. 127 | topo = Topo.new 128 | @formulae.each do |f| 129 | deps = (f[:dependencies] + f[:requirements].map { |req| req["default_formula"] }.compact).uniq 130 | topo[f[:full_name]] = deps.map do |dep| 131 | ff = @formulae.detect { |formula| formula[:name] == dep || formula[:full_name] == dep } 132 | ff[:full_name] if ff 133 | end.compact 134 | end 135 | @formulae = topo.tsort.map { |name| @formulae.detect { |formula| formula[:full_name] == name } } 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/bundle/brew_installer.rb: -------------------------------------------------------------------------------- 1 | module Bundle 2 | class BrewInstaller 3 | def self.reset! 4 | @installed_formulae = nil 5 | @outdated_formulae = nil 6 | @pinned_formulae = nil 7 | end 8 | 9 | def self.install(name, options = {}) 10 | new(name, options).run 11 | end 12 | 13 | def initialize(name, options = {}) 14 | @full_name = name 15 | @name = name.split("/").last 16 | @args = options.fetch(:args, []).map { |arg| "--#{arg}" } 17 | @conflicts_with_arg = options.fetch(:conflicts_with, []) 18 | @restart_service = options.fetch(:restart_service, false) 19 | end 20 | 21 | def run 22 | install_change_state! && service_change_state! 23 | end 24 | 25 | def install_change_state! 26 | return false unless resolve_conflicts! 27 | if installed? 28 | upgrade! 29 | else 30 | install! 31 | end 32 | end 33 | 34 | def restart_service? 35 | # Restart if `restart_service: :always`, or if the formula was installed or upgraded 36 | @restart_service && (@restart_service.to_s != 'changed' || changed?) 37 | end 38 | 39 | def changed? 40 | @changed 41 | end 42 | 43 | def service_change_state! 44 | if restart_service? 45 | puts "Restarting #{@name} service." if ARGV.verbose? 46 | BrewServices.restart(@full_name) 47 | else 48 | true 49 | end 50 | end 51 | 52 | def self.formula_installed_and_up_to_date?(formula) 53 | formula_installed?(formula) && !formula_upgradable?(formula) 54 | end 55 | 56 | def self.formula_in_array?(formula, array) 57 | return true if array.include?(formula) 58 | return true if array.include?(formula.split("/").last) 59 | resolved_full_name = Bundle::BrewDumper.formula_aliases[formula] 60 | return false unless resolved_full_name 61 | return true if array.include?(resolved_full_name) 62 | return true if array.include?(resolved_full_name.split("/").last) 63 | false 64 | end 65 | 66 | private 67 | 68 | def self.formula_installed?(formula) 69 | formula_in_array?(formula, installed_formulae) 70 | end 71 | 72 | def self.formula_upgradable?(formula) 73 | formula_in_array?(formula, upgradable_formulae) 74 | end 75 | 76 | def self.installed_formulae 77 | @installed_formulae ||= Bundle::BrewDumper.formula_names 78 | end 79 | 80 | def self.upgradable_formulae 81 | outdated_formulae - pinned_formulae 82 | end 83 | 84 | def self.outdated_formulae 85 | @outdated_formulae ||= Bundle::BrewDumper.formulae.map { |f| f[:name] if f[:outdated?] }.compact 86 | end 87 | 88 | def self.pinned_formulae 89 | @pinned_formulae ||= Bundle::BrewDumper.formulae.map { |f| f[:name] if f[:pinned?] }.compact 90 | end 91 | 92 | def installed? 93 | BrewInstaller.formula_installed?(@name) 94 | end 95 | 96 | def upgradable? 97 | BrewInstaller.formula_upgradable?(@name) 98 | end 99 | 100 | def conflicts_with 101 | @conflicts_with ||= begin 102 | conflicts_with = Set.new 103 | conflicts_with += @conflicts_with_arg 104 | 105 | if (formula_info = Bundle::BrewDumper.formula_info(@full_name)) 106 | if (formula_conflicts_with = formula_info[:conflicts_with]) 107 | conflicts_with += formula_conflicts_with 108 | end 109 | end 110 | 111 | conflicts_with.to_a 112 | end 113 | end 114 | 115 | def resolve_conflicts! 116 | conflicts_with.each do |conflict| 117 | if BrewInstaller.formula_installed?(conflict) 118 | if ARGV.verbose? 119 | puts <<-EOS.undent 120 | Unlinking #{conflict} formula. 121 | It is currently installed and conflicts with #{@name}. 122 | EOS 123 | end 124 | return false unless Bundle.system("brew", "unlink", conflict) 125 | if @restart_service 126 | puts "Stopping #{conflict} service (if it is running)." if ARGV.verbose? 127 | BrewServices.stop(conflict) 128 | end 129 | end 130 | end 131 | 132 | true 133 | end 134 | 135 | def install! 136 | puts "Installing #{@name} formula. It is not currently installed." if ARGV.verbose? 137 | if (success = Bundle.system("brew", "install", @full_name, *@args)) 138 | BrewInstaller.installed_formulae << @name 139 | end 140 | @changed = true 141 | 142 | success 143 | end 144 | 145 | def upgrade! 146 | if upgradable? 147 | puts "Upgrading #{@name} formula. It is installed but not up-to-date." if ARGV.verbose? 148 | Bundle.system("brew", "upgrade", @name) 149 | @changed = true 150 | else 151 | puts "Skipping install of #{@name} formula. It is already up-to-date." if ARGV.verbose? 152 | @changed = false 153 | true 154 | end 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/cleanup_command_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::Commands::Cleanup do 4 | context "read Brewfile and currently installation" do 5 | before do 6 | Bundle::Commands::Cleanup.reset! 7 | allow(ARGV).to receive(:value).and_return(nil) 8 | allow_any_instance_of(Pathname).to receive(:read).and_return <<-EOS 9 | tap 'x' 10 | tap 'y' 11 | cask '123' 12 | brew 'a' 13 | brew 'b' 14 | brew 'd2' 15 | brew 'homebrew/tap/f' 16 | brew 'homebrew/tap/g' 17 | brew 'homebrew/tap/h' 18 | brew 'homebrew/tap/i2' 19 | EOS 20 | end 21 | 22 | it "computes which casks to uninstall" do 23 | allow(Bundle::CaskDumper).to receive(:casks).and_return(%w[123 456]) 24 | expect(Bundle::Commands::Cleanup.casks_to_uninstall).to eql(%w[456]) 25 | end 26 | 27 | it "computes which formulae to uninstall" do 28 | allow(Bundle::BrewDumper).to receive(:formulae).and_return [ 29 | { :name => "a2", :full_name => "a2", :aliases => ["a"] }, 30 | { :name => "c", :full_name => "c" }, 31 | { :name => "d", :full_name => "homebrew/tap/d", :aliases => ["d2"] }, 32 | { :name => "e", :full_name => "homebrew/tap/e" }, 33 | { :name => "f", :full_name => "homebrew/tap/f" }, 34 | { :name => "h", :full_name => "other/tap/h" }, 35 | { :name => "i", :full_name => "homebrew/tap/i", :aliases => ["i2"] }, 36 | ] 37 | expect(Bundle::Commands::Cleanup.formulae_to_uninstall).to eql %w[ 38 | c 39 | homebrew/tap/e 40 | other/tap/h 41 | ] 42 | end 43 | 44 | it "computes which tap to untap" do 45 | allow(Bundle::TapDumper).to receive(:tap_names).and_return(%w[z homebrew/bundle homebrew/core]) 46 | expect(Bundle::Commands::Cleanup.taps_to_untap).to eql(%w[z]) 47 | end 48 | end 49 | 50 | context "no formulae to uninstall and no taps to untap" do 51 | before do 52 | Bundle::Commands::Cleanup.reset! 53 | allow(Bundle::Commands::Cleanup).to receive(:casks_to_uninstall).and_return([]) 54 | allow(Bundle::Commands::Cleanup).to receive(:formulae_to_uninstall).and_return([]) 55 | allow(Bundle::Commands::Cleanup).to receive(:taps_to_untap).and_return([]) 56 | allow(ARGV).to receive(:force?).and_return(true) 57 | end 58 | 59 | it "does nothing" do 60 | expect(Kernel).not_to receive(:system) 61 | Bundle::Commands::Cleanup.run 62 | end 63 | end 64 | 65 | context "there are casks to uninstall" do 66 | before do 67 | Bundle::Commands::Cleanup.reset! 68 | allow(Bundle::Commands::Cleanup).to receive(:casks_to_uninstall).and_return(%w[a b]) 69 | allow(Bundle::Commands::Cleanup).to receive(:formulae_to_uninstall).and_return([]) 70 | allow(Bundle::Commands::Cleanup).to receive(:taps_to_untap).and_return([]) 71 | allow(ARGV).to receive(:force?).and_return(true) 72 | end 73 | 74 | it "uninstalls casks" do 75 | expect(Kernel).to receive(:system).with(*%w[brew cask uninstall --force a b]) 76 | expect { Bundle::Commands::Cleanup.run }.to output(/Uninstalled 2 casks/).to_stdout 77 | end 78 | end 79 | 80 | context "there are formulae to uninstall" do 81 | before do 82 | Bundle::Commands::Cleanup.reset! 83 | allow(Bundle::Commands::Cleanup).to receive(:casks_to_uninstall).and_return([]) 84 | allow(Bundle::Commands::Cleanup).to receive(:formulae_to_uninstall).and_return(%w[a b]) 85 | allow(Bundle::Commands::Cleanup).to receive(:taps_to_untap).and_return([]) 86 | allow(ARGV).to receive(:force?).and_return(true) 87 | end 88 | 89 | it "uninstalls formulae" do 90 | expect(Kernel).to receive(:system).with(*%w[brew uninstall --force a b]) 91 | expect { Bundle::Commands::Cleanup.run }.to output(/Uninstalled 2 formulae/).to_stdout 92 | end 93 | end 94 | 95 | context "there are taps to untap" do 96 | before do 97 | Bundle::Commands::Cleanup.reset! 98 | allow(Bundle::Commands::Cleanup).to receive(:casks_to_uninstall).and_return([]) 99 | allow(Bundle::Commands::Cleanup).to receive(:formulae_to_uninstall).and_return([]) 100 | allow(Bundle::Commands::Cleanup).to receive(:taps_to_untap).and_return(%w[a b]) 101 | allow(ARGV).to receive(:force?).and_return(true) 102 | end 103 | 104 | it "untaps taps" do 105 | expect(Kernel).to receive(:system).with(*%w[brew untap a b]) 106 | Bundle::Commands::Cleanup.run 107 | end 108 | end 109 | 110 | context "there are casks and formulae to uninstall and taps to untap but without passing `--force`" do 111 | before do 112 | Bundle::Commands::Cleanup.reset! 113 | allow(Bundle::Commands::Cleanup).to receive(:casks_to_uninstall).and_return(%w[a b]) 114 | allow(Bundle::Commands::Cleanup).to receive(:formulae_to_uninstall).and_return(%w[a b]) 115 | allow(Bundle::Commands::Cleanup).to receive(:taps_to_untap).and_return(%w[a b]) 116 | allow(ARGV).to receive(:force?).and_return(false) 117 | end 118 | 119 | it "lists casks, formulae and taps" do 120 | expect(Bundle::Commands::Cleanup).to receive(:puts_columns).with(%w[a b]).exactly(3).times 121 | expect(Kernel).not_to receive(:system) 122 | expect { Bundle::Commands::Cleanup.run }.to output(/Would uninstall formulae:.*Would untap:/m).to_stdout 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Brew Bundle 2 | 3 | Bundler for non-Ruby dependencies from Homebrew 4 | 5 | [![Code Climate](https://codeclimate.com/github/Homebrew/homebrew-bundle/badges/gpa.svg)](https://codeclimate.com/github/Homebrew/homebrew-bundle) 6 | [![Coverage Status](https://coveralls.io/repos/Homebrew/homebrew-bundle/badge.svg)](https://coveralls.io/r/Homebrew/homebrew-bundle) 7 | [![Build Status](https://travis-ci.org/Homebrew/homebrew-bundle.svg)](https://travis-ci.org/Homebrew/homebrew-bundle) 8 | 9 | ## Requirements 10 | 11 | [Homebrew](https://github.com/Homebrew/brew) or [Linuxbrew](https://github.com/Linuxbrew/brew) are used for installing the dependencies. 12 | Linuxbrew is a fork of Homebrew for Linux, while Homebrew only works on macOS. 13 | This tool is primarily developed for use with Homebrew on macOS but should work with Linuxbrew on Linux, too. 14 | 15 | [brew tap](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/brew-tap.md) is new feature in Homebrew 0.9, adds more GitHub repos to the list of available formulae. 16 | 17 | [Homebrew Cask](https://github.com/caskroom/homebrew-cask) is optional and used for installing Mac applications. 18 | 19 | [mas-cli](https://github.com/argon/mas) is optional and used for installing Mac App Store applications. 20 | 21 | ## Install 22 | 23 | You can install as a Homebrew tap: 24 | 25 | $ brew tap Homebrew/bundle 26 | 27 | ## Usage 28 | 29 | Create a `Brewfile` in the root of your project: 30 | 31 | $ touch Brewfile 32 | 33 | Then list your Homebrew based dependencies in your `Brewfile`: 34 | 35 | ```ruby 36 | cask_args appdir: '/Applications' 37 | tap 'caskroom/cask' 38 | tap 'telemachus/brew', 'https://telemachus@bitbucket.org/telemachus/brew.git' 39 | brew 'imagemagick' 40 | brew 'mysql', restart_service: true, conflicts_with: ['homebrew/versions/mysql56'] 41 | brew 'emacs', args: ['with-cocoa', 'with-gnutls'] 42 | cask 'google-chrome' 43 | cask 'java' unless system '/usr/libexec/java_home --failfast' 44 | cask 'firefox', args: { appdir: '~/my-apps/Applications' } 45 | mas '1Password', id: 443987910 46 | ``` 47 | 48 | You can then easily install all of the dependencies with the following command: 49 | 50 | $ brew bundle 51 | 52 | If a dependency is already installed and there is an update available it will be upgraded. 53 | 54 | ### Dump 55 | 56 | You can create a `Brewfile` from all the existing Homebrew packages you have installed with: 57 | 58 | $ brew bundle dump 59 | 60 | The `--force` option will allow an existing `Brewfile` to be overwritten as well. 61 | 62 | ### Cleanup 63 | 64 | You can also use `Brewfile` as a whitelist. It's useful for maintainers/testers who regularly install lots of formulae. To uninstall all Homebrew formulae not listed in `Brewfile`: 65 | 66 | $ brew bundle cleanup 67 | 68 | Unless the `--force` option is passed, formulae will be listed rather than actually uninstalled. 69 | 70 | ### Check 71 | 72 | You can check there's anything to install/upgrade in the `Brewfile` by running: 73 | 74 | $ brew bundle check 75 | 76 | This provides a successful exit code if everything is up-to-date so is useful for scripting. 77 | 78 | ### Exec 79 | 80 | Runs an external command within Homebrew's [superenv](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/Formula-Cookbook.md#superenv-notes) build environment: 81 | 82 | $ brew bundle exec -- bundle install 83 | 84 | This sanitized build environment ignores unrequested dependencies, which makes sure that things you didn't specify in your `Brewfile` won't get picked up by commands like `bundle install`, `npm install`, etc. It will also add compiler flags which will help find keg-only dependencies like `openssl`, `icu4c`, etc. 85 | 86 | ### Restarting services 87 | 88 | You can choose whether `brew bundle` restarts a service every time it's run, or 89 | only when the formula is installed or upgraded in your `Brewfile`: 90 | 91 | ```ruby 92 | # Always restart myservice 93 | brew 'myservice', restart_service: true 94 | 95 | # Only restart when installing or upgrading myservice 96 | brew 'myservice', restart_service: :changed 97 | ``` 98 | 99 | ## Note 100 | 101 | Homebrew does not support installing specific versions of a library, only the most recent one, so there is no good mechanism for storing installed versions in a .lock file. 102 | 103 | If your software needs specific versions then perhaps you'll want to look at using [Vagrant](https://vagrantup.com/) to better match your development and production environments. 104 | 105 | ## Contributors 106 | 107 | Over 10 different people have contributed to the project, you can see them all here: https://github.com/Homebrew/homebrew-bundle/graphs/contributors 108 | 109 | ## Development 110 | 111 | Source hosted at [GitHub](https://github.com/Homebrew/homebrew-bundle). 112 | Report Issues/Feature requests on [GitHub Issues](https://github.com/Homebrew/homebrew-bundle/issues). 113 | 114 | Tests can be ran with `bundle && bundle exec rake spec` 115 | 116 | ### Note on Patches/Pull Requests 117 | 118 | * Fork the project. 119 | * Make your feature addition or bug fix. 120 | * Add tests for it. This is important so I don't break it in a future version unintentionally. 121 | * Add documentation if necessary. 122 | * Commit, do not change Rakefile or history. 123 | * Send a pull request. Bonus points for topic branches. 124 | 125 | ## Copyright 126 | 127 | Copyright (c) 2015 Homebrew maintainers and Andrew Nesbitt. See [LICENSE](https://github.com/Homebrew/homebrew-bundle/blob/master/LICENSE) for details. 128 | -------------------------------------------------------------------------------- /spec/brew_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::BrewInstaller do 4 | let(:formula) { "mysql" } 5 | let(:options) { { :args => ["with-option"] } } 6 | let(:installer) { Bundle::BrewInstaller.new(formula, options) } 7 | 8 | def do_install 9 | installer.run 10 | end 11 | 12 | context "restart_service option is true" do 13 | context "formula is installed successfully" do 14 | before do 15 | allow(ARGV).to receive(:verbose?).and_return(false) 16 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:install_change_state!).and_return(true) 17 | end 18 | 19 | it "restart service" do 20 | expect(Bundle::BrewServices).to receive(:restart).with(formula).and_return(true) 21 | Bundle::BrewInstaller.install(formula, :restart_service => true) 22 | end 23 | end 24 | 25 | context "formula isn't installed" do 26 | before do 27 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:install_change_state!).and_return(false) 28 | end 29 | 30 | it "did not call restart service" do 31 | expect(Bundle::BrewServices).not_to receive(:restart) 32 | Bundle::BrewInstaller.install(formula, :restart_service => true) 33 | end 34 | end 35 | end 36 | 37 | context "conflicts_with option is provided" do 38 | before do 39 | expect(Bundle::BrewDumper).to receive(:formula_info).and_return( 40 | { :name => "mysql", :conflicts_with => ["mysql55"] }, 41 | ) 42 | allow(Bundle::BrewInstaller).to receive(:formula_installed?).and_return(true) 43 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:install).and_return(true) 44 | expect(Bundle).to receive(:system).with("brew", "unlink", "mysql55").and_return(true) 45 | expect(Bundle).to receive(:system).with("brew", "unlink", "mysql56").and_return(true) 46 | expect(Bundle::BrewServices).to receive(:stop).with("mysql55").and_return(true) 47 | expect(Bundle::BrewServices).to receive(:stop).with("mysql56").and_return(true) 48 | expect(Bundle::BrewServices).to receive(:restart).with(formula).and_return(true) 49 | end 50 | 51 | it "unlinks conflicts and stops their services" do 52 | allow(ARGV).to receive(:verbose?).and_return(false) 53 | Bundle::BrewInstaller.install(formula, :restart_service => true, :conflicts_with => ["mysql56"]) 54 | end 55 | 56 | it "prints a message" do 57 | allow(ARGV).to receive(:verbose?).and_return(true) 58 | allow_any_instance_of(String).to receive(:undent).and_return("") 59 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:puts) 60 | Bundle::BrewInstaller.install(formula, :restart_service => true, :conflicts_with => ["mysql56"]) 61 | end 62 | end 63 | 64 | context ".outdated_formulae" do 65 | it "calls Homebrew" do 66 | Bundle::BrewInstaller.reset! 67 | expect(Bundle::BrewDumper).to receive(:formulae).and_return([ 68 | { :name => "a", :outdated? => true }, 69 | { :name => "b", :outdated? => true }, 70 | { :name => "c", :outdated? => false }, 71 | ]) 72 | expect(Bundle::BrewInstaller.outdated_formulae).to eql(%w[a b]) 73 | end 74 | end 75 | 76 | context ".pinned_formulae" do 77 | it "calls Homebrew" do 78 | Bundle::BrewInstaller.reset! 79 | expect(Bundle::BrewDumper).to receive(:formulae).and_return([ 80 | { :name => "a", :pinned? => true }, 81 | { :name => "b", :pinned? => true }, 82 | { :name => "c", :pinned? => false }, 83 | ]) 84 | expect(Bundle::BrewInstaller.pinned_formulae).to eql(%w[a b]) 85 | end 86 | end 87 | 88 | context ".formula_installed_and_up_to_date?" do 89 | before do 90 | Bundle::BrewDumper.reset! 91 | allow(Bundle::BrewInstaller).to receive(:outdated_formulae).and_return(%w[bar]) 92 | allow(Bundle::BrewDumper).to receive(:formulae).and_return [ 93 | { 94 | :name => "foo", 95 | :full_name => "homebrew/tap/foo", 96 | :aliases => ["foobar"], 97 | :args => [], 98 | :version => "1.0", 99 | :dependencies => [], 100 | :requirements => [], 101 | }, 102 | { 103 | :name => "bar", 104 | :full_name => "bar", 105 | :aliases => [], 106 | :args => [], 107 | :version => "1.0", 108 | :dependencies => [], 109 | :requirements => [], 110 | }, 111 | ] 112 | end 113 | 114 | it "returns result" do 115 | expect(Bundle::BrewInstaller.formula_installed_and_up_to_date?("foo")).to eql(true) 116 | expect(Bundle::BrewInstaller.formula_installed_and_up_to_date?("foobar")).to eql(true) 117 | expect(Bundle::BrewInstaller.formula_installed_and_up_to_date?("bar")).to eql(false) 118 | expect(Bundle::BrewInstaller.formula_installed_and_up_to_date?("baz")).to eql(false) 119 | end 120 | end 121 | 122 | context "when brew is installed" do 123 | before do 124 | allow(ARGV).to receive(:verbose?).and_return(false) 125 | end 126 | 127 | context "when no formula is installed" do 128 | before do 129 | allow(Bundle::BrewInstaller).to receive(:installed_formulae).and_return([]) 130 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:conflicts_with).and_return([]) 131 | end 132 | 133 | it "install formula" do 134 | expect(Bundle).to receive(:system).with("brew", "install", formula, "--with-option").and_return(true) 135 | expect(do_install).to eql(true) 136 | end 137 | end 138 | 139 | context "when formula is installed" do 140 | before do 141 | allow(Bundle::BrewInstaller).to receive(:installed_formulae).and_return([formula]) 142 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:conflicts_with).and_return([]) 143 | end 144 | 145 | context "when formula upgradable" do 146 | before do 147 | allow(Bundle::BrewInstaller).to receive(:outdated_formulae).and_return([formula]) 148 | end 149 | 150 | it "upgrade formula" do 151 | expect(Bundle).to receive(:system).with("brew", "upgrade", formula).and_return(true) 152 | expect(do_install).to eql(true) 153 | end 154 | 155 | context "when formula pinned" do 156 | before do 157 | allow(Bundle::BrewInstaller).to receive(:pinned_formulae).and_return([formula]) 158 | end 159 | 160 | it "does not upgrade formula" do 161 | expect(Bundle).not_to receive(:system).with("brew", "upgrade", formula) 162 | expect(do_install).to eql(true) 163 | end 164 | end 165 | 166 | context "when formula not upgraded" do 167 | before do 168 | allow(Bundle::BrewInstaller).to receive(:outdated_formulae).and_return([]) 169 | end 170 | 171 | it "does not upgrade formula" do 172 | expect(Bundle).not_to receive(:system) 173 | expect(do_install).to eql(true) 174 | end 175 | end 176 | end 177 | end 178 | end 179 | 180 | context '#changed?' do 181 | it 'should be falsy by default' do 182 | expect(Bundle::BrewInstaller.new(formula).changed?).to eql(nil) 183 | end 184 | end 185 | 186 | context '#restart_service?' do 187 | it 'should be false by default' do 188 | expect(Bundle::BrewInstaller.new(formula).restart_service?).to eql(false) 189 | end 190 | 191 | context 'if a service is unchanged' do 192 | before do 193 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:changed?).and_return(false) 194 | end 195 | 196 | it 'should be true with {restart_service: true}' do 197 | expect(Bundle::BrewInstaller.new(formula, restart_service: true).restart_service?).to eql(true) 198 | end 199 | 200 | it 'should be false if {restart_service: :changed}' do 201 | expect(Bundle::BrewInstaller.new(formula, restart_service: :changed).restart_service?).to eql(false) 202 | end 203 | end 204 | 205 | context 'if a service is changed' do 206 | before do 207 | allow_any_instance_of(Bundle::BrewInstaller).to receive(:changed?).and_return(true) 208 | end 209 | 210 | it 'should be true with {restart_service: true}' do 211 | expect(Bundle::BrewInstaller.new(formula, restart_service: true).restart_service?).to eql(true) 212 | end 213 | 214 | it 'should be true if {restart_service: :changed}' do 215 | expect(Bundle::BrewInstaller.new(formula, restart_service: :changed).restart_service?).to eql(true) 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/brew_dumper_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Bundle::BrewDumper do 4 | context "when no formula is installed" do 5 | before do 6 | Bundle::BrewDumper.reset! 7 | end 8 | subject { Bundle::BrewDumper } 9 | 10 | it "returns empty list" do 11 | expect(subject.formulae).to be_empty 12 | end 13 | 14 | it "dumps as empty string" do 15 | expect(subject.dump).to eql("") 16 | end 17 | end 18 | 19 | context "when Homebrew returns JSON with a malformed linked_keg" do 20 | before do 21 | Bundle::BrewDumper.reset! 22 | allow(Formula).to receive(:[]).and_return(nil) 23 | allow(Formula).to receive(:installed).and_return( 24 | [{ 25 | "name" => "foo", 26 | "full_name" => "homebrew/tap/foo", 27 | "desc" => "", 28 | "homepage" => "", 29 | "oldname" => nil, 30 | "aliases" => [], 31 | "versions" => { "stable" => "1.0", "bottle" => false }, 32 | "revision" => 0, 33 | "installed" => [{ 34 | "version" => "1.0", 35 | "used_options" => [], 36 | "built_as_bottle" => nil, 37 | "poured_from_bottle" => true, 38 | }], 39 | "linked_keg" => "fish", 40 | "keg_only" => nil, 41 | "dependencies" => [], 42 | "conflicts_with" => [], 43 | "caveats" => nil, 44 | "requirements" => [], 45 | "options" => [], 46 | "bottle" => {}, 47 | }] 48 | ) 49 | end 50 | subject { Bundle::BrewDumper } 51 | 52 | it "returns no version" do 53 | expect(subject.formulae).to contain_exactly(*[ 54 | { 55 | :name => "foo", 56 | :full_name => "homebrew/tap/foo", 57 | :aliases => [], 58 | :args => [], 59 | :version => nil, 60 | :dependencies => [], 61 | :requirements => [], 62 | :conflicts_with => [], 63 | :pinned? => false, 64 | :outdated? => false, 65 | }, 66 | ]) 67 | end 68 | end 69 | 70 | context "formulae `foo` and `bar` are installed" do 71 | before do 72 | Bundle::BrewDumper.reset! 73 | allow(Formula).to receive(:[]).and_return( 74 | { 75 | "name" => "foo", 76 | "full_name" => "homebrew/tap/foo", 77 | "desc" => "", 78 | "homepage" => "", 79 | "oldname" => nil, 80 | "aliases" => [], 81 | "versions" => { "stable" => "1.0", "bottle" => false }, 82 | "revision" => 0, 83 | "installed" => [{ 84 | "version" => "1.0", 85 | "used_options" => [], 86 | "built_as_bottle" => nil, 87 | "poured_from_bottle" => true, 88 | }], 89 | "linked_keg" => "1.0", 90 | "keg_only" => nil, 91 | "dependencies" => [], 92 | "conflicts_with" => [], 93 | "caveats" => nil, 94 | "requirements" => [], 95 | "options" => [], 96 | "bottle" => {}, 97 | }) 98 | allow(Formula).to receive(:installed).and_return([ 99 | { 100 | "name" => "foo", 101 | "full_name" => "homebrew/tap/foo", 102 | "desc" => "", 103 | "homepage" => "", 104 | "oldname" => nil, 105 | "aliases" => [], 106 | "versions" => { "stable" => "1.0", "bottle" => false }, 107 | "revision" => 0, 108 | "installed" => [{ 109 | "version" => "1.0", 110 | "used_options" => [], 111 | "built_as_bottle" => nil, 112 | "poured_from_bottle" => true, 113 | }], 114 | "linked_keg" => "1.0", 115 | "keg_only" => nil, 116 | "dependencies" => [], 117 | "conflicts_with" => [], 118 | "caveats" => nil, 119 | "requirements" => [], 120 | "options" => [], 121 | "bottle" => {}, 122 | }, 123 | { 124 | "name" => "bar", 125 | "full_name" => "bar", 126 | "desc" => "", 127 | "homepage" => "", 128 | "oldname" => nil, 129 | "aliases" => [], 130 | "versions" => { "stable" => "2.1", "bottle" => false }, 131 | "revision" => 0, 132 | "installed" => [{ 133 | "version" => "2.0", 134 | "used_options" => ["--with-a", "--with-b"], 135 | "built_as_bottle" => nil, 136 | "poured_from_bottle" => true, 137 | }], 138 | "linked_keg" => nil, 139 | "keg_only" => nil, 140 | "dependencies" => [], 141 | "conflicts_with" => [], 142 | "caveats" => nil, 143 | "requirements" => [], 144 | "options" => [], 145 | "bottle" => {}, 146 | "pinned" => true, 147 | "outdated" => true, 148 | }]) 149 | end 150 | subject { Bundle::BrewDumper } 151 | 152 | it "returns foo and bar with their information" do 153 | expect(subject.formulae).to contain_exactly(*[ 154 | { 155 | :name => "foo", 156 | :full_name => "homebrew/tap/foo", 157 | :aliases => [], 158 | :args => [], 159 | :version => "1.0", 160 | :dependencies => [], 161 | :requirements => [], 162 | :conflicts_with => [], 163 | :pinned? => false, 164 | :outdated? => false, 165 | }, 166 | { 167 | :name => "bar", 168 | :full_name => "bar", 169 | :aliases => [], 170 | :args => ["with-a", "with-b"], 171 | :version => "2.0", 172 | :dependencies => [], 173 | :requirements => [], 174 | :conflicts_with => [], 175 | :pinned? => true, 176 | :outdated? => true, 177 | }, 178 | ]) 179 | end 180 | 181 | it "dumps as foo and bar with args" do 182 | expect(subject.dump).to eql("brew 'bar', args: ['with-a', 'with-b']\nbrew 'homebrew/tap/foo'") 183 | end 184 | 185 | it "formula_info returns the formula" do 186 | expect(subject.formula_info('foo')[:name]).to eql("foo") 187 | end 188 | end 189 | 190 | context "HEAD and devel formulae are installed" do 191 | before do 192 | Bundle::BrewDumper.reset! 193 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return [ 194 | { 195 | :name => "foo", 196 | :full_name => "foo", 197 | :aliases => [], 198 | :args => ["devel"], 199 | :version => "1.1beta", 200 | :dependencies => [], 201 | :requirements => [], 202 | :conflicts_with => [], 203 | :pinned? => false, 204 | :outdated? => false, 205 | }, 206 | { 207 | :name => "bar", 208 | :full_name => "homebrew/tap/bar", 209 | :aliases => [], 210 | :args => ["HEAD"], 211 | :version => "HEAD", 212 | :dependencies => [], 213 | :requirements => [], 214 | :conflicts_with => [], 215 | :pinned? => false, 216 | :outdated? => false, 217 | }, 218 | ] 219 | end 220 | subject { Bundle::BrewDumper.formulae } 221 | 222 | it "returns with args `devel` and `HEAD`" do 223 | expect(subject[0][:args]).to include("devel") 224 | expect(subject[1][:args]).to include("HEAD") 225 | end 226 | end 227 | 228 | context "A formula link to the old keg" do 229 | before do 230 | Bundle::BrewDumper.reset! 231 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return [ 232 | { 233 | :name => "foo", 234 | :full_name => "homebrew/tap/foo", 235 | :aliases => [], 236 | :args => [], 237 | :version => "1.0", 238 | :dependencies => [], 239 | :requirements => [], 240 | :conflicts_with => [], 241 | :pinned? => false, 242 | :outdated? => false, 243 | }, 244 | ] 245 | end 246 | subject { Bundle::BrewDumper.formulae } 247 | 248 | it "returns with linked keg" do 249 | expect(subject[0][:version]).to eql("1.0") 250 | end 251 | end 252 | 253 | context "A formula with no linked keg" do 254 | before do 255 | Bundle::BrewDumper.reset! 256 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return [ 257 | { 258 | :name => "foo", 259 | :full_name => "homebrew/tap/foo", 260 | :aliases => [], 261 | :args => [], 262 | :version => "2.0", 263 | :dependencies => [], 264 | :requirements => [], 265 | :conflicts_with => [], 266 | :pinned? => false, 267 | :outdated? => false, 268 | }, 269 | ] 270 | end 271 | subject { Bundle::BrewDumper.formulae } 272 | 273 | it "returns with last one" do 274 | expect(subject[0][:version]).to eql("2.0") 275 | end 276 | end 277 | 278 | context "several formulae with dependant relations" do 279 | before do 280 | Bundle::BrewDumper.reset! 281 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return [ 282 | { 283 | :name => "a", 284 | :full_name => "a", 285 | :aliases => [], 286 | :args => [], 287 | :version => "1.0", 288 | :dependencies => ["b"], 289 | :requirements => [], 290 | :conflicts_with => [], 291 | :pinned? => false, 292 | :outdated? => false, 293 | }, 294 | { 295 | :name => "b", 296 | :full_name => "b", 297 | :aliases => [], 298 | :args => [], 299 | :version => "1.0", 300 | :dependencies => [], 301 | :requirements => [{ "name" => "foo", "default_formula" => "c", "cask" => "bar" }], 302 | :conflicts_with => [], 303 | :pinned? => false, 304 | :outdated? => false, 305 | }, 306 | { 307 | :name => "c", 308 | :full_name => "homebrew/tap/c", 309 | :aliases => [], 310 | :args => [], 311 | :version => "1.0", 312 | :dependencies => [], 313 | :requirements => [], 314 | :conflicts_with => [], 315 | :pinned? => false, 316 | :outdated? => false, 317 | }, 318 | ] 319 | end 320 | subject { Bundle::BrewDumper } 321 | 322 | it "returns formulae with correct order" do 323 | expect(subject.formulae.map { |f| f[:name] }).to eq %w[c b a] 324 | end 325 | 326 | it "returns all the cask requirements" do 327 | expect(subject.cask_requirements).to eq %w[bar] 328 | end 329 | end 330 | 331 | context "formulae with unsorted dependencies" do 332 | before do 333 | Bundle::BrewDumper.reset! 334 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return [ 335 | { 336 | :name => "a", 337 | :full_name => "a", 338 | :aliases => [], 339 | :args => [], 340 | :version => "1.0", 341 | :dependencies => ["b", "d", "c"], 342 | :requirements => [], 343 | :conflicts_with => [], 344 | :pinned? => false, 345 | :outdated? => false, 346 | }, 347 | { 348 | :name => "b", 349 | :full_name => "b", 350 | :aliases => [], 351 | :args => [], 352 | :version => "1.0", 353 | :dependencies => [], 354 | :requirements => [], 355 | :conflicts_with => [], 356 | :pinned? => false, 357 | :outdated? => false, 358 | }, 359 | { 360 | :name => "c", 361 | :full_name => "c", 362 | :aliases => [], 363 | :args => [], 364 | :version => "1.0", 365 | :dependencies => [], 366 | :requirements => [], 367 | :conflicts_with => [], 368 | :pinned? => false, 369 | :outdated? => false, 370 | }, 371 | { 372 | :name => "d", 373 | :full_name => "d", 374 | :aliases => [], 375 | :args => [], 376 | :version => "1.0", 377 | :dependencies => [], 378 | :requirements => [], 379 | :conflicts_with => [], 380 | :pinned? => false, 381 | :outdated? => false, 382 | }, 383 | ] 384 | end 385 | subject { Bundle::BrewDumper } 386 | 387 | it "returns formulae with correct order" do 388 | expect(subject.formulae.map { |f| f[:name] }).to eq %w[b c d a] 389 | end 390 | end 391 | 392 | context "when order of args for a formula is different in different environment" do 393 | it "dumps args in same order" do 394 | formula_info = [ 395 | [{ 396 | :name => "a", 397 | :full_name => "a", 398 | :aliases => [], 399 | :args => ['with-1', 'with-2'], 400 | :version => "1.0", 401 | :dependencies => ["b"], 402 | :requirements => [], 403 | :conflicts_with => [], 404 | :pinned? => false, 405 | :outdated? => false, 406 | }], 407 | [{ 408 | :name => "a", 409 | :full_name => "a", 410 | :aliases => [], 411 | :args => ['with-2', 'with-1'], 412 | :version => "1.0", 413 | :dependencies => ["b"], 414 | :requirements => [], 415 | :conflicts_with => [], 416 | :pinned? => false, 417 | :outdated? => false, 418 | }] 419 | ] 420 | dump_lines = formula_info.map do |info| 421 | Bundle::BrewDumper.reset! 422 | allow(Bundle::BrewDumper).to receive(:formulae_info).and_return(info) 423 | Bundle::BrewDumper.dump 424 | end 425 | expect(dump_lines[0]).to eql(dump_lines[1]) 426 | end 427 | end 428 | end 429 | --------------------------------------------------------------------------------