diff options
author | Homu <homu@barosl.com> | 2016-08-04 14:53:12 +0900 |
---|---|---|
committer | Homu <homu@barosl.com> | 2016-08-04 14:53:12 +0900 |
commit | e511f9f4bcbcfc23e363a67c83b66386543cc02c (patch) | |
tree | f7076f3be3afd4e92fa2fd3ef8a0f092513620f4 | |
parent | dda6432b71043b8065080f2fb83358614c4d04e8 (diff) | |
parent | 6117de98bb696a68cc15963b40d9157925e175a2 (diff) | |
download | bundler-e511f9f4bcbcfc23e363a67c83b66386543cc02c.tar.gz |
Auto merge of #4765 - mistydemeo:linkage, r=indirect
WIP: add command to check dynamic library linkage
This new command, linkage, checks for broken dynamic library links in C extensions.
I'd heard there was some interest in adding this functionality, so I thought I'd submit a preliminary PR for discussion. In my experience, broken dylib linkage is a common issue with C extensions, so I think having a good way to diagnose it would be valuable.
This command 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 and exit non-0.
TODOs:
* Add support for non-Darwin OSs
* Improve tests
A few questions:
* Is there a good way to mock functionality in the tests? Doing it in the standard rspec way isn't working since `bundle :command` runs in a subprocess. I'd like to be able to stub out stuff that actually checks dylibs and the like.
* Is making this a new command the right approach? I assumed this wouldn't be ideal to include in, say, `check` because it would slow it down.
-rw-r--r-- | lib/bundler/cli.rb | 15 | ||||
-rw-r--r-- | lib/bundler/cli/doctor.rb | 95 | ||||
-rw-r--r-- | man/bundle.ronn | 3 | ||||
-rw-r--r-- | spec/commands/doctor_spec.rb | 61 |
4 files changed, 174 insertions, 0 deletions
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 621deb0950..a20c76b30f 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -474,6 +474,21 @@ 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. + 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..8fd862a1c2 --- /dev/null +++ b/lib/bundler/cli/doctor.rb @@ -0,0 +1,95 @@ +# 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 otool_available? + system("otool --version 2>&1 >#{Bundler::NULL}") + end + + def ldd_available? + !system("ldd --help 2>&1 >#{Bundler::NULL}").nil? + 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/ + return [] unless otool_available? + dylibs_darwin(path) + when /(linux|solaris|bsd)/ + return [] unless ldd_available? + 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.warn "This bundle's gems must be installed to run this command." + Bundler.ui.warn "Install missing gems with `bundle install`." + exit 0 + 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/man/bundle.ronn b/man/bundle.ronn index 55e2c4afe8..72faac048b 100644 --- a/man/bundle.ronn +++ b/man/bundle.ronn @@ -82,6 +82,9 @@ We divide `bundle` subcommands into primary commands and utilities. * `bundle clean(1)`: Clean up unused gems in your bundler directory +* `bundle doctor(1)`: + Display warnings about common potential problems + ## PLUGINS When running a command that isn't listed in PRIMARY COMMANDS or UTILITIES, 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 |