diff options
Diffstat (limited to 'lib/bundler/cli')
-rw-r--r-- | lib/bundler/cli/add.rb | 26 | ||||
-rw-r--r-- | lib/bundler/cli/binstubs.rb | 41 | ||||
-rw-r--r-- | lib/bundler/cli/cache.rb | 35 | ||||
-rw-r--r-- | lib/bundler/cli/check.rb | 40 | ||||
-rw-r--r-- | lib/bundler/cli/clean.rb | 26 | ||||
-rw-r--r-- | lib/bundler/cli/common.rb | 93 | ||||
-rw-r--r-- | lib/bundler/cli/config.rb | 118 | ||||
-rw-r--r-- | lib/bundler/cli/console.rb | 42 | ||||
-rw-r--r-- | lib/bundler/cli/doctor.rb | 93 | ||||
-rw-r--r-- | lib/bundler/cli/exec.rb | 104 | ||||
-rw-r--r-- | lib/bundler/cli/gem.rb | 248 | ||||
-rw-r--r-- | lib/bundler/cli/info.rb | 51 | ||||
-rw-r--r-- | lib/bundler/cli/init.rb | 35 | ||||
-rw-r--r-- | lib/bundler/cli/inject.rb | 59 | ||||
-rw-r--r-- | lib/bundler/cli/install.rb | 214 | ||||
-rw-r--r-- | lib/bundler/cli/issue.rb | 40 | ||||
-rw-r--r-- | lib/bundler/cli/lock.rb | 64 | ||||
-rw-r--r-- | lib/bundler/cli/open.rb | 26 | ||||
-rw-r--r-- | lib/bundler/cli/outdated.rb | 255 | ||||
-rw-r--r-- | lib/bundler/cli/package.rb | 46 | ||||
-rw-r--r-- | lib/bundler/cli/platform.rb | 45 | ||||
-rw-r--r-- | lib/bundler/cli/plugin.rb | 23 | ||||
-rw-r--r-- | lib/bundler/cli/pristine.rb | 36 | ||||
-rw-r--r-- | lib/bundler/cli/show.rb | 76 | ||||
-rw-r--r-- | lib/bundler/cli/update.rb | 63 | ||||
-rw-r--r-- | lib/bundler/cli/viz.rb | 30 |
26 files changed, 1929 insertions, 0 deletions
diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb new file mode 100644 index 0000000000..e80c775433 --- /dev/null +++ b/lib/bundler/cli/add.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Add + def initialize(options, gem_name) + @gem_name = gem_name + @options = options + @options[:group] = @options[:group].split(",").map(&:strip) if !@options[:group].nil? && !@options[:group].empty? + end + + def run + version = @options[:version].nil? ? nil : @options[:version].split(",").map(&:strip) + + unless version.nil? + version.each do |v| + raise InvalidOption, "Invalid gem requirement pattern '#{v}'" unless Gem::Requirement::PATTERN =~ v.to_s + end + end + dependency = Bundler::Dependency.new(@gem_name, version, @options) + + Injector.inject([dependency], :conservative_versioning => @options[:version].nil?) # Perform conservative versioning only when version is not specified + Installer.install(Bundler.root, Bundler.definition) + end + end +end diff --git a/lib/bundler/cli/binstubs.rb b/lib/bundler/cli/binstubs.rb new file mode 100644 index 0000000000..95103b7dd8 --- /dev/null +++ b/lib/bundler/cli/binstubs.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Binstubs + attr_reader :options, :gems + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + Bundler.definition.validate_runtime! + Bundler.settings[:bin] = options["path"] if options["path"] + Bundler.settings[:bin] = nil if options["path"] && options["path"].empty? + installer = Installer.new(Bundler.root, Bundler.definition) + + if gems.empty? + Bundler.ui.error "`bundle binstubs` needs at least one gem to run." + exit 1 + end + + gems.each do |gem_name| + spec = Bundler.definition.specs.find {|s| s.name == gem_name } + unless spec + raise GemNotFound, Bundler::CLI::Common.gem_not_found_message( + gem_name, Bundler.definition.specs + ) + end + + if spec.name == "bundler" + Bundler.ui.warn "Sorry, Bundler can only be run via Rubygems." + elsif options[:standalone] + installer.generate_standalone_bundler_executable_stubs(spec) + else + installer.generate_bundler_executable_stubs(spec, :force => options[:force], :binstubs_cmd => true) + end + end + end + end +end diff --git a/lib/bundler/cli/cache.rb b/lib/bundler/cli/cache.rb new file mode 100644 index 0000000000..5ba105a31d --- /dev/null +++ b/lib/bundler/cli/cache.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +module Bundler + class CLI::Cache + attr_reader :options + def initialize(options) + @options = options + end + + def run + Bundler.definition.validate_runtime! + Bundler.definition.resolve_with_cache! + setup_cache_all + Bundler.settings[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") + Bundler.load.cache + Bundler.settings[:no_prune] = true if options["no-prune"] + Bundler.load.lock + rescue GemNotFound => e + Bundler.ui.error(e.message) + Bundler.ui.warn "Run `bundle install` to install missing gems." + exit 1 + end + + private + + def setup_cache_all + Bundler.settings[:cache_all] = options[:all] if options.key?("all") + + if Bundler.definition.has_local_dependencies? && !Bundler.settings[:cache_all] + Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ + "to package them as well, please pass the --all flag. This will be the default " \ + "on Bundler 2.0." + end + end + end +end diff --git a/lib/bundler/cli/check.rb b/lib/bundler/cli/check.rb new file mode 100644 index 0000000000..057a7e5695 --- /dev/null +++ b/lib/bundler/cli/check.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +module Bundler + class CLI::Check + attr_reader :options + + def initialize(options) + @options = options + end + + def run + if options[:path] + Bundler.settings[:path] = File.expand_path(options[:path]) + Bundler.settings[:disable_shared_gems] = true + end + + begin + definition = Bundler.definition + definition.validate_runtime! + not_installed = definition.missing_specs + rescue GemNotFound, VersionConflict + Bundler.ui.error "Bundler can't satisfy your Gemfile's dependencies." + Bundler.ui.warn "Install missing gems with `bundle install`." + exit 1 + end + + if not_installed.any? + Bundler.ui.error "The following gems are missing" + not_installed.each {|s| Bundler.ui.error " * #{s.name} (#{s.version})" } + Bundler.ui.warn "Install missing gems with `bundle install`" + exit 1 + elsif !Bundler.default_lockfile.file? && Bundler.settings[:frozen] + Bundler.ui.error "This bundle has been frozen, but there is no #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} present" + exit 1 + else + Bundler.load.lock(:preserve_unknown_sections => true) unless options[:"dry-run"] + Bundler.ui.info "The Gemfile's dependencies are satisfied" + end + end + end +end diff --git a/lib/bundler/cli/clean.rb b/lib/bundler/cli/clean.rb new file mode 100644 index 0000000000..5eba09c6bc --- /dev/null +++ b/lib/bundler/cli/clean.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +module Bundler + class CLI::Clean + attr_reader :options + + def initialize(options) + @options = options + end + + def run + require_path_or_force unless options[:"dry-run"] + Bundler.load.clean(options[:"dry-run"]) + end + + protected + + def require_path_or_force + if !Bundler.settings[:path] && !options[:force] + Bundler.ui.error "Cleaning all the gems on your system is dangerous! " \ + "If you're sure you want to remove every system gem not in this " \ + "bundle, run `bundle clean --force`." + exit 1 + end + end + end +end diff --git a/lib/bundler/cli/common.rb b/lib/bundler/cli/common.rb new file mode 100644 index 0000000000..bacbb2edc5 --- /dev/null +++ b/lib/bundler/cli/common.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +module Bundler + module CLI::Common + def self.output_post_install_messages(messages) + return if Bundler.settings["ignore_messages"] + messages.to_a.each do |name, msg| + print_post_install_message(name, msg) unless Bundler.settings["ignore_messages.#{name}"] + end + end + + def self.print_post_install_message(name, msg) + Bundler.ui.confirm "Post-install message from #{name}:" + Bundler.ui.info msg + end + + def self.output_without_groups_message + return unless Bundler.settings.without.any? + Bundler.ui.confirm without_groups_message + end + + def self.without_groups_message + groups = Bundler.settings.without + group_list = [groups[0...-1].join(", "), groups[-1..-1]]. + reject {|s| s.to_s.empty? }.join(" and ") + group_str = (groups.size == 1) ? "group" : "groups" + "Gems in the #{group_str} #{group_list} were not installed." + end + + def self.select_spec(name, regex_match = nil) + specs = [] + regexp = Regexp.new(name) if regex_match + + Bundler.definition.specs.each do |spec| + return spec if spec.name == name + specs << spec if regexp && spec.name =~ regexp + end + + case specs.count + when 0 + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + when 1 + specs.first + else + ask_for_spec_from(specs) + end + rescue RegexpError + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + end + + def self.ask_for_spec_from(specs) + if !$stdout.tty? && ENV["BUNDLE_SPEC_RUN"].nil? + raise GemNotFound, gem_not_found_message(name, Bundler.definition.dependencies) + end + + specs.each_with_index do |spec, index| + Bundler.ui.info "#{index.succ} : #{spec.name}", true + end + Bundler.ui.info "0 : - exit -", true + + num = Bundler.ui.ask("> ").to_i + num > 0 ? specs[num - 1] : nil + end + + def self.gem_not_found_message(missing_gem_name, alternatives) + require "bundler/similarity_detector" + message = "Could not find gem '#{missing_gem_name}'." + alternate_names = alternatives.map {|a| a.respond_to?(:name) ? a.name : a } + suggestions = SimilarityDetector.new(alternate_names).similar_word_list(missing_gem_name) + message += "\nDid you mean #{suggestions}?" if suggestions + message + end + + def self.ensure_all_gems_in_lockfile!(names, locked_gems = Bundler.locked_gems) + locked_names = locked_gems.specs.map(&:name) + names.-(locked_names).each do |g| + raise GemNotFound, gem_not_found_message(g, locked_names) + end + end + + def self.configure_gem_version_promoter(definition, options) + patch_level = patch_level_options(options) + raise InvalidOption, "Provide only one of the following options: #{patch_level.join(", ")}" unless patch_level.length <= 1 + definition.gem_version_promoter.tap do |gvp| + gvp.level = patch_level.first || :major + gvp.strict = options[:strict] || options["update-strict"] + end + end + + def self.patch_level_options(options) + [:major, :minor, :patch].select {|v| options.keys.include?(v.to_s) } + end + end +end diff --git a/lib/bundler/cli/config.rb b/lib/bundler/cli/config.rb new file mode 100644 index 0000000000..e8f13620ec --- /dev/null +++ b/lib/bundler/cli/config.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true +module Bundler + class CLI::Config + attr_reader :name, :options, :scope, :thor + attr_accessor :args + + def initialize(options, args, thor) + @options = options + @args = args + @thor = thor + @name = peek = args.shift + @scope = "global" + return unless peek && peek.start_with?("--") + @name = args.shift + @scope = peek[2..-1] + end + + def run + unless name + confirm_all + return + end + + unless valid_scope?(scope) + Bundler.ui.error "Invalid scope --#{scope} given. Please use --local or --global." + exit 1 + end + + if scope == "delete" + Bundler.settings.set_local(name, nil) + Bundler.settings.set_global(name, nil) + return + end + + if args.empty? + if options[:parseable] + if value = Bundler.settings[name] + Bundler.ui.info("#{name}=#{value}") + end + return + end + + confirm(name) + return + end + + Bundler.ui.info(message) if message + Bundler.settings.send("set_#{scope}", name, new_value) + end + + private + + def confirm_all + if @options[:parseable] + thor.with_padding do + Bundler.settings.all.each do |setting| + val = Bundler.settings[setting] + Bundler.ui.info "#{setting}=#{val}" + end + end + else + Bundler.ui.confirm "Settings are listed in order of priority. The top value will be used.\n" + Bundler.settings.all.each do |setting| + Bundler.ui.confirm "#{setting}" + show_pretty_values_for(setting) + Bundler.ui.confirm "" + end + end + end + + def confirm(name) + Bundler.ui.confirm "Settings for `#{name}` in order of priority. The top value will be used" + show_pretty_values_for(name) + end + + def new_value + pathname = Pathname.new(args.join(" ")) + if name.start_with?("local.") && pathname.directory? + pathname.expand_path.to_s + else + args.join(" ") + end + end + + def message + locations = Bundler.settings.locations(name) + if @options[:parseable] + "#{name}=#{new_value}" if new_value + elsif scope == "global" + if locations[:local] + "Your application has set #{name} to #{locations[:local].inspect}. " \ + "This will override the global value you are currently setting" + elsif locations[:env] + "You have a bundler environment variable for #{name} set to " \ + "#{locations[:env].inspect}. This will take precedence over the global value you are setting" + elsif locations[:global] && locations[:global] != args.join(" ") + "You are replacing the current global value of #{name}, which is currently " \ + "#{locations[:global].inspect}" + end + elsif scope == "local" && locations[:local] != args.join(" ") + "You are replacing the current local value of #{name}, which is currently " \ + "#{locations[:local].inspect}" + end + end + + def show_pretty_values_for(setting) + thor.with_padding do + Bundler.settings.pretty_values_for(setting).each do |line| + Bundler.ui.info line + end + end + end + + def valid_scope?(scope) + %w(delete local global).include?(scope) + end + end +end diff --git a/lib/bundler/cli/console.rb b/lib/bundler/cli/console.rb new file mode 100644 index 0000000000..715abf2554 --- /dev/null +++ b/lib/bundler/cli/console.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +module Bundler + class CLI::Console + attr_reader :options, :group + def initialize(options, group) + @options = options + @group = group + end + + def run + Bundler::SharedHelpers.major_deprecation "bundle console will be replaced " \ + "by `bin/console` generated by `bundle gem <name>`" + + group ? Bundler.require(:default, *(group.split.map!(&:to_sym))) : Bundler.require + ARGV.clear + + console = get_console(Bundler.settings[:console] || "irb") + console.start + end + + def get_console(name) + require name + get_constant(name) + rescue LoadError + Bundler.ui.error "Couldn't load console #{name}, falling back to irb" + require "irb" + get_constant("irb") + end + + def get_constant(name) + const_name = { + "pry" => :Pry, + "ripl" => :Ripl, + "irb" => :IRB, + }[name] + Object.const_get(const_name) + rescue NameError + Bundler.ui.error "Could not find constant #{const_name}" + exit 1 + end + end +end diff --git a/lib/bundler/cli/doctor.rb b/lib/bundler/cli/doctor.rb new file mode 100644 index 0000000000..ae27983240 --- /dev/null +++ b/lib/bundler/cli/doctor.rb @@ -0,0 +1,93 @@ +# 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? + Bundler.which("otool") + end + + def ldd_available? + Bundler.which("ldd") + 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 check! + require "bundler/cli/check" + Bundler::CLI::Check.new({}).run + end + + def run + Bundler.ui.level = "error" if options[:quiet] + check! + + definition = Bundler.definition + broken_links = {} + + 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? + message = "The following gems are missing OS dependencies:" + broken_links.map do |spec, paths| + paths.uniq.map do |path| + "\n * #{spec.name}: #{path}" + end + end.flatten.sort.each {|m| message += m } + raise ProductionError, message + else + Bundler.ui.info "No issues found with the installed bundle" + end + end + end +end diff --git a/lib/bundler/cli/exec.rb b/lib/bundler/cli/exec.rb new file mode 100644 index 0000000000..62f7bc26cb --- /dev/null +++ b/lib/bundler/cli/exec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true +require "bundler/current_ruby" + +module Bundler + class CLI::Exec + attr_reader :options, :args, :cmd + + RESERVED_SIGNALS = %w(SEGV BUS ILL FPE VTALRM KILL STOP).freeze + + def initialize(options, args) + @options = options + @cmd = args.shift + @args = args + + if Bundler.current_ruby.ruby_2? && !Bundler.current_ruby.jruby? + @args << { :close_others => !options.keep_file_descriptors? } + elsif options.keep_file_descriptors? + Bundler.ui.warn "Ruby version #{RUBY_VERSION} defaults to keeping non-standard file descriptors on Kernel#exec." + end + end + + def run + validate_cmd! + SharedHelpers.set_bundle_environment + if bin_path = Bundler.which(cmd) + if !Bundler.settings[:disable_exec_load] && ruby_shebang?(bin_path) + return kernel_load(bin_path, *args) + end + # First, try to exec directly to something in PATH + if Bundler.current_ruby.jruby_18? + kernel_exec(bin_path, *args) + else + kernel_exec([bin_path, cmd], *args) + end + else + # exec using the given command + kernel_exec(cmd, *args) + end + end + + private + + def validate_cmd! + return unless cmd.nil? + Bundler.ui.error "bundler: exec needs a command to run" + exit 128 + end + + def kernel_exec(*args) + ui = Bundler.ui + Bundler.ui = nil + Kernel.exec(*args) + rescue Errno::EACCES, Errno::ENOEXEC + Bundler.ui = ui + Bundler.ui.error "bundler: not executable: #{cmd}" + exit 126 + rescue Errno::ENOENT + Bundler.ui = ui + Bundler.ui.error "bundler: command not found: #{cmd}" + Bundler.ui.warn "Install missing gem executables with `bundle install`" + exit 127 + end + + def kernel_load(file, *args) + args.pop if args.last.is_a?(Hash) + ARGV.replace(args) + $0 = file + Process.setproctitle(process_title(file, args)) if Process.respond_to?(:setproctitle) + ui = Bundler.ui + Bundler.ui = nil + require "bundler/setup" + signals = Signal.list.keys - RESERVED_SIGNALS + signals.each {|s| trap(s, "DEFAULT") } + Kernel.load(file) + rescue SystemExit + raise + rescue Exception => e # rubocop:disable Lint/RescueException + Bundler.ui = ui + Bundler.ui.error "bundler: failed to load command: #{cmd} (#{file})" + backtrace = e.backtrace.take_while {|bt| !bt.start_with?(__FILE__) } + abort "#{e.class}: #{e.message}\n #{backtrace.join("\n ")}" + end + + def process_title(file, args) + "#{file} #{args.join(" ")}".strip + end + + def ruby_shebang?(file) + possibilities = [ + "#!/usr/bin/env ruby\n", + "#!/usr/bin/env jruby\n", + "#!#{Gem.ruby}\n", + ] + + if File.zero?(file) + Bundler.ui.warn "#{file} is empty" + return false + end + + first_line = File.open(file, "rb") {|f| f.read(possibilities.map(&:size).max) } + possibilities.any? {|shebang| first_line.start_with?(shebang) } + end + end +end diff --git a/lib/bundler/cli/gem.rb b/lib/bundler/cli/gem.rb new file mode 100644 index 0000000000..45782d71a3 --- /dev/null +++ b/lib/bundler/cli/gem.rb @@ -0,0 +1,248 @@ +# frozen_string_literal: true +require "pathname" + +module Bundler + class CLI + Bundler.require_thor_actions + include Thor::Actions + end + + class CLI::Gem + TEST_FRAMEWORK_VERSIONS = { + "rspec" => "3.0", + "minitest" => "5.0" + }.freeze + + attr_reader :options, :gem_name, :thor, :name, :target + + def initialize(options, gem_name, thor) + @options = options + @gem_name = resolve_name(gem_name) + + @thor = thor + thor.behavior = :invoke + thor.destination_root = nil + + @name = @gem_name + @target = SharedHelpers.pwd.join(gem_name) + + validate_ext_name if options[:ext] + end + + def run + Bundler.ui.confirm "Creating gem '#{name}'..." + + underscored_name = name.tr("-", "_") + namespaced_path = name.tr("-", "/") + constant_name = name.gsub(/-[_-]*(?![_-]|$)/) { "::" }.gsub(/([_-]+|(::)|^)(.|$)/) { $2.to_s + $3.upcase } + constant_array = constant_name.split("::") + + git_installed = Bundler.git_present? + + git_author_name = git_installed ? `git config user.name`.chomp : "" + github_username = git_installed ? `git config github.user`.chomp : "" + git_user_email = git_installed ? `git config user.email`.chomp : "" + + config = { + :name => name, + :underscored_name => underscored_name, + :namespaced_path => namespaced_path, + :makefile_path => "#{underscored_name}/#{underscored_name}", + :constant_name => constant_name, + :constant_array => constant_array, + :author => git_author_name.empty? ? "TODO: Write your name" : git_author_name, + :email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email, + :test => options[:test], + :ext => options[:ext], + :exe => options[:exe], + :bundler_version => bundler_dependency_version, + :github_username => github_username.empty? ? "[USERNAME]" : github_username + } + ensure_safe_gem_name(name, constant_array) + + templates = { + "Gemfile.tt" => "Gemfile", + "lib/newgem.rb.tt" => "lib/#{namespaced_path}.rb", + "lib/newgem/version.rb.tt" => "lib/#{namespaced_path}/version.rb", + "newgem.gemspec.tt" => "#{name}.gemspec", + "Rakefile.tt" => "Rakefile", + "README.md.tt" => "README.md", + "bin/console.tt" => "bin/console", + "bin/setup.tt" => "bin/setup" + } + + executables = %w( + bin/console + bin/setup + ) + + templates.merge!("gitignore.tt" => ".gitignore") if Bundler.git_present? + + if test_framework = ask_and_set_test_framework + config[:test] = test_framework + config[:test_framework_version] = TEST_FRAMEWORK_VERSIONS[test_framework] + + templates.merge!(".travis.yml.tt" => ".travis.yml") + + case test_framework + when "rspec" + templates.merge!( + "rspec.tt" => ".rspec", + "spec/spec_helper.rb.tt" => "spec/spec_helper.rb", + "spec/newgem_spec.rb.tt" => "spec/#{namespaced_path}_spec.rb" + ) + when "minitest" + templates.merge!( + "test/test_helper.rb.tt" => "test/test_helper.rb", + "test/newgem_test.rb.tt" => "test/#{namespaced_path}_test.rb" + ) + end + end + + config[:test_task] = config[:test] == "minitest" ? "test" : "spec" + + if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?", + "This means that any other developer or company will be legally allowed to use your code " \ + "for free as long as they admit you created it. You can read more about the MIT license " \ + "at http://choosealicense.com/licenses/mit.") + config[:mit] = true + Bundler.ui.info "MIT License enabled in config" + templates.merge!("LICENSE.txt.tt" => "LICENSE.txt") + end + + if ask_and_set(:coc, "Do you want to include a code of conduct in gems you generate?", + "Codes of conduct can increase contributions to your project by contributors who " \ + "prefer collaborative, safe spaces. You can read more about the code of conduct at " \ + "contributor-covenant.org. Having a code of conduct means agreeing to the responsibility " \ + "of enforcing it, so be sure that you are prepared to do that. Be sure that your email " \ + "address is specified as a contact in the generated code of conduct so that people know " \ + "who to contact in case of a violation. For suggestions about " \ + "how to enforce codes of conduct, see http://bit.ly/coc-enforcement.") + config[:coc] = true + Bundler.ui.info "Code of conduct enabled in config" + templates.merge!("CODE_OF_CONDUCT.md.tt" => "CODE_OF_CONDUCT.md") + end + + templates.merge!("exe/newgem.tt" => "exe/#{name}") if config[:exe] + + if options[:ext] + templates.merge!( + "ext/newgem/extconf.rb.tt" => "ext/#{name}/extconf.rb", + "ext/newgem/newgem.h.tt" => "ext/#{name}/#{underscored_name}.h", + "ext/newgem/newgem.c.tt" => "ext/#{name}/#{underscored_name}.c" + ) + end + + templates.each do |src, dst| + destination = target.join(dst) + SharedHelpers.filesystem_access(destination) do + thor.template("newgem/#{src}", destination, config) + end + end + + executables.each do |file| + SharedHelpers.filesystem_access(target.join(file)) do |path| + executable = (path.stat.mode | 0o111) + path.chmod(executable) + end + end + + if Bundler.git_present? + Bundler.ui.info "Initializing git repo in #{target}" + Dir.chdir(target) do + `git init` + `git add .` + end + end + + # Open gemspec in editor + open_editor(options["edit"], target.join("#{name}.gemspec")) if options[:edit] + rescue Errno::EEXIST => e + raise GenericSystemCallError.new(e, "There was a conflict while creating the new gem.") + end + + private + + def resolve_name(name) + SharedHelpers.pwd.join(name).basename.to_s + end + + def ask_and_set(key, header, message) + choice = options[key] + choice = Bundler.settings["gem.#{key}"] if choice.nil? + + if choice.nil? + Bundler.ui.confirm header + choice = Bundler.ui.yes? "#{message} y/(n):" + Bundler.settings.set_global("gem.#{key}", choice) + end + + choice + end + + def validate_ext_name + return unless gem_name.index("-") + + Bundler.ui.error "You have specified a gem name which does not conform to the \n" \ + "naming guidelines for C extensions. For more information, \n" \ + "see the 'Extension Naming' section at the following URL:\n" \ + "http://guides.rubygems.org/gems-with-extensions/\n" + exit 1 + end + + def ask_and_set_test_framework + test_framework = options[:test] || Bundler.settings["gem.test"] + + if test_framework.nil? + Bundler.ui.confirm "Do you want to generate tests with your gem?" + result = Bundler.ui.ask "Type 'rspec' or 'minitest' to generate those test files now and " \ + "in the future. rspec/minitest/(none):" + if result =~ /rspec|minitest/ + test_framework = result + else + test_framework = false + end + end + + if Bundler.settings["gem.test"].nil? + Bundler.settings.set_global("gem.test", test_framework) + end + + test_framework + end + + def bundler_dependency_version + v = Gem::Version.new(Bundler::VERSION) + req = v.segments[0..1] + req << "a" if v.prerelease? + req.join(".") + end + + def ensure_safe_gem_name(name, constant_array) + if name =~ /^\d/ + Bundler.ui.error "Invalid gem name #{name} Please give a name which does not start with numbers." + exit 1 + end + + constant_name = constant_array.join("::") + + existing_constant = constant_array.inject(Object) do |c, s| + defined = begin + c.const_defined?(s) + rescue NameError + Bundler.ui.error "Invalid gem name #{name} -- `#{constant_name}` is an invalid constant name" + exit 1 + end + (defined && c.const_get(s)) || break + end + + return unless existing_constant + Bundler.ui.error "Invalid gem name #{name} constant #{constant_name} is already in use. Please choose another gem name." + exit 1 + end + + def open_editor(editor, file) + thor.run(%(#{editor} "#{file}")) + end + end +end diff --git a/lib/bundler/cli/info.rb b/lib/bundler/cli/info.rb new file mode 100644 index 0000000000..4465fba9d4 --- /dev/null +++ b/lib/bundler/cli/info.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Info + attr_reader :gem_name, :options + def initialize(options, gem_name) + @options = options + @gem_name = gem_name + end + + def run + spec = spec_for_gem(gem_name) + + spec_not_found(gem_name) unless spec + return print_gem_path(spec) if @options[:path] + print_gem_info(spec) + end + + private + + def spec_for_gem(gem_name) + spec = Bundler.definition.specs.find {|s| s.name == gem_name } + spec || default_gem_spec(gem_name) + end + + def default_gem_spec(gem_name) + return unless Gem::Specification.respond_to?(:find_all_by_name) + gem_spec = Gem::Specification.find_all_by_name(gem_name).last + return gem_spec if gem_spec && gem_spec.respond_to?(:default_gem?) && gem_spec.default_gem? + end + + def spec_not_found(gem_name) + raise GemNotFound, Bundler::CLI::Common.gem_not_found_message(gem_name, Bundler.definition.dependencies) + end + + def print_gem_path(spec) + Bundler.ui.info spec.full_gem_path + end + + def print_gem_info(spec) + gem_info = String.new + gem_info << " * #{spec.name} (#{spec.version}#{spec.git_version})\n" + gem_info << "\tSummary: #{spec.summary}\n" if spec.summary + gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage + gem_info << "\tPath: #{spec.full_gem_path}\n" + gem_info << "\tDefault Gem: yes" if spec.respond_to?(:default_gem?) && spec.default_gem? + Bundler.ui.info gem_info + end + end +end diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb new file mode 100644 index 0000000000..8ffd1db41a --- /dev/null +++ b/lib/bundler/cli/init.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +module Bundler + class CLI::Init + attr_reader :options + def initialize(options) + @options = options + end + + def run + if File.exist?("Gemfile") + Bundler.ui.error "Gemfile already exists at #{SharedHelpers.pwd}/Gemfile" + exit 1 + end + + if options[:gemspec] + gemspec = File.expand_path(options[:gemspec]) + unless File.exist?(gemspec) + Bundler.ui.error "Gem specification #{gemspec} doesn't exist" + exit 1 + end + + spec = Bundler.load_gemspec_uncached(gemspec) + + puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" + File.open("Gemfile", "wb") do |file| + file << "# Generated from #{gemspec}\n" + file << spec.to_gemfile + end + else + puts "Writing new Gemfile to #{SharedHelpers.pwd}/Gemfile" + FileUtils.cp(File.expand_path("../../templates/Gemfile", __FILE__), "Gemfile") + end + end + end +end diff --git a/lib/bundler/cli/inject.rb b/lib/bundler/cli/inject.rb new file mode 100644 index 0000000000..b17292643f --- /dev/null +++ b/lib/bundler/cli/inject.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true +module Bundler + class CLI::Inject + attr_reader :options, :name, :version, :group, :source, :gems + def initialize(options, name, version) + @options = options + @name = name + @version = version || last_version_number + @group = options[:group].split(",") unless options[:group].nil? + @source = options[:source] + @gems = [] + end + + def run + # The required arguments allow Thor to give useful feedback when the arguments + # are incorrect. This adds those first two arguments onto the list as a whole. + gems.unshift(source).unshift(group).unshift(version).unshift(name) + + # Build an array of Dependency objects out of the arguments + deps = [] + # when `inject` support addition of more than one gem, then this loop will + # help. Currently this loop is running once. + gems.each_slice(4) do |gem_name, gem_version, gem_group, gem_source| + ops = Gem::Requirement::OPS.map {|key, _val| key } + has_op = ops.any? {|op| gem_version.start_with? op } + gem_version = "~> #{gem_version}" unless has_op + deps << Bundler::Dependency.new(gem_name, gem_version, "group" => gem_group, "source" => gem_source) + end + + added = Injector.inject(deps, options) + + if added.any? + Bundler.ui.confirm "Added to Gemfile:" + Bundler.ui.confirm(added.map do |d| + name = "'#{d.name}'" + requirement = ", '#{d.requirement}'" + group = ", :group => #{d.groups.inspect}" if d.groups != Array(:default) + source = ", :source => '#{d.source}'" unless d.source.nil? + %(gem #{name}#{requirement}#{group}#{source}) + end.join("\n")) + else + Bundler.ui.confirm "All gems were already present in the Gemfile" + end + end + + private + + def last_version_number + definition = Bundler.definition(true) + definition.resolve_remotely! + specs = definition.index[name].sort_by(&:version) + unless options[:pre] + specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + spec = specs.last + spec.version.to_s + end + end +end diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb new file mode 100644 index 0000000000..ff6bedd9fd --- /dev/null +++ b/lib/bundler/cli/install.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Install + attr_reader :options + def initialize(options) + @options = options + end + + def run + Bundler.ui.level = "error" if options[:quiet] + + warn_if_root + + [:with, :without].each do |option| + if options[option] + options[option] = options[option].join(":").tr(" ", ":").split(":") + end + end + + check_for_group_conflicts + + normalize_groups + + ENV["RB_USER_INSTALL"] = "1" if Bundler::FREEBSD + + # Disable color in deployment mode + Bundler.ui.shell = Thor::Shell::Basic.new if options[:deployment] + + check_for_options_conflicts + + check_trust_policy + + if options[:deployment] || options[:frozen] + unless Bundler.default_lockfile.exist? + flag = options[:deployment] ? "--deployment" : "--frozen" + raise ProductionError, "The #{flag} flag requires a #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)}. Please make " \ + "sure you have checked your #{Bundler.default_lockfile.relative_path_from(SharedHelpers.pwd)} into version control " \ + "before deploying." + end + + options[:local] = true if Bundler.app_cache.exist? + + Bundler.settings[:frozen] = "1" + end + + # When install is called with --no-deployment, disable deployment mode + if options[:deployment] == false + Bundler.settings.delete(:frozen) + options[:system] = true + end + + normalize_settings + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + if options["binstubs"] + Bundler::SharedHelpers.major_deprecation \ + "The --binstubs option will be removed in favor of `bundle binstubs`" + end + + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + + definition = Bundler.definition + definition.validate_runtime! + + installer = Installer.install(Bundler.root, definition, options) + Bundler.load.cache if Bundler.app_cache.exist? && !options["no-cache"] && !Bundler.settings[:frozen] + + Bundler.ui.confirm "Bundle complete! #{dependencies_count_for(definition)}, #{gems_installed_for(definition)}." + Bundler::CLI::Common.output_without_groups_message + + if Bundler.settings[:path] + absolute_path = File.expand_path(Bundler.settings[:path]) + relative_path = absolute_path.sub(File.expand_path(".") + File::SEPARATOR, "." + File::SEPARATOR) + Bundler.ui.confirm "Bundled gems are installed into #{relative_path}." + else + Bundler.ui.confirm "Use `bundle info [gemname]` to see where a bundled gem is installed." + end + + Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + + warn_ambiguous_gems + + if Bundler.settings[:clean] && Bundler.settings[:path] + require "bundler/cli/clean" + Bundler::CLI::Clean.new(options).run + end + rescue GemNotFound, VersionConflict => e + if options[:local] && Bundler.app_cache.exist? + Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory." + end + + unless Bundler.definition.has_rubygems_remotes? + Bundler.ui.warn <<-WARN, :wrap => true + Your Gemfile has no gem server sources. If you need gems that are \ + not already on your machine, add a line like this to your Gemfile: + source 'https://rubygems.org' + WARN + end + raise e + rescue Gem::InvalidSpecificationException => e + Bundler.ui.warn "You have one or more invalid gemspecs that need to be fixed." + raise e + end + + private + + def warn_if_root + return if Bundler.settings[:silence_root_warning] || Bundler::WINDOWS || !Process.uid.zero? + Bundler.ui.warn "Don't run Bundler as root. Bundler can ask for sudo " \ + "if it is needed, and installing your bundle as root will break this " \ + "application for all non-root users on this machine.", :wrap => true + end + + def dependencies_count_for(definition) + count = definition.dependencies.count + "#{count} Gemfile #{count == 1 ? "dependency" : "dependencies"}" + end + + def gems_installed_for(definition) + count = definition.specs.count + "#{count} #{count == 1 ? "gem" : "gems"} now installed" + end + + def check_for_group_conflicts + if options[:without] && options[:with] + conflicting_groups = options[:without] & options[:with] + unless conflicting_groups.empty? + Bundler.ui.error "You can't list a group in both, --with and --without." \ + " The offending groups are: #{conflicting_groups.join(", ")}." + exit 1 + end + end + end + + def check_for_options_conflicts + if (options[:path] || options[:deployment]) && options[:system] + error_message = String.new + error_message << "You have specified both --path as well as --system. Please choose only one option.\n" if options[:path] + error_message << "You have specified both --deployment as well as --system. Please choose only one option.\n" if options[:deployment] + raise InvalidOption.new(error_message) + end + end + + def check_trust_policy + if options["trust-policy"] + unless Bundler.rubygems.security_policies.keys.include?(options["trust-policy"]) + Bundler.ui.error "Rubygems doesn't know about trust policy '#{options["trust-policy"]}'. " \ + "The known policies are: #{Bundler.rubygems.security_policies.keys.join(", ")}." + exit 1 + end + Bundler.settings["trust-policy"] = options["trust-policy"] + else + Bundler.settings["trust-policy"] = nil if Bundler.settings["trust-policy"] + end + end + + def normalize_groups + Bundler.settings.with = [] if options[:with] && options[:with].empty? + Bundler.settings.without = [] if options[:without] && options[:without].empty? + + with = options.fetch("with", []) + with |= Bundler.settings.with.map(&:to_s) + with -= options[:without] if options[:without] + + without = options.fetch("without", []) + without |= Bundler.settings.without.map(&:to_s) + without -= options[:with] if options[:with] + + options[:with] = with + options[:without] = without + end + + def normalize_settings + Bundler.settings[:path] = nil if options[:system] + Bundler.settings[:path] = "vendor/bundle" if options[:deployment] + Bundler.settings[:path] = options["path"] if options["path"] + Bundler.settings[:path] ||= "bundle" if options["standalone"] + + Bundler.settings[:bin] = options["binstubs"] if options["binstubs"] + Bundler.settings[:bin] = nil if options["binstubs"] && options["binstubs"].empty? + + Bundler.settings[:shebang] = options["shebang"] if options["shebang"] + + Bundler.settings[:jobs] = options["jobs"] if options["jobs"] + + Bundler.settings[:no_prune] = true if options["no-prune"] + + Bundler.settings[:no_install] = true if options["no-install"] + + Bundler.settings[:clean] = options["clean"] if options["clean"] + + Bundler.settings.without = options[:without] + Bundler.settings.with = options[:with] + + Bundler.settings[:disable_shared_gems] = Bundler.settings[:path] ? true : nil + end + + def warn_ambiguous_gems + Installer.ambiguous_gems.to_a.each do |name, installed_from_uri, *also_found_in_uris| + Bundler.ui.error "Warning: the gem '#{name}' was found in multiple sources." + Bundler.ui.error "Installed from: #{installed_from_uri}" + Bundler.ui.error "Also found in:" + also_found_in_uris.each {|uri| Bundler.ui.error " * #{uri}" } + Bundler.ui.error "You should add a source requirement to restrict this gem to your preferred source." + Bundler.ui.error "For example:" + Bundler.ui.error " gem '#{name}', :source => '#{installed_from_uri}'" + Bundler.ui.error "Then uninstall the gem '#{name}' (or delete all bundled gems) and then install again." + end + end + end +end diff --git a/lib/bundler/cli/issue.rb b/lib/bundler/cli/issue.rb new file mode 100644 index 0000000000..ace0f985a9 --- /dev/null +++ b/lib/bundler/cli/issue.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Bundler + class CLI::Issue + def run + Bundler.ui.info <<-EOS.gsub(/^ {8}/, "") + Did you find an issue with Bundler? Before filing a new issue, + be sure to check out these resources: + + 1. Check out our troubleshooting guide for quick fixes to common issues: + https://github.com/bundler/bundler/blob/master/doc/TROUBLESHOOTING.md + + 2. Instructions for common Bundler uses can be found on the documentation + site: http://bundler.io/ + + 3. Information about each Bundler command can be found in the Bundler + man pages: http://bundler.io/man/bundle.1.html + + Hopefully the troubleshooting steps above resolved your problem! If things + still aren't working the way you expect them to, please let us know so + that we can diagnose and help fix the problem you're having. Please + view the Filing Issues guide for more information: + https://github.com/bundler/bundler/blob/master/doc/contributing/ISSUES.md + + EOS + + Bundler.ui.info Bundler::Env.new.report + + Bundler.ui.info "\n## Bundle Doctor" + doctor + end + + def doctor + require "bundler/cli/doctor" + Bundler::CLI::Doctor.new({}).run + end + end +end diff --git a/lib/bundler/cli/lock.rb b/lib/bundler/cli/lock.rb new file mode 100644 index 0000000000..223db9419f --- /dev/null +++ b/lib/bundler/cli/lock.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +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 + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + update = options[:update] + if update.is_a?(Array) # unlocking specific gems + Bundler::CLI::Common.ensure_all_gems_in_lockfile!(update) + update = { :gems => update, :lock_shared_dependencies => options[:conservative] } + end + definition = Bundler.definition(update) + + Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) if options[:update] + + options["remove-platform"].each do |platform| + definition.remove_platform(platform) + end + + options["add-platform"].each do |platform_string| + platform = Gem::Platform.new(platform_string) + if platform.to_s == "unknown" + Bundler.ui.warn "The platform `#{platform_string}` is unknown to RubyGems " \ + "and adding it will likely lead to resolution errors" + end + definition.add_platform(platform) + end + + if definition.platforms.empty? + raise InvalidOption, "Removing all platforms from the bundle is not allowed" + end + + 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/cli/open.rb b/lib/bundler/cli/open.rb new file mode 100644 index 0000000000..9a21f6811c --- /dev/null +++ b/lib/bundler/cli/open.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true +require "bundler/cli/common" +require "shellwords" + +module Bundler + class CLI::Open + attr_reader :options, :name + def initialize(options, name) + @options = options + @name = name + end + + def run + editor = [ENV["BUNDLER_EDITOR"], ENV["VISUAL"], ENV["EDITOR"]].find {|e| !e.nil? && !e.empty? } + return Bundler.ui.info("To open a bundled gem, set $EDITOR or $BUNDLER_EDITOR") unless editor + return unless spec = Bundler::CLI::Common.select_spec(name, :regex_match) + path = spec.full_gem_path + Dir.chdir(path) do + command = Shellwords.split(editor) + [path] + Bundler.with_clean_env do + system(*command) + end || Bundler.ui.info("Could not run '#{command.join(" ")}'") + end + end + end +end diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb new file mode 100644 index 0000000000..863d0dd388 --- /dev/null +++ b/lib/bundler/cli/outdated.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Outdated + attr_reader :options, :gems + + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + check_for_deployment_mode + + sources = Array(options[:source]) + + gems.each do |gem_name| + Bundler::CLI::Common.select_spec(gem_name) + end + + Bundler.definition.validate_runtime! + current_specs = Bundler.ui.silence { Bundler.definition.resolve } + current_dependencies = {} + Bundler.ui.silence do + Bundler.load.dependencies.each do |dep| + current_dependencies[dep.name] = dep + end + end + + definition = if gems.empty? && sources.empty? + # We're doing a full update + Bundler.definition(true) + else + Bundler.definition(:gems => gems, :sources => sources) + end + + Bundler::CLI::Common.configure_gem_version_promoter( + Bundler.definition, + options + ) + + # the patch level options imply strict is also true. It wouldn't make + # sense otherwise. + strict = options[:strict] || + Bundler::CLI::Common.patch_level_options(options).any? + + filter_options_patch = options.keys & + %w(filter-major filter-minor filter-patch) + + definition_resolution = proc do + options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! + end + + if options[:parseable] + Bundler.ui.silence(&definition_resolution) + else + definition_resolution.call + end + + Bundler.ui.info "" + outdated_gems_by_groups = {} + outdated_gems_list = [] + + # Loop through the current specs + gemfile_specs, dependency_specs = current_specs.partition do |spec| + current_dependencies.key? spec.name + end + + (gemfile_specs + dependency_specs).sort_by(&:name).each do |current_spec| + next if !gems.empty? && !gems.include?(current_spec.name) + + dependency = current_dependencies[current_spec.name] + active_spec = retrieve_active_spec(strict, definition, current_spec) + + next if active_spec.nil? + if filter_options_patch.any? + update_present = update_present_via_semver_portions(current_spec, active_spec, options) + next unless update_present + end + + gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version) + next unless gem_outdated || (current_spec.git_version != active_spec.git_version) + groups = nil + if dependency && !options[:parseable] + groups = dependency.groups.join(", ") + end + + outdated_gems_list << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } + + outdated_gems_by_groups[groups] ||= [] + outdated_gems_by_groups[groups] << { :active_spec => active_spec, + :current_spec => current_spec, + :dependency => dependency, + :groups => groups } + end + + if outdated_gems_list.empty? + display_nothing_outdated_message(filter_options_patch) + else + unless options[:parseable] + if options[:pre] + Bundler.ui.info "Outdated gems included in the bundle (including " \ + "pre-releases):" + else + Bundler.ui.info "Outdated gems included in the bundle:" + end + end + + options_include_groups = [:group, :groups].select do |v| + options.keys.include?(v.to_s) + end + + if options_include_groups.any? + ordered_groups = outdated_gems_by_groups.keys.compact.sort + [nil, ordered_groups].flatten.each do |groups| + gems = outdated_gems_by_groups[groups] + contains_group = if groups + groups.split(",").include?(options[:group]) + else + options[:group] == "group" + end + + next if (!options[:groups] && !contains_group) || gems.nil? + + unless options[:parseable] + if groups + Bundler.ui.info "===== Group #{groups} =====" + else + Bundler.ui.info "===== Without group =====" + end + end + + gems.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + groups, + options_include_groups.any? + ) + end + end + else + outdated_gems_list.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + options_include_groups.any? + ) + end + end + + exit 1 + end + end + + private + + def retrieve_active_spec(strict, definition, current_spec) + if strict + active_spec = definition.find_resolved_spec(current_spec) + else + active_specs = definition.find_indexed_specs(current_spec) + if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 + active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + active_spec = active_specs.last + end + + active_spec + end + + def display_nothing_outdated_message(filter_options_patch) + unless options[:parseable] + if filter_options_patch.any? + display = filter_options_patch.map do |o| + o.sub("filter-", "") + end.join(" or ") + + Bundler.ui.info "No #{display} updates to display.\n" + else + Bundler.ui.info "Bundle up to date!\n" + end + end + end + + def print_gem(current_spec, active_spec, dependency, groups, options_include_groups) + spec_version = "#{active_spec.version}#{active_spec.git_version}" + spec_version += " (from #{active_spec.loaded_from})" if Bundler.ui.debug? && active_spec.loaded_from + current_version = "#{current_spec.version}#{current_spec.git_version}" + + if dependency && dependency.specific? + dependency_version = %(, requested #{dependency.requirement}) + end + + spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \ + "installed #{current_version}#{dependency_version})" + + output_message = if options[:parseable] + spec_outdated_info.to_s + elsif options_include_groups || !groups + " * #{spec_outdated_info}" + else + " * #{spec_outdated_info} in groups \"#{groups}\"" + end + + Bundler.ui.info output_message.rstrip + end + + def check_for_deployment_mode + if Bundler.settings[:frozen] + raise ProductionError, "You are trying to check outdated gems in " \ + "deployment mode. Run `bundle outdated` elsewhere.\n" \ + "\nIf this is a development machine, remove the " \ + "#{Bundler.default_gemfile} freeze" \ + "\nby running `bundle install --no-deployment`." + end + end + + def update_present_via_semver_portions(current_spec, active_spec, options) + current_major = current_spec.version.segments.first + active_major = active_spec.version.segments.first + + update_present = false + update_present = active_major > current_major if options["filter-major"] + + if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major + current_minor = get_version_semver_portion_value(current_spec, 1) + active_minor = get_version_semver_portion_value(active_spec, 1) + + update_present = active_minor > current_minor if options["filter-minor"] + + if !update_present && options["filter-patch"] && current_minor == active_minor + current_patch = get_version_semver_portion_value(current_spec, 2) + active_patch = get_version_semver_portion_value(active_spec, 2) + + update_present = active_patch > current_patch + end + end + + update_present + end + + def get_version_semver_portion_value(spec, version_portion_index) + version_section = spec.version.segments[version_portion_index, 1] + version_section.nil? ? 0 : (version_section.first || 0) + end + end +end diff --git a/lib/bundler/cli/package.rb b/lib/bundler/cli/package.rb new file mode 100644 index 0000000000..cf65e8a68c --- /dev/null +++ b/lib/bundler/cli/package.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true +module Bundler + class CLI::Package + attr_reader :options + + def initialize(options) + @options = options + end + + def run + Bundler.ui.level = "error" if options[:quiet] + Bundler.settings[:path] = File.expand_path(options[:path]) if options[:path] + Bundler.settings[:cache_all_platforms] = options["all-platforms"] if options.key?("all-platforms") + Bundler.settings[:cache_path] = options["cache-path"] if options.key?("cache-path") + + setup_cache_all + install + + # TODO: move cache contents here now that all bundles are locked + custom_path = Pathname.new(options[:path]) if options[:path] + Bundler.load.cache(custom_path) + end + + private + + def install + require "bundler/cli/install" + options = self.options.dup + if Bundler.settings[:cache_all_platforms] + options["local"] = false + options["update"] = true + end + Bundler::CLI::Install.new(options).run + end + + def setup_cache_all + Bundler.settings[:cache_all] = options[:all] if options.key?("all") + + if Bundler.definition.has_local_dependencies? && !Bundler.settings[:cache_all] + Bundler.ui.warn "Your Gemfile contains path and git dependencies. If you want " \ + "to package them as well, please pass the --all flag. This will be the default " \ + "on Bundler 2.0." + end + end + end +end diff --git a/lib/bundler/cli/platform.rb b/lib/bundler/cli/platform.rb new file mode 100644 index 0000000000..9fdab0a53c --- /dev/null +++ b/lib/bundler/cli/platform.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +module Bundler + class CLI::Platform + attr_reader :options + def initialize(options) + @options = options + end + + def run + platforms, ruby_version = Bundler.ui.silence do + locked_ruby_version = Bundler.locked_gems && Bundler.locked_gems.ruby_version + gemfile_ruby_version = Bundler.definition.ruby_version && Bundler.definition.ruby_version.single_version_string + [Bundler.definition.platforms.map {|p| "* #{p}" }, + locked_ruby_version || gemfile_ruby_version] + end + output = [] + + if options[:ruby] + if ruby_version + output << ruby_version + else + output << "No ruby version specified" + end + else + output << "Your platform is: #{RUBY_PLATFORM}" + output << "Your app has gems that work on these platforms:\n#{platforms.join("\n")}" + + if ruby_version + output << "Your Gemfile specifies a Ruby version requirement:\n* #{ruby_version}" + + begin + Bundler.definition.validate_runtime! + output << "Your current platform satisfies the Ruby version requirement." + rescue RubyVersionMismatch => e + output << e.message + end + else + output << "Your Gemfile does not specify a Ruby version requirement." + end + end + + Bundler.ui.info output.join("\n\n") + end + end +end diff --git a/lib/bundler/cli/plugin.rb b/lib/bundler/cli/plugin.rb new file mode 100644 index 0000000000..277822dafc --- /dev/null +++ b/lib/bundler/cli/plugin.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require "bundler/vendored_thor" +module Bundler + class CLI::Plugin < Thor + desc "install PLUGINS", "Install the plugin from the source" + long_desc <<-D + Install plugins either from the rubygems source provided (with --source option) or from a git source provided with (--git option). If no sources are provided, it uses Gem.sources + D + method_option "source", :type => :string, :default => nil, :banner => + "URL of the RubyGems source to fetch the plugin from" + method_option "version", :type => :string, :default => nil, :banner => + "The version of the plugin to fetch" + method_option "git", :type => :string, :default => nil, :banner => + "URL of the git repo to fetch from" + method_option "branch", :type => :string, :default => nil, :banner => + "The git branch to checkout" + method_option "ref", :type => :string, :default => nil, :banner => + "The git revision to check out" + def install(*plugins) + Bundler::Plugin.install(plugins, options) + end + end +end diff --git a/lib/bundler/cli/pristine.rb b/lib/bundler/cli/pristine.rb new file mode 100644 index 0000000000..10d03b4b41 --- /dev/null +++ b/lib/bundler/cli/pristine.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Pristine + def run + definition = Bundler.definition + definition.validate_runtime! + installer = Bundler::Installer.new(Bundler.root, definition) + + Bundler.load.specs.each do |spec| + next if spec.name == "bundler" # Source::Rubygems doesn't install bundler + + gem_name = "#{spec.name} (#{spec.version}#{spec.git_version})" + gem_name += " (#{spec.platform})" if !spec.platform.nil? && spec.platform != Gem::Platform::RUBY + + case source = spec.source + when Source::Rubygems + cached_gem = spec.cache_file + unless File.exist?(cached_gem) + Bundler.ui.error("Failed to pristine #{gem_name}. Cached gem #{cached_gem} does not exist.") + next + end + when Source::Git + source.remote! + else + Bundler.ui.warn("Cannot pristine #{gem_name}. Gem is sourced from local path.") + next + end + FileUtils.rm_rf spec.full_gem_path + + Bundler::GemInstaller.new(spec, installer, false, 0, true).install_from_spec + end + end + end +end diff --git a/lib/bundler/cli/show.rb b/lib/bundler/cli/show.rb new file mode 100644 index 0000000000..47d4470aec --- /dev/null +++ b/lib/bundler/cli/show.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Show + attr_reader :options, :gem_name, :latest_specs + def initialize(options, gem_name) + @options = options + @gem_name = gem_name + @verbose = options[:verbose] || options[:outdated] + @latest_specs = fetch_latest_specs if @verbose + end + + def run + Bundler.ui.silence do + Bundler.definition.validate_runtime! + Bundler.load.lock + end + + if gem_name + if gem_name == "bundler" + path = File.expand_path("../../../..", __FILE__) + else + spec = Bundler::CLI::Common.select_spec(gem_name, :regex_match) + return unless spec + path = spec.full_gem_path + unless File.directory?(path) + Bundler.ui.warn "The gem #{gem_name} has been deleted. It was installed at:" + end + end + return Bundler.ui.info(path) + end + + if options[:paths] + Bundler.load.specs.sort_by(&:name).map do |s| + Bundler.ui.info s.full_gem_path + end + else + Bundler.ui.info "Gems included by the bundle:" + Bundler.load.specs.sort_by(&:name).each do |s| + desc = " * #{s.name} (#{s.version}#{s.git_version})" + if @verbose + latest = latest_specs.find {|l| l.name == s.name } + Bundler.ui.info <<-END.gsub(/^ +/, "") + #{desc} + \tSummary: #{s.summary || "No description available."} + \tHomepage: #{s.homepage || "No website available."} + \tStatus: #{outdated?(s, latest) ? "Outdated - #{s.version} < #{latest.version}" : "Up to date"} + END + else + Bundler.ui.info desc + end + end + end + end + + private + + def fetch_latest_specs + definition = Bundler.definition(true) + if options[:outdated] + Bundler.ui.info "Fetching remote specs for outdated check...\n\n" + Bundler.ui.silence { definition.resolve_remotely! } + else + definition.resolve_with_cache! + end + Bundler.reset! + definition.specs + end + + def outdated?(current, latest) + return false unless latest + Gem::Version.new(current.version) < Gem::Version.new(latest.version) + end + end +end diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb new file mode 100644 index 0000000000..df7524f004 --- /dev/null +++ b/lib/bundler/cli/update.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require "bundler/cli/common" + +module Bundler + class CLI::Update + attr_reader :options, :gems + def initialize(options, gems) + @options = options + @gems = gems + end + + def run + Bundler.ui.level = "error" if options[:quiet] + + Plugin.gemfile_install(Bundler.default_gemfile) if Bundler.feature_flag.plugins? + + sources = Array(options[:source]) + groups = Array(options[:group]).map(&:to_sym) + + if gems.empty? && sources.empty? && groups.empty? && !options[:ruby] && !options[:bundler] + # We're doing a full update + Bundler.definition(true) + else + unless Bundler.default_lockfile.exist? + raise GemfileLockNotFound, "This Bundle hasn't been installed yet. " \ + "Run `bundle install` to update and install the bundled gems." + end + Bundler::CLI::Common.ensure_all_gems_in_lockfile!(gems) + + if groups.any? + specs = Bundler.definition.specs_for groups + gems.concat(specs.map(&:name)) + end + + Bundler.definition(:gems => gems, :sources => sources, :ruby => options[:ruby], + :lock_shared_dependencies => options[:conservative]) + end + + Bundler::CLI::Common.configure_gem_version_promoter(Bundler.definition, options) + + Bundler::Fetcher.disable_endpoint = options["full-index"] + + opts = options.dup + opts["update"] = true + opts["local"] = options[:local] + + Bundler.settings[:jobs] = opts["jobs"] if opts["jobs"] + + Bundler.definition.validate_runtime! + installer = Installer.install Bundler.root, Bundler.definition, opts + Bundler.load.cache if Bundler.app_cache.exist? + + if Bundler.settings[:clean] && Bundler.settings[:path] + require "bundler/cli/clean" + Bundler::CLI::Clean.new(options).run + end + + Bundler.ui.confirm "Bundle updated!" + Bundler::CLI::Common.output_without_groups_message + Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + end + end +end diff --git a/lib/bundler/cli/viz.rb b/lib/bundler/cli/viz.rb new file mode 100644 index 0000000000..767fe8f3de --- /dev/null +++ b/lib/bundler/cli/viz.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true +module Bundler + class CLI::Viz + attr_reader :options, :gem_name + def initialize(options) + @options = options + end + + def run + # make sure we get the right `graphviz`. There is also a `graphviz` + # gem we're not built to support + gem "ruby-graphviz" + require "graphviz" + + options[:without] = options[:without].join(":").tr(" ", ":").split(":") + output_file = File.expand_path(options[:file]) + + graph = Graph.new(Bundler.load, output_file, options[:version], options[:requirements], options[:format], options[:without]) + graph.viz + rescue LoadError => e + Bundler.ui.error e.inspect + Bundler.ui.warn "Make sure you have the graphviz ruby gem. You can install it with:" + Bundler.ui.warn "`gem install ruby-graphviz`" + rescue StandardError => e + raise unless e.message =~ /GraphViz not installed or dot not in PATH/ + Bundler.ui.error e.message + Bundler.ui.warn "Please install GraphViz. On a Mac with Homebrew, you can run `brew install graphviz`." + end + end +end |