diff options
-rw-r--r-- | lib/bundler/cli.rb | 14 | ||||
-rw-r--r-- | lib/bundler/cli/lock.rb | 36 | ||||
-rw-r--r-- | lib/bundler/dsl.rb | 116 | ||||
-rw-r--r-- | lib/bundler/friendly_errors.rb | 3 | ||||
-rw-r--r-- | man/bundle.ronn | 6 | ||||
-rw-r--r-- | spec/bundler/dsl_spec.rb | 9 | ||||
-rw-r--r-- | spec/commands/lock_spec.rb | 94 | ||||
-rw-r--r-- | spec/install/gems/simple_case_spec.rb | 2 |
8 files changed, 260 insertions, 20 deletions
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index 6a22072565..b1a19f5530 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -379,6 +379,20 @@ module Bundler Inject.new(options, name, version, gems).run end + desc "lock", "Creates a lockfile without installing" + method_option "update", :type => :boolean, :default => false, :banner => + "ignore the existing lockfile" + method_option "local", :type => :boolean, :default => false, :banner => + "do not attempt to fetch remote gemspecs and use the local gem cache only" + method_option "print", :type => :boolean, :default => false, :banner => + "print the lockfile to STDOUT instead of writing to the file system" + method_option "lockfile", :type => :string, :default => nil, :banner => + "the path the lockfile should be written to" + def lock + require 'bundler/cli/lock' + Lock.new(options).run + end + desc "env", "Print information about the environment Bundler is running under" def env Env.new.write($stdout) diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb new file mode 100644 index 0000000000..543deb5945 --- /dev/null +++ b/lib/bundler/cli/lock.rb @@ -0,0 +1,36 @@ +module Bundler + class CLI::Lock + attr_reader :options + + def initialize(options) + @options = options + end + + def run + unless Bundler.default_gemfile + Bundler.ui.error "Unable to find a Gemfile to lock" + exit 1 + end + + print = options[:print] + ui = Bundler.ui + Bundler.ui = UI::Silent.new if print + + unlock = options[:update] + definition = Bundler.definition(unlock) + definition.resolve_remotely! unless options[:local] + + if print + puts definition.to_lock + else + file = options[:lockfile] + file = file ? File.expand_path(file) : Bundler.default_lockfile + puts "Writing lockfile to #{file}" + definition.lock(file) + end + + Bundler.ui = ui + end + + end +end diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb index e6c4b975ef..f9f09e8b00 100644 --- a/lib/bundler/dsl.rb +++ b/lib/bundler/dsl.rb @@ -30,14 +30,9 @@ module Bundler def eval_gemfile(gemfile, contents = nil) contents ||= Bundler.read_file(gemfile.to_s) instance_eval(contents, gemfile.to_s, 1) - rescue SyntaxError => e - syntax_msg = e.message.gsub("#{gemfile}:", 'on line ') - raise GemfileError, "Gemfile syntax error #{syntax_msg}" - rescue ScriptError, RegexpError, NameError, ArgumentError, RuntimeError => e - e.backtrace[0] = "#{e.backtrace[0]}: #{e.message} (#{e.class})" - Bundler.ui.warn e.backtrace.join("\n ") - raise GemfileError, "There was an error in your Gemfile," \ - " and Bundler cannot continue." + rescue Exception => e + message = "There was an error parsing `#{File.basename gemfile.to_s}`: #{e.message}" + raise DSLError.new(message, gemfile, e.backtrace, contents) end def gemspec(opts = nil) @@ -184,9 +179,7 @@ module Bundler end def method_missing(name, *args) - location = caller[0].split(':')[0..1].join(':') - raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile\n" \ - " from #{location}" + raise GemfileError, "Undefined local variable or method `#{name}' for Gemfile" end private @@ -327,5 +320,106 @@ module Bundler end end + class DSLError < GemfileError + # @return [String] the description that should be presented to the user. + # + attr_reader :description + + # @return [String] the path of the dsl file that raised the exception. + # + attr_reader :dsl_path + + # @return [Exception] the backtrace of the exception raised by the + # evaluation of the dsl file. + # + attr_reader :backtrace + + # @param [Exception] backtrace @see backtrace + # @param [String] dsl_path @see dsl_path + # + def initialize(description, dsl_path, backtrace, contents = nil) + @status_code = $!.respond_to?(:status_code) && $!.status_code + + @description = description + @dsl_path = dsl_path + @backtrace = backtrace + @contents = contents + end + + def status_code + @status_code || super + end + + # @return [String] the contents of the DSL that cause the exception to + # be raised. + # + def contents + @contents ||= begin + dsl_path && File.exist?(dsl_path) && File.read(dsl_path) + end + end + + # The message of the exception reports the content of podspec for the + # line that generated the original exception. + # + # @example Output + # + # Invalid podspec at `RestKit.podspec` - undefined method + # `exclude_header_search_paths=' for #<Pod::Specification for + # `RestKit/Network (0.9.3)`> + # + # from spec-repos/master/RestKit/0.9.3/RestKit.podspec:36 + # ------------------------------------------- + # # because it would break: #import <CoreData/CoreData.h> + # > ns.exclude_header_search_paths = 'Code/RestKit.h' + # end + # ------------------------------------------- + # + # @return [String] the message of the exception. + # + def message + @message ||= begin + trace_line, description = parse_line_number_from_description + + m = "\n[!] " + m << description + m << ". Bundler cannot continue.\n" + + return m unless backtrace && dsl_path && contents + + trace_line = backtrace.find { |l| l.include?(dsl_path.to_s) } || trace_line + return m unless trace_line + line_numer = trace_line.split(':')[1].to_i - 1 + return m unless line_numer + + lines = contents.lines.to_a + indent = ' # ' + indicator = indent.gsub('#', '>') + first_line = (line_numer.zero?) + last_line = (line_numer == (lines.count - 1)) + + m << "\n" + m << "#{indent}from #{trace_line.gsub(/:in.*$/, '')}\n" + m << "#{indent}-------------------------------------------\n" + m << "#{indent}#{ lines[line_numer - 1] }" unless first_line + m << "#{indicator}#{ lines[line_numer] }" + m << "#{indent}#{ lines[line_numer + 1] }" unless last_line + m << "\n" unless m.end_with?("\n") + m << "#{indent}-------------------------------------------\n" + end + end + + private + + def parse_line_number_from_description + description = self.description + if dsl_path && description =~ /((#{Regexp.quote File.expand_path(dsl_path)}|#{Regexp.quote dsl_path.to_s}):\d+)/ + trace_line = Regexp.last_match[1] + description = description.sub(/#{Regexp.quote trace_line}:\s*/, '').sub("\n", ' - ') + end + [trace_line, description] + end + end + end end diff --git a/lib/bundler/friendly_errors.rb b/lib/bundler/friendly_errors.rb index 1431725d3d..4bd3e53381 100644 --- a/lib/bundler/friendly_errors.rb +++ b/lib/bundler/friendly_errors.rb @@ -5,6 +5,9 @@ require "bundler/vendored_thor" module Bundler def self.with_friendly_errors yield + rescue Bundler::Dsl::DSLError => e + Bundler.ui.error e.message + exit e.status_code rescue Bundler::BundlerError => e Bundler.ui.error e.message, :wrap => true Bundler.ui.trace e diff --git a/man/bundle.ronn b/man/bundle.ronn index 7b69c6735c..f7b9e94fd4 100644 --- a/man/bundle.ronn +++ b/man/bundle.ronn @@ -67,6 +67,9 @@ We divide `bundle` subcommands into primary commands and utilities. * `bundle open(1)`: Open an installed gem in the editor +* `bundle lock(1)`: + Generate a lockfile for your dependencies + * `bundle viz(1)`: Generate a visual representation of your dependencies @@ -92,7 +95,4 @@ and execute it, passing down any extra arguments to it. These commands are obsolete and should no longer be used -* `bundle lock(1)` -* `bundle unlock(1)` * `bundle cache(1)` - diff --git a/spec/bundler/dsl_spec.rb b/spec/bundler/dsl_spec.rb index c2dacedd08..5c6be9efca 100644 --- a/spec/bundler/dsl_spec.rb +++ b/spec/bundler/dsl_spec.rb @@ -69,8 +69,7 @@ describe Bundler::Dsl do expect(Bundler).to receive(:read_file).with("Gemfile"). and_return("unknown") - error_msg = "Undefined local variable or method `unknown'" \ - " for Gemfile\\s+from Gemfile:1" + error_msg = "There was an error parsing `Gemfile`: Undefined local variable or method `unknown' for Gemfile. Bundler cannot continue." expect { subject.eval_gemfile("Gemfile") }. to raise_error(Bundler::GemfileError, Regexp.new(error_msg)) end @@ -80,7 +79,7 @@ describe Bundler::Dsl do it "handles syntax errors with a useful message" do expect(Bundler).to receive(:read_file).with("Gemfile").and_return("}") expect { subject.eval_gemfile("Gemfile") }. - to raise_error(Bundler::GemfileError, /Gemfile syntax error/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: (syntax error, unexpected tSTRING_DEND|(compile error - )?syntax error, unexpected '}'). Bundler cannot continue./) end end @@ -177,7 +176,7 @@ describe Bundler::Dsl do it "will raise a Bundler::GemfileError" do gemfile "gem 'foo', :path => /unquoted/string/syntax/error" expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. - to raise_error(Bundler::GemfileError, /Gemfile syntax error/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`:( compile error -)? unknown regexp options - trg. Bundler cannot continue./) end end @@ -185,7 +184,7 @@ describe Bundler::Dsl do it "will raise a Bundler::GemfileError" do gemfile "s = 'foo'.freeze; s.strip!" expect { Bundler::Dsl.evaluate(bundled_app("Gemfile"), nil, true) }. - to raise_error(Bundler::GemfileError, /There was an error in your Gemfile/) + to raise_error(Bundler::GemfileError, /There was an error parsing `Gemfile`: can't modify frozen String. Bundler cannot continue./) end end end diff --git a/spec/commands/lock_spec.rb b/spec/commands/lock_spec.rb new file mode 100644 index 0000000000..b3f4f8cb63 --- /dev/null +++ b/spec/commands/lock_spec.rb @@ -0,0 +1,94 @@ +require "spec_helper" + +describe "bundle lock" do + def strip_lockfile(lockfile) + strip_whitespace(lockfile).sub(/\n\Z/, '') + end + + def read_lockfile(file = "Gemfile.lock") + strip_lockfile bundled_app(file).read + end + + before :each do + gemfile <<-G + source "file://#{gem_repo1}" + gem "rails" + gem "with_license" + gem "foo" + G + + @lockfile = strip_lockfile <<-L + GEM + remote: file:#{gem_repo1}/ + specs: + actionmailer (2.3.2) + activesupport (= 2.3.2) + actionpack (2.3.2) + activesupport (= 2.3.2) + activerecord (2.3.2) + activesupport (= 2.3.2) + activeresource (2.3.2) + activesupport (= 2.3.2) + activesupport (2.3.2) + foo (1.0) + rails (2.3.2) + actionmailer (= 2.3.2) + actionpack (= 2.3.2) + activerecord (= 2.3.2) + activeresource (= 2.3.2) + rake (= 10.0.2) + rake (10.0.2) + with_license (1.0) + + PLATFORMS + ruby + + DEPENDENCIES + foo + rails + with_license + L + end + + it "prints a lockfile when there is no existing lockfile with --print" do + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "prints a lockfile when there is an existing lockfile with --print" do + lockfile @lockfile + + bundle "lock --print" + + expect(out).to eq(@lockfile) + end + + it "writes a lockfile when there is no existing lockfile" do + bundle "lock" + + expect(read_lockfile).to eq(@lockfile) + end + + it "writes a lockfile when there is an outdated lockfile using --update" do + lockfile @lockfile.gsub('2.3.2', '2.3.1') + + bundle "lock --update" + + expect(read_lockfile).to eq(@lockfile) + end + + it "does not fetch remote specs when using the --local option" do + bundle "lock --update --local" + + expect(out).to include("in the gems available on this machine.") + end + + it "writes to a custom location using --lockfile" do + bundle "lock --lockfile=lock" + + expect(out).to match(/Writing lockfile to.+lock/) + expect(read_lockfile "lock").to eq(@lockfile) + expect { read_lockfile }.to raise_error + end +end diff --git a/spec/install/gems/simple_case_spec.rb b/spec/install/gems/simple_case_spec.rb index 759de0884f..0f05588c4d 100644 --- a/spec/install/gems/simple_case_spec.rb +++ b/spec/install/gems/simple_case_spec.rb @@ -17,7 +17,7 @@ describe "bundle install with gem sources" do G expect(err).to eq "" - expect(out).to match(/StandardError: FAIL/) + expect(out).to match(/StandardError, "FAIL"/) expect(bundled_app("Gemfile.lock")).not_to exist end |