diff options
author | Misty De Meo <mistydemeo@github.com> | 2016-07-05 15:15:28 +1000 |
---|---|---|
committer | Misty De Meo <mistydemeo@github.com> | 2016-08-02 11:21:16 +1000 |
commit | c1f6dd96f899b97ae1a3972cef7e453776df1dd6 (patch) | |
tree | 38fe09b2db9905128ed1ff66d0ea6bcb7c20b3d7 | |
parent | 7f82490b82680c1786bd0af7f014d7ddab07629f (diff) | |
download | bundler-c1f6dd96f899b97ae1a3972cef7e453776df1dd6.tar.gz |
Add command to diagnose common issues
This new command, doctor, checks for common problems.
Currently, it looks for broken dynamic library links in C
extensions.
It scans all of the specifications in the bundle for .bundle files in C
extensions. If any of the dynamic libraries linked against no longer
exist, bundler will report a message to the console.
-rw-r--r-- | lib/bundler/cli.rb | 18 | ||||
-rw-r--r-- | lib/bundler/cli/doctor.rb | 85 | ||||
-rw-r--r-- | spec/commands/doctor_spec.rb | 61 |
3 files changed, 164 insertions, 0 deletions
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 621deb0950..5e568995d0 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -474,6 +474,24 @@ module Bundler Inject.new(options, name, version, gems).run end + desc "doctor [OPTIONS]", "Checks the bundle for common problems" + long_desc <<-D + Doctor scans the OS dependencies of each of the gems requested in the Gemfile. If + missing dependencies are detected, Bundler prints them and exits status 1. + Otherwise, Bundler prints a success message and exits with a status of 0. + + The bundle's gem dependencies must all be installed to run this command; if + they are not, Bundler prints an error message and exits with a status of 2. + D + method_option "gemfile", :type => :string, :banner => + "Use the specified gemfile instead of Gemfile" + method_option "quiet", :type => :boolean, :banner => + "Only output warnings and errors." + def doctor + require "bundler/cli/doctor" + Doctor.new(options).run + end + if Bundler.settings[:plugins] require "bundler/cli/plugin" desc "plugin SUBCOMMAND ...ARGS", "manage the bundler plugins" diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb new file mode 100644 index 0000000000..370e46ac69 --- /dev/null +++ b/lib/bundler/cli/doctor.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Bundler + class CLI::Doctor + DARWIN_REGEX = /\s+(.+) \(compatibility / + LDD_REGEX = /\t\S+ => (\S+) \(\S+\)/ + + attr_reader :options + + def initialize(options) + @options = options + end + + def dylibs_darwin(path) + output = `/usr/bin/otool -L "#{path}"`.chomp + dylibs = output.split("\n")[1..-1].map {|l| l.match(DARWIN_REGEX).captures[0] }.uniq + # ignore @rpath and friends + dylibs.reject {|dylib| dylib.start_with? "@" } + end + + def dylibs_ldd(path) + output = `/usr/bin/ldd "#{path}"`.chomp + output.split("\n").map do |l| + match = l.match(LDD_REGEX) + next if match.nil? + match.captures[0] + end.compact + end + + def dylibs(path) + case RbConfig::CONFIG["host_os"] + when /darwin/ + dylibs_darwin(path) + when /(linux|solaris|bsd)/ + dylibs_ldd(path) + else # Windows, etc. + Bundler.ui.warn("Dynamic library check not supported on this platform.") + [] + end + end + + def bundles_for_gem(spec) + Dir.glob("#{spec.full_gem_path}/**/*.bundle") + end + + def run + Bundler.ui.level = "error" if options[:quiet] + + broken_links = {} + + begin + definition = Bundler.definition + definition.validate_ruby! + not_installed = definition.missing_specs + raise GemNotFound if not_installed.any? + rescue GemNotFound + Bundler.ui.error "This bundle's gems must be installed to run this command." + Bundler.ui.warn "Install missing gems with `bundle install`." + exit 2 + end + + definition.specs.each do |spec| + bundles_for_gem(spec).each do |bundle| + bad_paths = dylibs(bundle).select {|f| !File.exist?(f) } + if bad_paths.any? + broken_links[spec] ||= [] + broken_links[spec].concat(bad_paths) + end + end + end + + if broken_links.any? + Bundler.ui.error "The following gems are missing OS dependencies" + broken_links.each do |spec, paths| + paths.uniq.each do |path| + Bundler.ui.error " * #{spec.name}: #{path}" + end + end + exit 1 + end + end + end +end diff --git a/spec/commands/doctor_spec.rb b/spec/commands/doctor_spec.rb new file mode 100644 index 0000000000..236138a6c8 --- /dev/null +++ b/spec/commands/doctor_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true +require "spec_helper" +require "stringio" +require "bundler/cli" +require "bundler/cli/doctor" + +describe "bundle doctor" do + before(:each) do + @stdout = StringIO.new + + [:error, :warn].each do |method| + allow(Bundler.ui).to receive(method).and_wrap_original do |m, message| + m.call message + @stdout.puts message + end + end + end + + it "exits with no message if the installed gem has no C extensions" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + Bundler::CLI::Doctor.new({}).run + expect(@stdout.string).to be_empty + end + + it "exits with no message if the installed gem's C extension dylib breakage is fine" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/lib/libSystem.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/lib/libSystem.dylib").and_return(true) + doctor.run + expect(@stdout.string).to be_empty + end + + it "exits with a message if one of the linked libraries is missing" do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rack" + G + + bundle :install + doctor = Bundler::CLI::Doctor.new({}) + expect(doctor).to receive(:bundles_for_gem).exactly(2).times.and_return ["/path/to/rack/rack.bundle"] + expect(doctor).to receive(:dylibs).exactly(2).times.and_return ["/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib"] + allow(File).to receive(:exist?).and_call_original + allow(File).to receive(:exist?).with("/usr/local/opt/icu4c/lib/libicui18n.57.1.dylib").and_return(false) + expect { doctor.run }.to raise_error SystemExit + expect(@stdout.string).to include("libicui18n.57.1.dylib") + end +end |