diff options
author | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2007-11-10 07:48:56 +0000 |
---|---|---|
committer | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2007-11-10 07:48:56 +0000 |
commit | fbf59bdbea63efd34ccc144e648467d2f52e7345 (patch) | |
tree | 244f0e7ae112cc7dd135e5d1ac24e6c70ba71b4a /lib/rubygems | |
parent | 7a4aad75356496559460041a6c063bdb736c7236 (diff) | |
download | ruby-fbf59bdbea63efd34ccc144e648467d2f52e7345.tar.gz |
Import RubyGems trunk revision 1493.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@13862 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rubygems')
78 files changed, 11644 insertions, 0 deletions
diff --git a/lib/rubygems/builder.rb b/lib/rubygems/builder.rb new file mode 100644 index 0000000000..f7f07e86bf --- /dev/null +++ b/lib/rubygems/builder.rb @@ -0,0 +1,81 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + ## + # The Builder class processes RubyGem specification files + # to produce a .gem file. + # + class Builder + + include UserInteraction + ## + # Constructs a builder instance for the provided specification + # + # spec:: [Gem::Specification] The specification instance + # + def initialize(spec) + require "yaml" + require "rubygems/package" + require "rubygems/security" + + @spec = spec + end + + ## + # Builds the gem from the specification. Returns the name of the file + # written. + # + def build + @spec.mark_version + @spec.validate + @signer = sign + write_package + say success + @spec.file_name + end + + def success + <<-EOM + Successfully built RubyGem + Name: #{@spec.name} + Version: #{@spec.version} + File: #{@spec.full_name+'.gem'} +EOM + end + + private + + def sign + # if the signing key was specified, then load the file, and swap + # to the public key (TODO: we should probably just omit the + # signing key in favor of the signing certificate, but that's for + # the future, also the signature algorithm should be configurable) + signer = nil + if @spec.respond_to?(:signing_key) && @spec.signing_key + signer = Gem::Security::Signer.new(@spec.signing_key, @spec.cert_chain) + @spec.signing_key = nil + @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s } + end + signer + end + + def write_package + Package.open(@spec.file_name, "w", @signer) do |pkg| + pkg.metadata = @spec.to_yaml + @spec.files.each do |file| + next if File.directory? file + pkg.add_file_simple(file, File.stat(@spec.file_name).mode & 0777, + File.size(file)) do |os| + os.write File.open(file, "rb"){|f|f.read} + end + end + end + end + end +end + diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb new file mode 100644 index 0000000000..66855c7c6a --- /dev/null +++ b/lib/rubygems/command.rb @@ -0,0 +1,406 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'optparse' + +require 'rubygems/user_interaction' + +module Gem + + # Base class for all Gem commands. When creating a new gem command, define + # #arguments, #defaults_str, #description and #usage (as appropriate). + class Command + + include UserInteraction + + # The name of the command. + attr_reader :command + + # The options for the command. + attr_reader :options + + # The default options for the command. + attr_accessor :defaults + + # The name of the command for command-line invocation. + attr_accessor :program_name + + # A short description of the command. + attr_accessor :summary + + # Initializes a generic gem command named +command+. +summary+ is a short + # description displayed in `gem help commands`. +defaults+ are the + # default options. Defaults should be mirrored in #defaults_str, unless + # there are none. + # + # Use add_option to add command-line switches. + def initialize(command, summary=nil, defaults={}) + @command = command + @summary = summary + @program_name = "gem #{command}" + @defaults = defaults + @options = defaults.dup + @option_groups = Hash.new { |h,k| h[k] = [] } + @parser = nil + @when_invoked = nil + end + + # True if +long+ begins with the characters from +short+. + def begins?(long, short) + return false if short.nil? + long[0, short.length] == short + end + + # Override to provide command handling. + def execute + fail "Generic command has no actions" + end + + # Get all gem names from the command line. + def get_all_gem_names + args = options[:args] + + if args.nil? or args.empty? then + raise Gem::CommandLineError, + "Please specify at least one gem name (e.g. gem build GEMNAME)" + end + + gem_names = args.select { |arg| arg !~ /^-/ } + end + + # Get the single gem name from the command line. Fail if there is no gem + # name or if there is more than one gem name given. + def get_one_gem_name + args = options[:args] + + if args.nil? or args.empty? then + raise Gem::CommandLineError, + "Please specify a gem name on the command line (e.g. gem build GEMNAME)" + end + + if args.size > 1 then + raise Gem::CommandLineError, + "Too many gem names (#{args.join(', ')}); please specify only one" + end + + args.first + end + + # Get a single optional argument from the command line. If more than one + # argument is given, return only the first. Return nil if none are given. + def get_one_optional_argument + args = options[:args] || [] + args.first + end + + # Override to provide details of the arguments a command takes. + # It should return a left-justified string, one argument per line. + def arguments + "" + end + + # Override to display the default values of the command + # options. (similar to +arguments+, but displays the default + # values). + def defaults_str + "" + end + + # Override to display a longer description of what this command does. + def description + nil + end + + # Override to display the usage for an individual gem command. + def usage + program_name + end + + # Display the help message for the command. + def show_help + parser.program_name = usage + say parser + end + + # Invoke the command with the given list of arguments. + def invoke(*args) + handle_options(args) + if options[:help] + show_help + elsif @when_invoked + @when_invoked.call(options) + else + execute + end + end + + # Call the given block when invoked. + # + # Normal command invocations just executes the +execute+ method of + # the command. Specifying an invocation block allows the test + # methods to override the normal action of a command to determine + # that it has been invoked correctly. + def when_invoked(&block) + @when_invoked = block + end + + # Add a command-line option and handler to the command. + # + # See OptionParser#make_switch for an explanation of +opts+. + # + # +handler+ will be called with two values, the value of the argument and + # the options hash. + def add_option(*opts, &handler) # :yields: value, options + group_name = Symbol === opts.first ? opts.shift : :options + + @option_groups[group_name] << [opts, handler] + end + + # Remove previously defined command-line argument +name+. + def remove_option(name) + @option_groups.each do |_, option_list| + option_list.reject! { |args, _| args.any? { |x| x =~ /^#{name}/ } } + end + end + + # Merge a set of command options with the set of default options + # (without modifying the default option hash). + def merge_options(new_options) + @options = @defaults.clone + new_options.each do |k,v| @options[k] = v end + end + + # True if the command handles the given argument list. + def handles?(args) + begin + parser.parse!(args.dup) + return true + rescue + return false + end + end + + # Handle the given list of arguments by parsing them and recording + # the results. + def handle_options(args) + args = add_extra_args(args) + @options = @defaults.clone + parser.parse!(args) + @options[:args] = args + end + + def add_extra_args(args) + result = [] + s_extra = Command.specific_extra_args(@command) + extra = Command.extra_args + s_extra + while ! extra.empty? + ex = [] + ex << extra.shift + ex << extra.shift if extra.first.to_s =~ /^[^-]/ + result << ex if handles?(ex) + end + result.flatten! + result.concat(args) + result + end + + private + + # Create on demand parser. + def parser + create_option_parser if @parser.nil? + @parser + end + + def create_option_parser + @parser = OptionParser.new + + @parser.separator("") + regular_options = @option_groups.delete :options + + configure_options "", regular_options + + @option_groups.sort_by { |n,_| n.to_s }.each do |group_name, option_list| + configure_options group_name, option_list + end + + configure_options "Common", Command.common_options + + @parser.separator("") + unless arguments.empty? + @parser.separator(" Arguments:") + arguments.split(/\n/).each do |arg_desc| + @parser.separator(" #{arg_desc}") + end + @parser.separator("") + end + + @parser.separator(" Summary:") + wrap(@summary, 80 - 4).split("\n").each do |line| + @parser.separator(" #{line.strip}") + end + + if description then + formatted = description.split("\n\n").map do |chunk| + wrap(chunk, 80 - 4) + end.join("\n") + + @parser.separator "" + @parser.separator " Description:" + formatted.split("\n").each do |line| + @parser.separator " #{line.rstrip}" + end + end + + unless defaults_str.empty? + @parser.separator("") + @parser.separator(" Defaults:") + defaults_str.split(/\n/).each do |line| + @parser.separator(" #{line}") + end + end + end + + def configure_options(header, option_list) + return if option_list.nil? or option_list.empty? + + header = header.to_s.empty? ? '' : "#{header} " + @parser.separator " #{header}Options:" + + option_list.each do |args, handler| + dashes = args.select { |arg| arg =~ /^-/ } + @parser.on(*args) do |value| + handler.call(value, @options) + end + end + + @parser.separator '' + end + + # Wraps +text+ to +width+ + def wrap(text, width) + text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n") + end + + ################################################################## + # Class methods for Command. + class << self + def common_options + @common_options ||= [] + end + + def add_common_option(*args, &handler) + Gem::Command.common_options << [args, handler] + end + + def extra_args + @extra_args ||= [] + end + + def extra_args=(value) + case value + when Array + @extra_args = value + when String + @extra_args = value.split + end + end + + # Return an array of extra arguments for the command. The extra + # arguments come from the gem configuration file read at program + # startup. + def specific_extra_args(cmd) + specific_extra_args_hash[cmd] + end + + # Add a list of extra arguments for the given command. +args+ + # may be an array or a string to be split on white space. + def add_specific_extra_args(cmd,args) + args = args.split(/\s+/) if args.kind_of? String + specific_extra_args_hash[cmd] = args + end + + # Accessor for the specific extra args hash (self initializing). + def specific_extra_args_hash + @specific_extra_args_hash ||= Hash.new do |h,k| + h[k] = Array.new + end + end + end + + # ---------------------------------------------------------------- + # Add the options common to all commands. + + add_common_option('-h', '--help', + 'Get help on this command') do + |value, options| + options[:help] = true + end + + add_common_option('-V', '--[no-]verbose', + 'Set the verbose level of output') do |value, options| + # Set us to "really verbose" so the progess meter works + if Gem.configuration.verbose and value then + Gem.configuration.verbose = 1 + else + Gem.configuration.verbose = value + end + end + + add_common_option('-q', '--quiet', 'Silence commands') do |value, options| + Gem.configuration.verbose = false + end + + # Backtrace and config-file are added so they show up in the help + # commands. Both options are actually handled before the other + # options get parsed. + + add_common_option('--config-file FILE', + "Use this config file instead of default") do + end + + add_common_option('--backtrace', + 'Show stack backtrace on errors') do + end + + add_common_option('--debug', + 'Turn on Ruby debugging') do + end + + # :stopdoc: + HELP = %{ + RubyGems is a sophisticated package manager for Ruby. This is a + basic help message containing pointers to more information. + + Usage: + gem -h/--help + gem -v/--version + gem command [arguments...] [options...] + + Examples: + gem install rake + gem list --local + gem build package.gemspec + gem help install + + Further help: + gem help commands list all 'gem' commands + gem help examples show some examples of usage + gem help platforms show information about platforms + gem help <COMMAND> show help on COMMAND + (e.g. 'gem help install') + Further information: + http://rubygems.rubyforge.org + }.gsub(/^ /, "") + + # :startdoc: + + end # class + + # This is where Commands will be placed in the namespace + module Commands; end + +end diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb new file mode 100644 index 0000000000..a80c821c5c --- /dev/null +++ b/lib/rubygems/command_manager.rb @@ -0,0 +1,144 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'timeout' +require 'rubygems/command' +require 'rubygems/user_interaction' + +module Gem + + #################################################################### + # The command manager registers and installs all the individual + # sub-commands supported by the gem command. + class CommandManager + include UserInteraction + + # Return the authoratative instance of the command manager. + def self.instance + @command_manager ||= CommandManager.new + end + + # Register all the subcommands supported by the gem command. + def initialize + @commands = {} + register_command :build + register_command :cert + register_command :check + register_command :cleanup + register_command :contents + register_command :dependency + register_command :environment + register_command :fetch + register_command :generate_index + register_command :help + register_command :install + register_command :list + register_command :lock + register_command :mirror + register_command :outdated + register_command :pristine + register_command :query + register_command :rdoc + register_command :search + register_command :server + register_command :sources + register_command :specification + register_command :uninstall + register_command :unpack + register_command :update + register_command :which + end + + # Register the command object. + def register_command(command_obj) + @commands[command_obj] = false + end + + # Return the registered command from the command name. + def [](command_name) + command_name = command_name.intern + return nil if @commands[command_name].nil? + @commands[command_name] ||= load_and_instantiate(command_name) + end + + # Return a list of all command names (as strings). + def command_names + @commands.keys.collect {|key| key.to_s}.sort + end + + # Run the config specificed by +args+. + def run(args) + process_args(args) + rescue StandardError, Timeout::Error => ex + alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}" + ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if + Gem.configuration.backtrace + terminate_interaction(1) + rescue Interrupt + alert_error "Interrupted" + terminate_interaction(1) + end + + def process_args(args) + args = args.to_str.split(/\s+/) if args.respond_to?(:to_str) + if args.size == 0 + say Gem::Command::HELP + terminate_interaction(1) + end + case args[0] + when '-h', '--help' + say Gem::Command::HELP + terminate_interaction(0) + when '-v', '--version' + say Gem::RubyGemsPackageVersion + terminate_interaction(0) + when /^-/ + alert_error "Invalid option: #{args[0]}. See 'gem --help'." + terminate_interaction(1) + else + cmd_name = args.shift.downcase + cmd = find_command(cmd_name) + cmd.invoke(*args) + end + end + + def find_command(cmd_name) + possibilities = find_command_possibilities(cmd_name) + if possibilities.size > 1 + raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]" + end + if possibilities.size < 1 + raise "Unknown command #{cmd_name}" + end + + self[possibilities.first] + end + + def find_command_possibilities(cmd_name) + len = cmd_name.length + self.command_names.select { |n| cmd_name == n[0,len] } + end + + private + def load_and_instantiate(command_name) + command_name = command_name.to_s + retried = false + + begin + const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } + Gem::Commands.const_get("#{const_name}Command").new + rescue NameError + if retried then + raise + else + retried = true + require "rubygems/commands/#{command_name}_command" + retry + end + end + end + end +end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb new file mode 100644 index 0000000000..c2e1abc92f --- /dev/null +++ b/lib/rubygems/commands/build_command.rb @@ -0,0 +1,53 @@ +require 'rubygems/command' +require 'rubygems/builder' + +class Gem::Commands::BuildCommand < Gem::Command + + def initialize + super('build', 'Build a gem from a gemspec') + end + + def arguments # :nodoc: + "GEMSPEC_FILE gemspec file name to build a gem for" + end + + def usage # :nodoc: + "#{program_name} GEMSPEC_FILE" + end + + def execute + gemspec = get_one_gem_name + if File.exist?(gemspec) + specs = load_gemspecs(gemspec) + specs.each do |spec| + Gem::Builder.new(spec).build + end + else + alert_error "Gemspec file not found: #{gemspec}" + end + end + + def load_gemspecs(filename) + if yaml?(filename) + result = [] + open(filename) do |f| + begin + while not f.eof? and spec = Gem::Specification.from_yaml(f) + result << spec + end + rescue Gem::EndOfYAMLException => e + # OK + end + end + else + result = [Gem::Specification.load(filename)] + end + result + end + + def yaml?(filename) + line = open(filename) { |f| line = f.gets } + result = line =~ %r{^--- *!ruby/object:Gem::Specification} + result + end +end diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb new file mode 100644 index 0000000000..2c32099254 --- /dev/null +++ b/lib/rubygems/commands/cert_command.rb @@ -0,0 +1,86 @@ +require 'rubygems/command' +require 'rubygems/security' + +class Gem::Commands::CertCommand < Gem::Command + + def initialize + super 'cert', 'Manage RubyGems certificates and signing settings' + + add_option('-a', '--add CERT', + 'Add a trusted certificate.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + Gem::Security.add_trusted_cert(cert) + say "Added '#{cert.subject.to_s}'" + end + + add_option('-l', '--list', + 'List trusted certificates.') do |value, options| + glob_str = File::join(Gem::Security::OPT[:trust_dir], '*.pem') + Dir::glob(glob_str) do |path| + begin + cert = OpenSSL::X509::Certificate.new(File.read(path)) + # this could proably be formatted more gracefully + say cert.subject.to_s + rescue OpenSSL::X509::CertificateError + next + end + end + end + + add_option('-r', '--remove STRING', + 'Remove trusted certificates containing', + 'STRING.') do |value, options| + trust_dir = Gem::Security::OPT[:trust_dir] + glob_str = File::join(trust_dir, '*.pem') + + Dir::glob(glob_str) do |path| + begin + cert = OpenSSL::X509::Certificate.new(File.read(path)) + if cert.subject.to_s.downcase.index(value) + say "Removed '#{cert.subject.to_s}'" + File.unlink(path) + end + rescue OpenSSL::X509::CertificateError + next + end + end + end + + add_option('-b', '--build EMAIL_ADDR', + 'Build private key and self-signed', + 'certificate for EMAIL_ADDR.') do |value, options| + vals = Gem::Security.build_self_signed_cert(value) + File.chmod 0600, vals[:key_path] + say "Public Cert: #{vals[:cert_path]}" + say "Private Key: #{vals[:key_path]}" + say "Don't forget to move the key file to somewhere private..." + end + + add_option('-C', '--certificate CERT', + 'Certificate for --sign command.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + Gem::Security::OPT[:issuer_cert] = cert + end + + add_option('-K', '--private-key KEY', + 'Private key for --sign command.') do |value, options| + key = OpenSSL::PKey::RSA.new(File.read(value)) + Gem::Security::OPT[:issuer_key] = key + end + + add_option('-s', '--sign NEWCERT', + 'Sign a certificate with my key and', + 'certificate.') do |value, options| + cert = OpenSSL::X509::Certificate.new(File.read(value)) + my_cert = Gem::Security::OPT[:issuer_cert] + my_key = Gem::Security::OPT[:issuer_key] + cert = Gem::Security.sign_cert(cert, my_key, my_cert) + File.open(value, 'wb') { |file| file.write(cert.to_pem) } + end + end + + def execute + end + +end + diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb new file mode 100644 index 0000000000..ca5e14b12d --- /dev/null +++ b/lib/rubygems/commands/check_command.rb @@ -0,0 +1,74 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/validator' + +class Gem::Commands::CheckCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'check', 'Check installed gems', + :verify => false, :alien => false + + add_option( '--verify FILE', + 'Verify gem file against its internal', + 'checksum') do |value, options| + options[:verify] = value + end + + add_option('-a', '--alien', "Report 'unmanaged' or rogue files in the", + "gem repository") do |value, options| + options[:alien] = true + end + + add_option('-t', '--test', "Run unit tests for gem") do |value, options| + options[:test] = true + end + + add_version_option 'run tests for' + end + + def execute + if options[:test] + version = options[:version] || Gem::Requirement.default + gem_spec = Gem::SourceIndex.from_installed_gems.search(get_one_gem_name, version).first + Gem::Validator.new.unit_test(gem_spec) + end + + if options[:alien] + say "Performing the 'alien' operation" + Gem::Validator.new.alien.each do |key, val| + if(val.size > 0) + say "#{key} has #{val.size} problems" + val.each do |error_entry| + say "\t#{error_entry.path}:" + say "\t#{error_entry.problem}" + say + end + else + say "#{key} is error-free" + end + say + end + end + + if options[:verify] + gem_name = options[:verify] + unless gem_name + alert_error "Must specify a .gem file with --verify NAME" + return + end + unless File.exist?(gem_name) + alert_error "Unknown file: #{gem_name}." + return + end + say "Verifying gem: '#{gem_name}'" + begin + Gem::Validator.new.verify_gem_file(gem_name) + rescue Exception => e + alert_error "#{gem_name} is invalid." + end + end + end + +end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb new file mode 100644 index 0000000000..f6deac9829 --- /dev/null +++ b/lib/rubygems/commands/cleanup_command.rb @@ -0,0 +1,93 @@ +require 'rubygems/command' +require 'rubygems/source_index' +require 'rubygems/dependency_list' + +module Gem + module Commands + class CleanupCommand < Command + def initialize + super( + 'cleanup', + 'Clean up old versions of installed gems in the local repository', + { + :force => false, + :test => false, + :install_dir => Gem.dir + }) + add_option('-d', '--dryrun', "") do |value, options| + options[:dryrun] = true + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to cleanup" + end + + def defaults_str # :nodoc: + "--no-dryrun" + end + + def usage # :nodoc: + "#{program_name} [GEMNAME ...]" + end + + def execute + say "Cleaning up installed gems..." + srcindex = Gem::SourceIndex.from_installed_gems + primary_gems = {} + + srcindex.each do |name, spec| + if primary_gems[spec.name].nil? or primary_gems[spec.name].version < spec.version + primary_gems[spec.name] = spec + end + end + + gems_to_cleanup = [] + + unless options[:args].empty? then + options[:args].each do |gem_name| + specs = Gem.cache.search(/^#{gem_name}$/i) + specs.each do |spec| + gems_to_cleanup << spec + end + end + else + srcindex.each do |name, spec| + gems_to_cleanup << spec + end + end + + gems_to_cleanup = gems_to_cleanup.select { |spec| + primary_gems[spec.name].version != spec.version + } + + uninstall_command = Gem::CommandManager.instance['uninstall'] + deplist = DependencyList.new + gems_to_cleanup.uniq.each do |spec| deplist.add(spec) end + + deplist.dependency_order.each do |spec| + if options[:dryrun] then + say "Dry Run Mode: Would uninstall #{spec.full_name}" + else + say "Attempting uninstall on #{spec.full_name}" + + options[:args] = [spec.name] + options[:version] = "= #{spec.version}" + options[:executables] = true + + uninstall_command.merge_options(options) + + begin + uninstall_command.execute + rescue Gem::DependencyRemovalException => ex + say "Unable to uninstall #{spec.full_name} ... continuing with remaining gems" + end + end + end + + say "Clean Up Complete" + end + end + + end +end diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb new file mode 100644 index 0000000000..5060403fd8 --- /dev/null +++ b/lib/rubygems/commands/contents_command.rb @@ -0,0 +1,74 @@ +require 'rubygems/command' +require 'rubygems/version_option' + +class Gem::Commands::ContentsCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'contents', 'Display the contents of the installed gems', + :specdirs => [], :lib_only => false + + add_version_option + + add_option('-s', '--spec-dir a,b,c', Array, + "Search for gems under specific paths") do |spec_dirs, options| + options[:specdirs] = spec_dirs + end + + add_option('-l', '--[no-]lib-only', + "Only return files in the Gem's lib_dirs") do |lib_only, options| + options[:lib_only] = lib_only + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to list contents for" + end + + def defaults_str # :nodoc: + "--no-lib-only" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + def execute + version = options[:version] || Gem::Requirement.default + gem = get_one_gem_name + + s = options[:specdirs].map do |i| + [i, File.join(i, "specifications")] + end.flatten + + path_kind = if s.empty? then + s = Gem::SourceIndex.installed_spec_directories + "default gem paths" + else + "specified path" + end + + si = Gem::SourceIndex.from_gems_in(*s) + + gem_spec = si.search(/\A#{gem}\z/, version).last + + unless gem_spec then + say "Unable to find gem '#{gem}' in #{path_kind}" + + if Gem.configuration.verbose then + say "\nDirectories searched:" + s.each { |dir| say dir } + end + + terminate_interaction + end + + files = options[:lib_only] ? gem_spec.lib_files : gem_spec.files + files.each do |f| + say File.join(gem_spec.full_gem_path, f) + end + end + +end + diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb new file mode 100644 index 0000000000..1a43505d7c --- /dev/null +++ b/lib/rubygems/commands/dependency_command.rb @@ -0,0 +1,150 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::DependencyCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'dependency', + 'Show the dependencies of an installed gem', + :version => Gem::Requirement.default, :domain => :local + + add_version_option + add_platform_option + + add_option('-R', '--[no-]reverse-dependencies', + 'Include reverse dependencies in the output') do + |value, options| + options[:reverse_dependencies] = value + end + + add_option('-p', '--pipe', + "Pipe Format (name --version ver)") do |value, options| + options[:pipe_format] = value + end + + add_local_remote_options + end + + def arguments # :nodoc: + "GEMNAME name of gem to show dependencies for" + end + + def defaults_str # :nodoc: + "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + def execute + options[:args] << '.' if options[:args].empty? + specs = {} + + source_indexes = [] + + if local? then + source_indexes << Gem::SourceIndex.from_installed_gems + end + + if remote? then + Gem::SourceInfoCache.cache_data.map do |_, sice| + source_indexes << sice.source_index + end + end + + options[:args].each do |name| + new_specs = nil + source_indexes.each do |source_index| + new_specs = find_gems(name, source_index) + end + + say "No match found for #{name} (#{options[:version]})" if + new_specs.empty? + + specs = specs.merge new_specs + end + + terminate_interaction 1 if specs.empty? + + reverse = Hash.new { |h, k| h[k] = [] } + + if options[:reverse_dependencies] then + specs.values.each do |source_index, spec| + reverse[spec.full_name] = find_reverse_dependencies spec, source_index + end + end + + if options[:pipe_format] then + specs.values.sort_by { |_, spec| spec }.each do |_, spec| + unless spec.dependencies.empty? + spec.dependencies.each do |dep| + say "#{dep.name} --version '#{dep.version_requirements}'" + end + end + end + else + response = '' + + specs.values.sort_by { |_, spec| spec }.each do |_, spec| + response << print_dependencies(spec) + unless reverse[spec.full_name].empty? then + response << " Used by\n" + reverse[spec.full_name].each do |sp, dep| + response << " #{sp} (#{dep})\n" + end + end + response << "\n" + end + + say response + end + end + + def print_dependencies(spec, level = 0) + response = '' + response << ' ' * level + "Gem #{spec.full_name}\n" + unless spec.dependencies.empty? then + spec.dependencies.each do |dep| + response << ' ' * level + " #{dep}\n" + end + end + response + end + + # Retuns list of [specification, dep] that are satisfied by spec. + def find_reverse_dependencies(spec, source_index) + result = [] + + source_index.each do |name, sp| + sp.dependencies.each do |dep| + dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep + + if spec.name == dep.name and + dep.version_requirements.satisfied_by?(spec.version) then + result << [sp.full_name, dep] + end + end + end + + result + end + + def find_gems(name, source_index) + specs = {} + + spec_list = source_index.search name, options[:version] + + spec_list.each do |spec| + specs[spec.full_name] = [source_index, spec] + end + + specs + end +end + diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb new file mode 100644 index 0000000000..337d74893b --- /dev/null +++ b/lib/rubygems/commands/environment_command.rb @@ -0,0 +1,80 @@ +require 'rubygems/command' + +class Gem::Commands::EnvironmentCommand < Gem::Command + + def initialize + super 'environment', 'Display information about the RubyGems environment' + end + + def arguments # :nodoc: + args = <<-EOF + packageversion display the package version + gemdir display the path where gems are installed + gempath display path used to search for gems + version display the gem format version + remotesources display the remote gem servers + <omitted> display everything + EOF + return args.gsub(/^\s+/, '') + end + + def usage # :nodoc: + "#{program_name} [arg]" + end + + def execute + out = '' + arg = options[:args][0] + if begins?("packageversion", arg) then + out << Gem::RubyGemsPackageVersion + elsif begins?("version", arg) then + out << Gem::RubyGemsVersion + elsif begins?("gemdir", arg) then + out << Gem.dir + elsif begins?("gempath", arg) then + out << Gem.path.join("\n") + elsif begins?("remotesources", arg) then + out << Gem.sources.join("\n") + elsif arg then + fail Gem::CommandLineError, "Unknown enviroment option [#{arg}]" + else + out = "RubyGems Environment:\n" + + out << " - RUBYGEMS VERSION: #{Gem::RubyGemsVersion} (#{Gem::RubyGemsPackageVersion})\n" + + out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" + out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL + out << ") [#{RUBY_PLATFORM}]\n" + + out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n" + + out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? + + out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" + + out << " - RUBYGEMS PLATFORMS:\n" + Gem.platforms.each do |platform| + out << " - #{platform}\n" + end + + out << " - GEM PATHS:\n" + Gem.path.each do |p| + out << " - #{p}\n" + end + + out << " - GEM CONFIGURATION:\n" + Gem.configuration.each do |name, value| + out << " - #{name.inspect} => #{value.inspect}\n" + end + + out << " - REMOTE SOURCES:\n" + Gem.sources.each do |s| + out << " - #{s}\n" + end + end + say out + true + end + +end + diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb new file mode 100644 index 0000000000..7db365eba0 --- /dev/null +++ b/lib/rubygems/commands/fetch_command.rb @@ -0,0 +1,62 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::FetchCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'fetch', 'Download a gem and place it in the current directory' + + add_bulk_threshold_option + add_proxy_option + add_source_option + + add_version_option + add_platform_option + end + + def arguments # :nodoc: + 'GEMNAME name of gem to download' + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + version = options[:version] || Gem::Requirement.default + + gem_names = get_all_gem_names + + gem_names.each do |gem_name| + dep = Gem::Dependency.new gem_name, version + specs_and_sources = Gem::SourceInfoCache.search_with_source dep, true + + specs_and_sources.sort_by { |spec,| spec.version } + + spec, source_uri = specs_and_sources.last + + gem_file = "#{spec.full_name}.gem" + + gem_path = File.join source_uri, 'gems', gem_file + + gem = Gem::RemoteFetcher.fetcher.fetch_path gem_path + + File.open gem_file, 'wb' do |fp| + fp.write gem + end + + say "Downloaded #{gem_file}" + end + end + +end + diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb new file mode 100644 index 0000000000..1bd87569ed --- /dev/null +++ b/lib/rubygems/commands/generate_index_command.rb @@ -0,0 +1,57 @@ +require 'rubygems/command' +require 'rubygems/indexer' + +class Gem::Commands::GenerateIndexCommand < Gem::Command + + def initialize + super 'generate_index', + 'Generates the index files for a gem server directory', + :directory => '.' + + add_option '-d', '--directory=DIRNAME', + 'repository base dir containing gems subdir' do |dir, options| + options[:directory] = File.expand_path dir + end + end + + def defaults_str # :nodoc: + "--directory ." + end + + def description # :nodoc: + <<-EOF +The generate_index command creates a set of indexes for serving gems +statically. The command expects a 'gems' directory under the path given to +the --directory option. When done, it will generate a set of files like this: + + gems/ # .gem files you want to index + quick/index + quick/index.rz # quick index manifest + quick/<gemname>.gemspec.rz # legacy YAML quick index file + quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file + Marshal.<version> + Marshal.<version>.Z # Marshal full index + yaml + yaml.Z # legacy YAML full index + +The .Z and .rz extension files are compressed with the inflate algorithm. The +Marshal version number comes from ruby's Marshal::MAJOR_VERSION and +Marshal::MINOR_VERSION constants. It is used to ensure compatibility. The +yaml indexes exist for legacy RubyGems clients and fallback in case of Marshal +version changes. + EOF + end + + def execute + if not File.exist?(options[:directory]) or + not File.directory?(options[:directory]) then + alert_error "unknown directory name #{directory}." + terminate_interaction 1 + else + indexer = Gem::Indexer.new options[:directory] + indexer.generate_index + end + end + +end + diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb new file mode 100644 index 0000000000..05ea3f7a71 --- /dev/null +++ b/lib/rubygems/commands/help_command.rb @@ -0,0 +1,172 @@ +require 'rubygems/command' + +class Gem::Commands::HelpCommand < Gem::Command + + # :stopdoc: + EXAMPLES = <<-EOF +Some examples of 'gem' usage. + +* Install 'rake', either from local directory or remote server: + + gem install rake + +* Install 'rake', only from remote server: + + gem install rake --remote + +* Install 'rake' from remote server, and run unit tests, + and generate RDocs: + + gem install --remote rake --test --rdoc --ri + +* Install 'rake', but only version 0.3.1, even if dependencies + are not met, and into a specific directory: + + gem install rake --version 0.3.1 --force --install-dir $HOME/.gems + +* List local gems whose name begins with 'D': + + gem list D + +* List local and remote gems whose name contains 'log': + + gem search log --both + +* List only remote gems whose name contains 'log': + + gem search log --remote + +* Uninstall 'rake': + + gem uninstall rake + +* Create a gem: + + See http://rubygems.rubyforge.org/wiki/wiki.pl?CreateAGemInTenMinutes + +* See information about RubyGems: + + gem environment + +* Update all gems on your system: + + gem update + EOF + + PLATFORMS = <<-'EOF' +RubyGems platforms are composed of three parts, a CPU, an OS, and a +version. These values are taken from values in rbconfig.rb. You can view +your current platform by running `gem environment`. + +RubyGems matches platforms as follows: + + * The CPU must match exactly, unless one of the platforms has + "universal" as the CPU. + * The OS must match exactly. + * The versions must match exactly unless one of the versions is nil. + +For commands that install, uninstall and list gems, you can override what +RubyGems thinks your platform is with the --platform option. The platform +you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin +platforms, the version is the compiler version, not the OS version. (Ruby +compiled with VC6 uses "60" as the compiler version, VC8 uses "80".) + +Example platforms: + + x86-freebsd # Any FreeBSD version on an x86 CPU + universal-darwin-8 # Darwin 8 only gems that run on any CPU + x86-mswin32-80 # Windows gems compiled with VC8 + +When building platform gems, set the platform in the gem specification to +Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's +platform. + EOF + # :startdoc: + + def initialize + super 'help', "Provide help on the 'gem' command" + end + + def arguments # :nodoc: + args = <<-EOF + commands List all 'gem' commands + examples Show examples of 'gem' usage + <command> Show specific help for <command> + EOF + return args.gsub(/^\s+/, '') + end + + def usage # :nodoc: + "#{program_name} ARGUMENT" + end + + def execute + command_manager = Gem::CommandManager.instance + arg = options[:args][0] + + if begins? "commands", arg then + out = [] + out << "GEM commands are:" + out << nil + + margin_width = 4 + + desc_width = command_manager.command_names.map { |n| n.size }.max + 4 + + summary_width = 80 - margin_width - desc_width + wrap_indent = ' ' * (margin_width + desc_width) + format = "#{' ' * margin_width}%-#{desc_width}s%s" + + command_manager.command_names.each do |cmd_name| + summary = command_manager[cmd_name].summary + summary = wrap(summary, summary_width).split "\n" + out << sprintf(format, cmd_name, summary.shift) + until summary.empty? do + out << "#{wrap_indent}#{summary.shift}" + end + end + + out << nil + out << "For help on a particular command, use 'gem help COMMAND'." + out << nil + out << "Commands may be abbreviated, so long as they are unambiguous." + out << "e.g. 'gem i rake' is short for 'gem install rake'." + + say out.join("\n") + + elsif begins? "options", arg then + say Gem::Command::HELP + + elsif begins? "examples", arg then + say EXAMPLES + + elsif begins? "platforms", arg then + say PLATFORMS + + elsif options[:help] then + command = command_manager[options[:help]] + if command + # help with provided command + command.invoke("--help") + else + alert_error "Unknown command #{options[:help]}. Try 'gem help commands'" + end + + elsif arg then + possibilities = command_manager.find_command_possibilities(arg.downcase) + if possibilities.size == 1 + command = command_manager[possibilities.first] + command.invoke("--help") + elsif possibilities.size > 1 + alert_warning "Ambiguous command #{arg} (#{possibilities.join(', ')})" + else + alert_warning "Unknown command #{arg}. Try gem help commands" + end + + else + say Gem::Command::HELP + end + end + +end + diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb new file mode 100644 index 0000000000..4c67c0487b --- /dev/null +++ b/lib/rubygems/commands/install_command.rb @@ -0,0 +1,125 @@ +require 'rubygems/command' +require 'rubygems/doc_manager' +require 'rubygems/install_update_options' +require 'rubygems/dependency_installer' +require 'rubygems/local_remote_options' +require 'rubygems/validator' +require 'rubygems/version_option' + +class Gem::Commands::InstallCommand < Gem::Command + + include Gem::VersionOption + include Gem::LocalRemoteOptions + include Gem::InstallUpdateOptions + + def initialize + defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ + :generate_rdoc => true, + :generate_ri => true, + :install_dir => Gem.dir, + :test => false, + :version => Gem::Requirement.default, + }) + + super 'install', 'Install a gem into the local repository', defaults + + add_install_update_options + add_local_remote_options + add_platform_option + add_version_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to install" + end + + def defaults_str # :nodoc: + "--both --version '#{Gem::Requirement.default}' --rdoc --ri --no-force\n" \ + "--no-test --install-dir #{Gem.dir}" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...] [options] -- --build-flags" + end + + def execute + if options[:include_dependencies] then + alert "`gem install -y` is now default and will be removed" + alert "use --ignore-dependencies to install only the gems you list" + end + + installed_gems = [] + + ENV['GEM_PATH'] = options[:install_dir] # HACK what does this do? + + install_options = { + :env_shebang => options[:env_shebang], + :domain => options[:domain], + :force => options[:force], + :ignore_dependencies => options[:ignore_dependencies], + :install_dir => options[:install_dir], + :security_policy => options[:security_policy], + :wrappers => options[:wrappers], + } + + get_all_gem_names.each do |gem_name| + begin + inst = Gem::DependencyInstaller.new gem_name, options[:version], + install_options + inst.install + + inst.installed_gems.each do |spec| + say "Successfully installed #{spec.full_name}" + end + + installed_gems.push(*inst.installed_gems) + rescue Gem::InstallError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + rescue Gem::GemNotFoundException => e + alert_error e.message +# rescue => e +# # TODO: Fix this handle to allow the error to propagate to +# # the top level handler. Examine the other errors as +# # well. This implementation here looks suspicious to me -- +# # JimWeirich (4/Jan/05) +# alert_error "Error installing gem #{gem_name}: #{e.message}" +# return + end + end + + unless installed_gems.empty? then + gems = installed_gems.length == 1 ? 'gem' : 'gems' + say "#{installed_gems.length} #{gems} installed" + end + + # NOTE: *All* of the RI documents must be generated first. + # For some reason, RI docs cannot be generated after any RDoc + # documents are generated. + + if options[:generate_ri] then + installed_gems.each do |gem| + Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri + end + end + + if options[:generate_rdoc] then + installed_gems.each do |gem| + Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc + end + end + + if options[:test] then + installed_gems.each do |spec| + gem_spec = Gem::SourceIndex.from_installed_gems.search(spec.name, spec.version.version).first + result = Gem::Validator.new.unit_test(gem_spec) + if result and not result.passed? + unless ask_yes_no("...keep Gem?", true) then + Gem::Uninstaller.new(spec.name, :version => spec.version.version).uninstall + end + end + end + end + end + +end + diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb new file mode 100644 index 0000000000..e179ff57ee --- /dev/null +++ b/lib/rubygems/commands/list_command.rb @@ -0,0 +1,35 @@ +require 'rubygems/command' +require 'rubygems/commands/query_command' + +module Gem + module Commands + class ListCommand < QueryCommand + + def initialize + super( + 'list', + 'Display all gems whose name starts with STRING' + ) + remove_option('--name-matches') + end + + def arguments # :nodoc: + "STRING start of gem name to look for" + end + + def defaults_str # :nodoc: + "--local --no-details" + end + + def usage # :nodoc: + "#{program_name} [STRING]" + end + + def execute + string = get_one_optional_argument || '' + options[:name] = /^#{string}/i + super + end + end + end +end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb new file mode 100644 index 0000000000..3a3dcc0c6b --- /dev/null +++ b/lib/rubygems/commands/lock_command.rb @@ -0,0 +1,101 @@ +require 'rubygems/command' + +class Gem::Commands::LockCommand < Gem::Command + + def initialize + super 'lock', 'Generate a lockdown list of gems', + :strict => false + + add_option '-s', '--[no-]strict', + 'fail if unable to satisfy a dependency' do |strict, options| + options[:strict] = strict + end + end + + def arguments # :nodoc: + "GEMNAME name of gem to lock\nVERSION version of gem to lock" + end + + def defaults_str # :nodoc: + "--no-strict" + end + + def description # :nodoc: + <<-EOF +The lock command will generate a list of +gem+ statements that will lock down +the versions for the gem given in the command line. It will specify exact +versions in the requirements list to ensure that the gems loaded will always +be consistent. A full recursive search of all effected gems will be +generated. + +Example: + + gemlock rails-1.0.0 > lockdown.rb + +will produce in lockdown.rb: + + require "rubygems" + gem 'rails', '= 1.0.0' + gem 'rake', '= 0.7.0.1' + gem 'activesupport', '= 1.2.5' + gem 'activerecord', '= 1.13.2' + gem 'actionpack', '= 1.11.2' + gem 'actionmailer', '= 1.1.5' + gem 'actionwebservice', '= 1.0.0' + +Just load lockdown.rb from your application to ensure that the current +versions are loaded. Make sure that lockdown.rb is loaded *before* any +other require statements. + +Notice that rails 1.0.0 only requires that rake 0.6.2 or better be used. +Rake-0.7.0.1 is the most recent version installed that satisfies that, so we +lock it down to the exact version. + EOF + end + + def usage # :nodoc: + "#{program_name} GEMNAME-VERSION [GEMNAME-VERSION ...]" + end + + def complain(message) + if options.strict then + raise message + else + say "# #{message}" + end + end + + def execute + say 'require "rubygems"' + + locked = {} + + pending = options[:args] + + until pending.empty? do + full_name = pending.shift + + spec = Gem::SourceIndex.load_specification spec_path(full_name) + + say "gem '#{spec.name}', '= #{spec.version}'" unless locked[spec.name] + locked[spec.name] = true + + spec.dependencies.each do |dep| + next if locked[dep.name] + candidates = Gem.source_index.search dep.name, dep.requirement_list + + if candidates.empty? then + complain "Unable to satisfy '#{dep}' from currently installed gems." + else + pending << candidates.last.full_name + end + end + end + end + + def spec_path(gem_full_name) + File.join Gem.path, "specifications", "#{gem_full_name }.gemspec" + end + +end + diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb new file mode 100644 index 0000000000..74f6970e9e --- /dev/null +++ b/lib/rubygems/commands/mirror_command.rb @@ -0,0 +1,105 @@ +require 'yaml' +require 'zlib' + +require 'rubygems/command' +require 'rubygems/gem_open_uri' + +class Gem::Commands::MirrorCommand < Gem::Command + + def initialize + super 'mirror', 'Mirror a gem repository' + end + + def description # :nodoc: + <<-EOF +The mirror command uses the ~/.gemmirrorrc config file to mirror remote gem +repositories to a local path. The config file is a YAML document that looks +like this: + + --- + - from: http://gems.example.com # source repository URI + to: /path/to/mirror # destination directory + +Multiple sources and destinations may be specified. + EOF + end + + def execute + config_file = File.join Gem.user_home, '.gemmirrorrc' + + raise "Config file #{config_file} not found" unless File.exist? config_file + + mirrors = YAML.load_file config_file + + raise "Invalid config file #{config_file}" unless mirrors.respond_to? :each + + mirrors.each do |mir| + raise "mirror missing 'from' field" unless mir.has_key? 'from' + raise "mirror missing 'to' field" unless mir.has_key? 'to' + + get_from = mir['from'] + save_to = File.expand_path mir['to'] + + raise "Directory not found: #{save_to}" unless File.exist? save_to + raise "Not a directory: #{save_to}" unless File.directory? save_to + + gems_dir = File.join save_to, "gems" + + if File.exist? gems_dir then + raise "Not a directory: #{gems_dir}" unless File.directory? gems_dir + else + Dir.mkdir gems_dir + end + + sourceindex_data = '' + + say "fetching: #{get_from}/Marshal.#{Gem.marshal_version}.Z" + + get_from = URI.parse get_from + + if get_from.scheme.nil? then + get_from = get_from.to_s + elsif get_from.scheme == 'file' then + get_from = get_from.to_s[5..-1] + end + + open File.join(get_from, "Marshal.#{Gem.marshal_version}.Z"), "rb" do |y| + sourceindex_data = Zlib::Inflate.inflate y.read + open File.join(save_to, "Marshal.#{Gem.marshal_version}"), "wb" do |out| + out.write sourceindex_data + end + end + + sourceindex = Marshal.load(sourceindex_data) + + progress = ui.progress_reporter sourceindex.size, + "Fetching #{sourceindex.size} gems" + sourceindex.each do |fullname, gem| + gem_file = "#{fullname}.gem" + gem_dest = File.join gems_dir, gem_file + + unless File.exist? gem_dest then + begin + open "#{get_from}/gems/#{gem_file}", "rb" do |g| + contents = g.read + open gem_dest, "wb" do |out| + out.write contents + end + end + rescue + old_gf = gem_file + gem_file = gem_file.downcase + retry if old_gf != gem_file + alert_error $! + end + end + + progress.updated gem_file + end + + progress.done + end + end + +end + diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb new file mode 100644 index 0000000000..9c0062019b --- /dev/null +++ b/lib/rubygems/commands/outdated_command.rb @@ -0,0 +1,30 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' +require 'rubygems/version_option' + +class Gem::Commands::OutdatedCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'outdated', 'Display all gems that need updates' + + add_local_remote_options + add_platform_option + end + + def execute + locals = Gem::SourceIndex.from_installed_gems + + locals.outdated.sort.each do |name| + local = locals.search(/^#{name}$/).last + remotes = Gem::SourceInfoCache.search_with_source(/^#{name}$/, true) + remote = remotes.last.first + say "#{local.name} (#{local.version} < #{remote.version})" + end + end + +end + diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb new file mode 100644 index 0000000000..2900e7e739 --- /dev/null +++ b/lib/rubygems/commands/pristine_command.rb @@ -0,0 +1,133 @@ +require 'fileutils' +require 'rubygems/command' +require 'rubygems/format' +require 'rubygems/installer' +require 'rubygems/version_option' + +class Gem::Commands::PristineCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'pristine', + 'Restores installed gems to pristine condition from files located in the gem cache', + :version => Gem::Requirement.default + + add_option('--all', + 'Restore all installed gems to pristine', + 'condition') do |value, options| + options[:all] = value + end + + add_version_option('restore to', 'pristine condition') + end + + def arguments # :nodoc: + "GEMNAME gem to restore to pristine condition (unless --all)" + end + + def defaults_str # :nodoc: + "--all" + end + + def description # :nodoc: + <<-EOF +The pristine command compares the installed gems with the contents of the +cached gem and restores any files that don't match the cached gem's copy. + +If you have made modifications to your installed gems, the pristine command +will revert them. After all the gem's files have been checked all bin stubs +for the gem are regenerated. + +If the cached gem cannot be found, you will need to use `gem install` to +revert the gem. + EOF + end + + def usage # :nodoc: + "#{program_name} [args]" + end + + def execute + gem_name = nil + + specs = if options[:all] then + Gem::SourceIndex.from_installed_gems.map do |name, spec| + spec + end + else + gem_name = get_one_gem_name + Gem::SourceIndex.from_installed_gems.search(gem_name, + options[:version]) + end + + if specs.empty? then + raise Gem::Exception, + "Failed to find gem #{gem_name} #{options[:version]}" + end + + install_dir = Gem.dir # TODO use installer option + + raise Gem::FilePermissionError.new(install_dir) unless + File.writable?(install_dir) + + say "Restoring gem(s) to pristine condition..." + + specs.each do |spec| + gem = Dir[File.join(Gem.dir, 'cache', "#{spec.full_name}.gem")].first + + if gem.nil? then + alert_error "Cached gem for #{spec.full_name} not found, use `gem install` to restore" + next + end + + # TODO use installer options + installer = Gem::Installer.new gem, :wrappers => true + + gem_file = File.join install_dir, "cache", "#{spec.full_name}.gem" + + security_policy = nil # TODO use installer option + + format = Gem::Format.from_file_by_path gem_file, security_policy + + target_directory = File.join(install_dir, "gems", format.spec.full_name) + target_directory.untaint + + pristine_files = format.file_entries.collect { |data| data[0]["path"] } + file_map = {} + + format.file_entries.each do |entry, file_data| + file_map[entry["path"]] = file_data + end + + Dir.chdir target_directory do + deployed_files = Dir.glob(File.join("**", "*")) + + Dir.glob(File.join("**", ".*")) + + pristine_files = pristine_files.map { |f| File.expand_path f } + deployed_files = deployed_files.map { |f| File.expand_path f } + + to_redeploy = (pristine_files - deployed_files) + to_redeploy = to_redeploy.map { |path| path.untaint} + + if to_redeploy.length > 0 then + say "Restoring #{to_redeploy.length} file#{to_redeploy.length == 1 ? "" : "s"} to #{spec.full_name}..." + + to_redeploy.each do |path| + say " #{path}" + FileUtils.mkdir_p File.dirname(path) + File.open(path, "wb") do |out| + out.write file_map[path] + end + end + else + say "#{spec.full_name} is in pristine condition" + end + end + + installer.generate_bin + end + end + +end + diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb new file mode 100644 index 0000000000..581d4bb734 --- /dev/null +++ b/lib/rubygems/commands/query_command.rb @@ -0,0 +1,118 @@ +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' + +class Gem::Commands::QueryCommand < Gem::Command + + include Gem::LocalRemoteOptions + + def initialize(name = 'query', + summary = 'Query gem information in local or remote repositories') + super name, summary, + :name => /.*/, :domain => :local, :details => false, :versions => true + + add_option('-n', '--name-matches REGEXP', + 'Name of gem(s) to query on matches the', + 'provided REGEXP') do |value, options| + options[:name] = /#{value}/i + end + + add_option('-d', '--[no-]details', + 'Display detailed information of gem(s)') do |value, options| + options[:details] = value + end + + add_option( '--[no-]versions', + 'Display only gem names') do |value, options| + options[:versions] = value + options[:details] = false unless value + end + + add_local_remote_options + end + + def defaults_str # :nodoc: + "--local --name-matches '.*' --no-details --versions" + end + + def execute + name = options[:name] + + if local? then + say + say "*** LOCAL GEMS ***" + say + output_query_results Gem.cache.search(name) + end + + if remote? then + say + say "*** REMOTE GEMS ***" + say + output_query_results Gem::SourceInfoCache.search(name) + end + end + + private + + def output_query_results(gemspecs) + output = [] + gem_list_with_version = {} + + gemspecs.flatten.each do |gemspec| + gem_list_with_version[gemspec.name] ||= [] + gem_list_with_version[gemspec.name] << gemspec + end + + gem_list_with_version = gem_list_with_version.sort_by do |name, spec| + name.downcase + end + + gem_list_with_version.each do |gem_name, list_of_matching| + list_of_matching = list_of_matching.sort_by { |x| x.version.to_ints }.reverse + seen_versions = {} + + list_of_matching.delete_if do |item| + if seen_versions[item.version] then + true + else + seen_versions[item.version] = true + false + end + end + + entry = gem_name.dup + if options[:versions] then + entry << " (#{list_of_matching.map{|gem| gem.version.to_s}.join(", ")})" + end + + entry << "\n" << format_text(list_of_matching[0].summary, 68, 4) if + options[:details] + output << entry + end + + say output.join(options[:details] ? "\n\n" : "\n") + end + + ## + # Used for wrapping and indenting text + # + def format_text(text, wrap, indent=0) + result = [] + work = text.dup + + while work.length > wrap + if work =~ /^(.{0,#{wrap}})[ \n]/o then + result << $1 + work.slice!(0, $&.length) + else + result << work.slice!(0, wrap) + end + end + + result << work if work.length.nonzero? + result.join("\n").gsub(/^/, " " * indent) + end + +end + diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb new file mode 100644 index 0000000000..f2e677c115 --- /dev/null +++ b/lib/rubygems/commands/rdoc_command.rb @@ -0,0 +1,78 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/doc_manager' + +module Gem + module Commands + class RdocCommand < Command + include VersionOption + + def initialize + super('rdoc', + 'Generates RDoc for pre-installed gems', + { + :version => Gem::Requirement.default, + :include_rdoc => true, + :include_ri => true, + }) + add_option('--all', + 'Generate RDoc/RI documentation for all', + 'installed gems') do |value, options| + options[:all] = value + end + add_option('--[no-]rdoc', + 'Include RDoc generated documents') do + |value, options| + options[:include_rdoc] = value + end + add_option('--[no-]ri', + 'Include RI generated documents' + ) do |value, options| + options[:include_ri] = value + end + add_version_option + end + + def arguments # :nodoc: + "GEMNAME gem to generate documentation for (unless --all)" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}' --rdoc --ri" + end + + def usage # :nodoc: + "#{program_name} [args]" + end + + def execute + if options[:all] + specs = Gem::SourceIndex.from_installed_gems.collect { |name, spec| + spec + } + else + gem_name = get_one_gem_name + specs = Gem::SourceIndex.from_installed_gems.search( + gem_name, options[:version]) + end + + if specs.empty? + fail "Failed to find gem #{gem_name} to generate RDoc for #{options[:version]}" + end + if options[:include_ri] + specs.each do |spec| + Gem::DocManager.new(spec).generate_ri + end + end + if options[:include_rdoc] + specs.each do |spec| + Gem::DocManager.new(spec).generate_rdoc + end + end + + true + end + end + + end +end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb new file mode 100644 index 0000000000..96da19c0f7 --- /dev/null +++ b/lib/rubygems/commands/search_command.rb @@ -0,0 +1,37 @@ +require 'rubygems/command' +require 'rubygems/commands/query_command' + +module Gem + module Commands + + class SearchCommand < QueryCommand + + def initialize + super( + 'search', + 'Display all gems whose name contains STRING' + ) + remove_option('--name-matches') + end + + def arguments # :nodoc: + "STRING fragment of gem name to search for" + end + + def defaults_str # :nodoc: + "--local --no-details" + end + + def usage # :nodoc: + "#{program_name} [STRING]" + end + + def execute + string = get_one_optional_argument + options[:name] = /#{string}/i + super + end + end + + end +end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb new file mode 100644 index 0000000000..34e5e46fec --- /dev/null +++ b/lib/rubygems/commands/server_command.rb @@ -0,0 +1,48 @@ +require 'rubygems/command' +require 'rubygems/server' + +class Gem::Commands::ServerCommand < Gem::Command + + def initialize + super 'server', 'Documentation and gem repository HTTP server', + :port => 8808, :gemdir => Gem.dir, :daemon => false + + add_option '-p', '--port=PORT', + 'port to listen on' do |port, options| + options[:port] = port + end + + add_option '-d', '--dir=GEMDIR', + 'directory from which to serve gems' do |gemdir, options| + options[:gemdir] = gemdir + end + + add_option '--[no]-daemon', 'run as a daemon' do |daemon, options| + options[:daemon] = daemon + end + end + + def defaults_str # :nodoc: + "--port 8808 --dir #{Gem.dir} --no-daemon" + end + + def description # :nodoc: + <<-EOF +The server command starts up a web server that hosts the RDoc for your +installed gems and can operate as a server for installation of gems on other +machines. + +The cache files for installed gems must exist to use the server as a source +for gem installation. + +To install gems from a running server, use `gem install GEMNAME --source +http://gem_server_host:8808` + EOF + end + + def execute + Gem::Server.run options + end + +end + diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb new file mode 100644 index 0000000000..def9c01a3f --- /dev/null +++ b/lib/rubygems/commands/sources_command.rb @@ -0,0 +1,115 @@ +require 'rubygems/command' +require 'rubygems/remote_fetcher' +require 'rubygems/source_info_cache' +require 'rubygems/source_info_cache_entry' + +class Gem::Commands::SourcesCommand < Gem::Command + + def initialize + super 'sources', + 'Manage the sources and cache file RubyGems uses to search for gems' + + add_option '-a', '--add SOURCE_URI', 'Add source' do |value, options| + options[:add] = value + end + + add_option '-l', '--list', 'List sources' do |value, options| + options[:list] = value + end + + add_option '-r', '--remove SOURCE_URI', 'Remove source' do |value, options| + options[:remove] = value + end + + add_option '-u', '--update', 'Update source cache' do |value, options| + options[:update] = value + end + + add_option '-c', '--clear-all', + 'Remove all sources (clear the cache)' do |value, options| + options[:clear_all] = value + end + end + + def defaults_str + '--list' + end + + def execute + options[:list] = !(options[:add] || options[:remove] || options[:clear_all] || options[:update]) + + if options[:clear_all] then + remove_cache_file("user", Gem::SourceInfoCache.user_cache_file) + remove_cache_file("system", Gem::SourceInfoCache.system_cache_file) + end + + if options[:add] then + source_uri = options[:add] + + sice = Gem::SourceInfoCacheEntry.new nil, nil + begin + sice.refresh source_uri + + Gem::SourceInfoCache.cache_data[source_uri] = sice + Gem::SourceInfoCache.cache.update + Gem::SourceInfoCache.cache.flush + + Gem.sources << source_uri + Gem.configuration.write + + say "#{source_uri} added to sources" + rescue URI::Error, ArgumentError + say "#{source_uri} is not a URI" + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{source_uri}:\n\t#{e.message}" + end + end + + if options[:update] then + Gem::SourceInfoCache.cache.refresh + Gem::SourceInfoCache.cache.flush + + say "source cache successfully updated" + end + + if options[:remove] then + source_uri = options[:remove] + + unless Gem.sources.include? source_uri then + say "source #{source_uri} not present in cache" + else + Gem::SourceInfoCache.cache_data.delete source_uri + Gem::SourceInfoCache.cache.update + Gem::SourceInfoCache.cache.flush + Gem.sources.delete source_uri + Gem.configuration.write + + say "#{source_uri} removed from sources" + end + end + + if options[:list] then + say "*** CURRENT SOURCES ***" + say + + Gem.sources.each do |source_uri| + say source_uri + end + end + end + + private + + def remove_cache_file(desc, fn) + FileUtils.rm_rf fn rescue nil + if ! File.exist?(fn) + say "*** Removed #{desc} source cache ***" + elsif ! File.writable?(fn) + say "*** Unable to remove #{desc} source cache (write protected) ***" + else + say "*** Unable to remove #{desc} source cache ***" + end + end + +end + diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb new file mode 100644 index 0000000000..954b38ac37 --- /dev/null +++ b/lib/rubygems/commands/specification_command.rb @@ -0,0 +1,72 @@ +require 'yaml' +require 'rubygems/command' +require 'rubygems/local_remote_options' +require 'rubygems/version_option' +require 'rubygems/source_info_cache' + +class Gem::Commands::SpecificationCommand < Gem::Command + + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super 'specification', 'Display gem specification (in yaml)', + :domain => :local, :version => Gem::Requirement.default + + add_version_option('examine') + add_platform_option + + add_option('--all', 'Output specifications for all versions of', + 'the gem') do |value, options| + options[:all] = true + end + + add_local_remote_options + end + + def arguments # :nodoc: + "GEMFILE name of gem to show the gemspec for" + end + + def defaults_str # :nodoc: + "--local --version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} [GEMFILE]" + end + + def execute + specs = [] + gem = get_one_gem_name + + if local? then + source_index = Gem::SourceIndex.from_installed_gems + specs.push(*source_index.search(/\A#{gem}\z/, options[:version])) + end + + if remote? then + alert_warning "Remote information is not complete\n\n" + + Gem::SourceInfoCache.cache_data.each do |_,sice| + specs.push(*sice.source_index.search(gem, options[:version])) + end + end + + if specs.empty? then + alert_error "Unknown gem '#{gem}'" + terminate_interaction 1 + end + + output = lambda { |spec| say spec.to_yaml; say "\n" } + + if options[:all] then + specs.each(&output) + else + spec = specs.sort_by { |spec| spec.version }.last + output[spec] + end + end + +end + diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb new file mode 100644 index 0000000000..7d2908836c --- /dev/null +++ b/lib/rubygems/commands/uninstall_command.rb @@ -0,0 +1,56 @@ +require 'rubygems/command' +require 'rubygems/version_option' +require 'rubygems/uninstaller' + +module Gem + module Commands + class UninstallCommand < Command + + include VersionOption + + def initialize + super 'uninstall', 'Uninstall gems from the local repository', + :version => Gem::Requirement.default + + add_option('-a', '--[no-]all', + 'Uninstall all matching versions' + ) do |value, options| + options[:all] = value + end + + add_option('-i', '--[no-]ignore-dependencies', + 'Ignore dependency requirements while', + 'uninstalling') do |value, options| + options[:ignore] = value + end + + add_option('-x', '--[no-]executables', + 'Uninstall applicable executables without', + 'confirmation') do |value, options| + options[:executables] = value + end + + add_version_option + add_platform_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to uninstall" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}' --no-force" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + get_all_gem_names.each do |gem_name| + Gem::Uninstaller.new(gem_name, options).uninstall + end + end + end + end +end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb new file mode 100644 index 0000000000..ece24745a2 --- /dev/null +++ b/lib/rubygems/commands/unpack_command.rb @@ -0,0 +1,76 @@ +require 'fileutils' +require 'rubygems/command' +require 'rubygems/installer' +require 'rubygems/version_option' + +class Gem::Commands::UnpackCommand < Gem::Command + + include Gem::VersionOption + + def initialize + super 'unpack', 'Unpack an installed gem to the current directory', + :version => Gem::Requirement.default + add_version_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to unpack" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}'" + end + + def usage # :nodoc: + "#{program_name} GEMNAME" + end + + #-- + # TODO: allow, e.g., 'gem unpack rake-0.3.1'. Find a general solution for + # this, so that it works for uninstall as well. (And check other commands + # at the same time.) + def execute + gemname = get_one_gem_name + path = get_path(gemname, options[:version]) + if path + target_dir = File.basename(path).sub(/\.gem$/, '') + FileUtils.mkdir_p target_dir + Gem::Installer.new(path).unpack(File.expand_path(target_dir)) + say "Unpacked gem: '#{target_dir}'" + else + alert_error "Gem '#{gemname}' not installed." + end + end + + # Return the full path to the cached gem file matching the given + # name and version requirement. Returns 'nil' if no match. + # + # Example: + # + # get_path('rake', '> 0.4') # -> '/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem' + # get_path('rake', '< 0.1') # -> nil + # get_path('rak') # -> nil (exact name required) + #-- + # TODO: This should be refactored so that it's a general service. I don't + # think any of our existing classes are the right place though. Just maybe + # 'Cache'? + # + # TODO: It just uses Gem.dir for now. What's an easy way to get the list of + # source directories? + def get_path(gemname, version_req) + return gemname if gemname =~ /\.gem$/i + specs = Gem::SourceIndex.from_installed_gems.search(/\A#{gemname}\z/, version_req) + selected = specs.sort_by { |s| s.version }.last + return nil if selected.nil? + # We expect to find (basename).gem in the 'cache' directory. + # Furthermore, the name match must be exact (ignoring case). + if gemname =~ /^#{selected.name}$/i + filename = selected.full_name + '.gem' + return File.join(Gem.dir, 'cache', filename) + else + return nil + end + end + +end + diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb new file mode 100644 index 0000000000..e17ba2516a --- /dev/null +++ b/lib/rubygems/commands/update_command.rb @@ -0,0 +1,149 @@ +require 'rubygems/command' +require 'rubygems/install_update_options' +require 'rubygems/local_remote_options' +require 'rubygems/source_info_cache' +require 'rubygems/version_option' + +module Gem + module Commands + class UpdateCommand < Command + + include Gem::InstallUpdateOptions + include Gem::LocalRemoteOptions + include Gem::VersionOption + + def initialize + super( + 'update', + 'Update the named gems (or all installed gems) in the local repository', + { + :generate_rdoc => true, + :generate_ri => true, + :force => false, + :test => false, + :install_dir => Gem.dir + }) + + add_install_update_options + + add_option('--system', + 'Update the RubyGems system software') do |value, options| + options[:system] = value + end + + add_local_remote_options + + add_platform_option + end + + def arguments # :nodoc: + "GEMNAME name of gem to update" + end + + def defaults_str # :nodoc: + "--rdoc --ri --no-force --no-test\n" + + "--install-dir #{Gem.dir}" + end + + def usage # :nodoc: + "#{program_name} GEMNAME [GEMNAME ...]" + end + + def execute + if options[:system] then + say "Updating RubyGems..." + + unless options[:args].empty? then + fail "No gem names are allowed with the --system option" + end + + options[:args] = ["rubygems-update"] + else + say "Updating installed gems..." + end + + hig = highest_installed_gems = {} + + Gem::SourceIndex.from_installed_gems.each do |name, spec| + if hig[spec.name].nil? or hig[spec.name].version < spec.version + hig[spec.name] = spec + end + end + + remote_gemspecs = Gem::SourceInfoCache.search(//) + + gems_to_update = if options[:args].empty? then + which_to_update(highest_installed_gems, remote_gemspecs) + else + options[:args] + end + + options[:domain] = :remote # install from remote source + + # HACK use the real API + install_command = Gem::CommandManager.instance['install'] + + gems_to_update.uniq.sort.each do |name| + say "Attempting remote update of #{name}" + options[:args] = [name] + options[:ignore_dependencies] = true # HACK skip seen gems instead + install_command.merge_options(options) + install_command.execute + end + + if gems_to_update.include?("rubygems-update") then + latest_ruby_gem = remote_gemspecs.select { |s| + s.name == 'rubygems-update' + }.sort_by { |s| + s.version + }.last + + say "Updating version of RubyGems to #{latest_ruby_gem.version}" + installed = do_rubygems_update(latest_ruby_gem.version.to_s) + + say "RubyGems system software updated" if installed + else + say "Gems: [#{gems_to_update.uniq.sort.collect{|g| g.to_s}.join(', ')}] updated" + end + end + + def do_rubygems_update(version_string) + args = [] + args.push '--prefix', Gem.prefix unless Gem.prefix.nil? + args << '--no-rdoc' unless options[:generate_rdoc] + args << '--no-ri' unless options[:generate_ri] + + update_dir = File.join(Gem.dir, 'gems', + "rubygems-update-#{version_string}") + + success = false + + Dir.chdir update_dir do + say "Installing RubyGems #{version_string}" + setup_cmd = "#{Gem.ruby} setup.rb #{args.join ' '}" + + # Make sure old rubygems isn't loaded + if Gem.win_platform? then + system "set RUBYOPT= & #{setup_cmd}" + else + system "RUBYOPT=\"\" #{setup_cmd}" + end + end + end + + def which_to_update(highest_installed_gems, remote_gemspecs) + result = [] + highest_installed_gems.each do |l_name, l_spec| + highest_remote_gem = + remote_gemspecs.select { |spec| spec.name == l_name }. + sort_by { |spec| spec.version }. + last + if highest_remote_gem and l_spec.version < highest_remote_gem.version + result << l_name + end + end + result + end + end + end +end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb new file mode 100644 index 0000000000..b42244ce7d --- /dev/null +++ b/lib/rubygems/commands/which_command.rb @@ -0,0 +1,86 @@ +require 'rubygems/command' +require 'rubygems/gem_path_searcher' + +class Gem::Commands::WhichCommand < Gem::Command + + EXT = %w[.rb .rbw .so .dll] # HACK + + def initialize + super 'which', 'Find the location of a library', + :search_gems_first => false, :show_all => false + + add_option '-a', '--[no-]all', 'show all matching files' do |show_all, options| + options[:show_all] = show_all + end + + add_option '-g', '--[no-]gems-first', + 'search gems before non-gems' do |gems_first, options| + options[:search_gems_first] = gems_first + end + end + + def arguments # :nodoc: + "FILE name of file to find" + end + + def defaults_str # :nodoc: + "--no-gems-first --no-all" + end + + def usage # :nodoc: + "#{program_name} FILE [FILE ...]" + end + + def execute + searcher = Gem::GemPathSearcher.new + + options[:args].each do |arg| + dirs = $LOAD_PATH + spec = searcher.find arg + + if spec then + if options[:search_gems_first] then + dirs = gem_paths(spec) + $LOAD_PATH + else + dirs = $LOAD_PATH + gem_paths(spec) + end + + say "(checking gem #{spec.full_name} for #{arg})" if + Gem.configuration.verbose + end + + paths = find_paths arg, dirs + + if paths.empty? then + say "Can't find #{arg}" + else + say paths + end + end + end + + def find_paths(package_name, dirs) + result = [] + + dirs.each do |dir| + EXT.each do |ext| + full_path = File.join dir, "#{package_name}#{ext}" + if File.exist? full_path then + result << full_path + return result unless options[:show_all] + end + end + end + + result + end + + def gem_paths(spec) + spec.require_paths.collect { |d| File.join spec.full_gem_path, d } + end + + def usage # :nodoc: + "#{program_name} FILE [...]" + end + +end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb new file mode 100644 index 0000000000..5bca0bd14e --- /dev/null +++ b/lib/rubygems/config_file.rb @@ -0,0 +1,224 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'yaml' +require 'rubygems' + +# Store the gem command options specified in the configuration file. The +# config file object acts much like a hash. + +class Gem::ConfigFile + + DEFAULT_BACKTRACE = false + DEFAULT_BENCHMARK = false + DEFAULT_BULK_THRESHOLD = 1000 + DEFAULT_VERBOSITY = true + DEFAULT_UPDATE_SOURCES = true + + # List of arguments supplied to the config file object. + attr_reader :args + + # True if we print backtraces on errors. + attr_writer :backtrace + + # True if we are benchmarking this run. + attr_accessor :benchmark + + # Bulk threshold value. If the number of missing gems are above + # this threshold value, then a bulk download technique is used. + attr_accessor :bulk_threshold + + # Verbose level of output: + # * false -- No output + # * true -- Normal output + # * :loud -- Extra output + attr_accessor :verbose + + # True if we want to update the SourceInfoCache every time, false otherwise + attr_accessor :update_sources + + # Create the config file object. +args+ is the list of arguments + # from the command line. + # + # The following command line options are handled early here rather + # than later at the time most command options are processed. + # + # * --config-file and --config-file==NAME -- Obviously these need + # to be handled by the ConfigFile object to ensure we get the + # right config file. + # + # * --backtrace -- Backtrace needs to be turned on early so that + # errors before normal option parsing can be properly handled. + # + # * --debug -- Enable Ruby level debug messages. Handled early + # for the same reason as --backtrace. + # + def initialize(arg_list) + @config_file_name = nil + need_config_file_name = false + + arg_list = arg_list.map do |arg| + if need_config_file_name then + @config_file_name = arg + nil + elsif arg =~ /^--config-file=(.*)/ then + @config_file_name = $1 + nil + elsif arg =~ /^--config-file$/ then + need_config_file_name = true + nil + else + arg + end + end.compact + + @backtrace = DEFAULT_BACKTRACE + @benchmark = DEFAULT_BENCHMARK + @bulk_threshold = DEFAULT_BULK_THRESHOLD + @verbose = DEFAULT_VERBOSITY + @update_sources = DEFAULT_UPDATE_SOURCES + + begin + # HACK $SAFE ok? + @hash = open(config_file_name.dup.untaint) {|f| YAML.load(f) } + rescue ArgumentError + warn "Failed to load #{config_file_name}" + rescue Errno::ENOENT + # Ignore missing config file error. + rescue Errno::EACCES + warn "Failed to load #{config_file_name} due to permissions problem." + end + + @hash ||= {} + + # HACK these override command-line args, which is bad + @backtrace = @hash[:backtrace] if @hash.key? :backtrace + @benchmark = @hash[:benchmark] if @hash.key? :benchmark + @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold + Gem.sources.replace @hash[:sources] if @hash.key? :sources + @verbose = @hash[:verbose] if @hash.key? :verbose + @update_sources = @hash[:update_sources] if @hash.key? :update_sources + + handle_arguments arg_list + end + + # True if the backtrace option has been specified, or debug is on. + def backtrace + @backtrace or $DEBUG + end + + # The name of the configuration file. + def config_file_name + @config_file_name || Gem.config_file + end + + # Delegates to @hash + def each(&block) + hash = @hash.dup + hash.delete :update_sources + hash.delete :verbose + hash.delete :benchmark + hash.delete :backtrace + hash.delete :bulk_threshold + + yield :update_sources, @update_sources + yield :verbose, @verbose + yield :benchmark, @benchmark + yield :backtrace, @backtrace + yield :bulk_threshold, @bulk_threshold + + yield 'config_file_name', @config_file_name if @config_file_name + + hash.each(&block) + end + + # Handle the command arguments. + def handle_arguments(arg_list) + @args = [] + + arg_list.each do |arg| + case arg + when /^--(backtrace|traceback)$/ then + @backtrace = true + when /^--bench(mark)?$/ then + @benchmark = true + when /^--debug$/ then + $DEBUG = true + else + @args << arg + end + end + end + + # Really verbose mode gives you extra output. + def really_verbose + case verbose + when true, false, nil then false + else true + end + end + + # to_yaml only overwrites things you can't override on the command line. + def to_yaml # :nodoc: + yaml_hash = {} + yaml_hash[:backtrace] = @hash.key?(:backtrace) ? @hash[:backtrace] : + DEFAULT_BACKTRACE + yaml_hash[:benchmark] = @hash.key?(:benchmark) ? @hash[:benchmark] : + DEFAULT_BENCHMARK + yaml_hash[:bulk_threshold] = @hash.key?(:bulk_threshold) ? + @hash[:bulk_threshold] : DEFAULT_BULK_THRESHOLD + yaml_hash[:sources] = Gem.sources + yaml_hash[:update_sources] = @hash.key?(:update_sources) ? + @hash[:update_sources] : DEFAULT_UPDATE_SOURCES + yaml_hash[:verbose] = @hash.key?(:verbose) ? @hash[:verbose] : + DEFAULT_VERBOSITY + + keys = yaml_hash.keys.map { |key| key.to_s } + keys << 'debug' + re = Regexp.union(*keys) + + @hash.each do |key, value| + key = key.to_s + next if key =~ re + yaml_hash[key.to_s] = value + end + + yaml_hash.to_yaml + end + + # Writes out this config file, replacing its source. + def write + File.open config_file_name, 'w' do |fp| + fp.write self.to_yaml + end + end + + # Return the configuration information for +key+. + def [](key) + @hash[key.to_s] + end + + # Set configuration option +key+ to +value+. + def []=(key, value) + @hash[key.to_s] = value + end + + def ==(other) # :nodoc: + self.class === other and + @backtrace == other.backtrace and + @benchmark == other.benchmark and + @bulk_threshold == other.bulk_threshold and + @verbose == other.verbose and + @update_sources == other.update_sources and + @hash == other.hash + end + + protected + + attr_reader :hash + +end + diff --git a/lib/rubygems/custom_require.rb b/lib/rubygems/custom_require.rb new file mode 100755 index 0000000000..598ec3ef98 --- /dev/null +++ b/lib/rubygems/custom_require.rb @@ -0,0 +1,38 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +module Kernel + alias gem_original_require require # :nodoc: + + # + # We replace Ruby's require with our own, which is capable of + # loading gems on demand. + # + # When you call <tt>require 'x'</tt>, this is what happens: + # * If the file can be loaded from the existing Ruby loadpath, it + # is. + # * Otherwise, installed gems are searched for a file that matches. + # If it's found in gem 'y', that gem is activated (added to the + # loadpath). + # + # The normal <tt>require</tt> functionality of returning false if + # that file has already been loaded is preserved. + # + def require(path) # :nodoc: + gem_original_require path + rescue LoadError => load_error + if load_error.message =~ /\A[Nn]o such file to load -- #{Regexp.escape path}\z/ and + spec = Gem.searcher.find(path) then + Gem.activate(spec.name, false, "= #{spec.version}") + gem_original_require path + else + raise load_error + end + end +end # module Kernel + diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb new file mode 100644 index 0000000000..be731d564e --- /dev/null +++ b/lib/rubygems/dependency.rb @@ -0,0 +1,65 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# The Dependency class holds a Gem name and a Gem::Requirement +class Gem::Dependency + + attr_accessor :name + + attr_writer :version_requirements + + def <=>(other) + [@name] <=> [other.name] + end + + ## + # Constructs the dependency + # + # name:: [String] name of the Gem + # version_requirements:: [String Array] version requirement (e.g. ["> 1.2"]) + # + def initialize(name, version_requirements) + @name = name + @version_requirements = Gem::Requirement.create version_requirements + @version_requirement = nil # Avoid warnings. + end + + def version_requirements + normalize if defined? @version_requirement and @version_requirement + @version_requirements + end + + def requirement_list + version_requirements.as_list + end + + alias requirements_list requirement_list + + def normalize + ver = @version_requirement.instance_eval { @version } + @version_requirements = Gem::Requirement.new([ver]) + @version_requirement = nil + end + + def to_s # :nodoc: + "#{name} (#{version_requirements})" + end + + def ==(other) # :nodoc: + self.class === other && + self.name == other.name && + self.version_requirements == other.version_requirements + end + + def hash + name.hash + version_requirements.hash + end + +end + diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb new file mode 100644 index 0000000000..49afc76c79 --- /dev/null +++ b/lib/rubygems/dependency_installer.rb @@ -0,0 +1,219 @@ +require 'rubygems' +require 'rubygems/dependency_list' +require 'rubygems/installer' +require 'rubygems/source_info_cache' +require 'rubygems/user_interaction' + +class Gem::DependencyInstaller + + include Gem::UserInteraction + + attr_reader :gems_to_install + attr_reader :installed_gems + + DEFAULT_OPTIONS = { + :env_shebang => false, + :domain => :both, # HACK dup + :force => false, + :ignore_dependencies => false, + :security_policy => Gem::Security::NoSecurity, # HACK AlmostNo? Low? + :wrappers => true + } + + ## + # Creates a new installer instance that will install +gem_name+ using + # version requirement +version+ and +options+. + # + # Options are: + # :env_shebang:: See Gem::Installer::new. + # :domain:: :local, :remote, or :both. :local only searches gems in the + # current directory. :remote searches only gems in Gem::sources. + # :both searches both. + # :force:: See Gem::Installer#install. + # :ignore_dependencies: Don't install any dependencies. + # :install_dir: See Gem::Installer#install. + # :security_policy: See Gem::Installer::new and Gem::Security. + # :wrappers: See Gem::Installer::new + def initialize(gem_name, version = nil, options = {}) + options = DEFAULT_OPTIONS.merge options + @env_shebang = options[:env_shebang] + @domain = options[:domain] + @force = options[:force] + @ignore_dependencies = options[:ignore_dependencies] + @install_dir = options[:install_dir] || Gem.dir + @security_policy = options[:security_policy] + @wrappers = options[:wrappers] + + @installed_gems = [] + + spec_and_source = nil + + local_gems = Dir["#{gem_name}*"].sort.reverse + unless local_gems.empty? then + local_gems.each do |gem_file| + next unless gem_file =~ /gem$/ + begin + spec = Gem::Format.from_file_by_path(gem_file).spec + spec_and_source = [spec, gem_file] + break + rescue SystemCallError, Gem::Package::FormatError + end + end + end + + if spec_and_source.nil? then + version ||= Gem::Requirement.default + @dep = Gem::Dependency.new gem_name, version + spec_and_sources = find_gems_with_sources(@dep).reverse + + spec_and_source = spec_and_sources.find do |spec, source| + Gem::Platform.match spec.platform + end + end + + if spec_and_source.nil? then + raise Gem::GemNotFoundException, + "could not find #{gem_name} locally or in a repository" + end + + @specs_and_sources = [spec_and_source] + + gather_dependencies + end + + ## + # Returns a list of pairs of gemspecs and source_uris that match + # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) + # sources. Gems are sorted with newer gems prefered over older gems, and + # local gems prefered over remote gems. + def find_gems_with_sources(dep) + gems_and_sources = [] + + if @domain == :both or @domain == :local then + Dir[File.join(Dir.pwd, "#{dep.name}-[0-9]*.gem")].each do |gem_file| + spec = Gem::Format.from_file_by_path(gem_file).spec + gems_and_sources << [spec, gem_file] if spec.name == dep.name + end + end + + if @domain == :both or @domain == :remote then + gems_and_sources.push(*Gem::SourceInfoCache.search_with_source(dep, true)) + end + + gems_and_sources.sort_by do |gem, source| + [gem, source !~ /^http:\/\// ? 1 : 0] # local gems win + end + end + + ## + # Moves the gem +spec+ from +source_uri+ to the cache dir unless it is + # already there. If the source_uri is local the gem cache dir copy is + # always replaced. + def download(spec, source_uri) + gem_file_name = "#{spec.full_name}.gem" + local_gem_path = File.join @install_dir, 'cache', gem_file_name + + Gem.ensure_gem_subdirectories @install_dir + + source_uri = URI.parse source_uri unless URI::Generic === source_uri + scheme = source_uri.scheme + + # URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if scheme =~ /^[a-z]$/i + + case scheme + when 'http' then + unless File.exist? local_gem_path then + say "Downloading gem #{gem_file_name}" if + Gem.configuration.really_verbose + + remote_gem_path = source_uri + "gems/#{gem_file_name}" + + gem = Gem::RemoteFetcher.fetcher.fetch_path remote_gem_path + + File.open local_gem_path, 'wb' do |fp| + fp.write gem + end + end + when nil, 'file' then # TODO test for local overriding cache + begin + FileUtils.cp source_uri.to_s, local_gem_path + rescue Errno::EACCES + local_gem_path = source_uri.to_s + end + + say "Using local gem #{local_gem_path}" if + Gem.configuration.really_verbose + else + raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}" + end + + local_gem_path + end + + ## + # Gathers all dependencies necessary for the installation from local and + # remote sources unless the ignore_dependencies was given. + def gather_dependencies + specs = @specs_and_sources.map { |spec,_| spec } + + dependency_list = Gem::DependencyList.new + dependency_list.add(*specs) + + unless @ignore_dependencies then + to_do = specs.dup + seen = {} + + until to_do.empty? do + spec = to_do.shift + next if spec.nil? or seen[spec.name] + seen[spec.name] = true + + spec.dependencies.each do |dep| + results = find_gems_with_sources(dep).reverse # local gems first + + results.each do |dep_spec, source_uri| + next if seen[dep_spec.name] + @specs_and_sources << [dep_spec, source_uri] + dependency_list.add dep_spec + to_do.push dep_spec + end + end + end + end + + @gems_to_install = dependency_list.dependency_order.reverse + end + + ## + # Installs the gem and all its dependencies. + def install + spec_dir = File.join @install_dir, 'specifications' + source_index = Gem::SourceIndex.from_gems_in spec_dir + + @gems_to_install.each do |spec| + last = spec == @gems_to_install.last + # HACK is this test for full_name acceptable? + next if source_index.any? { |n,_| n == spec.full_name } and not last + + say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose + + _, source_uri = @specs_and_sources.assoc spec + local_gem_path = download spec, source_uri + + inst = Gem::Installer.new local_gem_path, + :env_shebang => @env_shebang, + :force => @force, + :ignore_dependencies => @ignore_dependencies, + :install_dir => @install_dir, + :security_policy => @security_policy, + :wrappers => @wrappers + + spec = inst.install + + @installed_gems << spec + end + end + +end + diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb new file mode 100644 index 0000000000..81aa65bfb2 --- /dev/null +++ b/lib/rubygems/dependency_list.rb @@ -0,0 +1,165 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'tsort' + +class Gem::DependencyList + + include TSort + + def self.from_source_index(src_index) + deps = new + + src_index.each do |full_name, spec| + deps.add spec + end + + deps + end + + def initialize + @specs = [] + end + + # Adds +gemspecs+ to the dependency list. + def add(*gemspecs) + @specs.push(*gemspecs) + end + + # Return a list of the specifications in the dependency list, + # sorted in order so that no spec in the list depends on a gem + # earlier in the list. + # + # This is useful when removing gems from a set of installed gems. + # By removing them in the returned order, you don't get into as + # many dependency issues. + # + # If there are circular dependencies (yuck!), then gems will be + # returned in order until only the circular dependents and anything + # they reference are left. Then arbitrary gemspecs will be returned + # until the circular dependency is broken, after which gems will be + # returned in dependency order again. + def dependency_order + sorted = strongly_connected_components.flatten + + result = [] + seen = {} + + sorted.each do |spec| + if index = seen[spec.name] then + if result[index].version < spec.version then + result[index] = spec + end + else + seen[spec.name] = result.length + result << spec + end + end + + result.reverse + end + + def find_name(full_name) + @specs.find { |spec| spec.full_name == full_name } + end + + # Are all the dependencies in the list satisfied? + def ok? + @specs.all? do |spec| + spec.dependencies.all? do |dep| + @specs.find { |s| s.satisfies_requirement? dep } + end + end + end + + # Is is ok to remove a gem from the dependency list? + # + # If removing the gemspec creates breaks a currently ok dependency, + # then it is NOT ok to remove the gem. + def ok_to_remove?(full_name) + gem_to_remove = find_name full_name + + siblings = @specs.find_all { |s| + s.name == gem_to_remove.name && + s.full_name != gem_to_remove.full_name + } + + deps = [] + + @specs.each do |spec| + spec.dependencies.each do |dep| + deps << dep if gem_to_remove.satisfies_requirement?(dep) + end + end + + deps.all? { |dep| + siblings.any? { |s| + s.satisfies_requirement? dep + } + } + end + + def remove_by_name(full_name) + @specs.delete_if { |spec| spec.full_name == full_name } + end + + # Return a hash of predecessors. <tt>result[spec]</tt> is an + # Array of gemspecs that have a dependency satisfied by the named + # spec. + def spec_predecessors + result = Hash.new { |h,k| h[k] = [] } + + specs = @specs.sort.reverse + + specs.each do |spec| + specs.each do |other| + next if spec == other + + other.dependencies.each do |dep| + if spec.satisfies_requirement? dep then + result[spec] << other + end + end + end + end + + result + end + + def tsort_each_node(&block) + @specs.each(&block) + end + + def tsort_each_child(node, &block) + specs = @specs.sort.reverse + + node.dependencies.each do |dep| + specs.each do |spec| + if spec.satisfies_requirement? dep then + begin + yield spec + rescue TSort::Cyclic + end + break + end + end + end + end + + private + + # Count the number of gemspecs in the list +specs+ that are not in + # +ignored+. + def active_count(specs, ignored) + result = 0 + specs.each do |spec| + result += 1 unless ignored[spec.full_name] + end + result + end + +end + diff --git a/lib/rubygems/digest/digest_adapter.rb b/lib/rubygems/digest/digest_adapter.rb new file mode 100755 index 0000000000..d5a00b059d --- /dev/null +++ b/lib/rubygems/digest/digest_adapter.rb @@ -0,0 +1,40 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + # There is an incompatibility between the way Ruby 1.8.5 and 1.8.6 + # handles digests. This DigestAdapter will take a pre-1.8.6 digest + # and adapt it to the 1.8.6 API. + # + # Note that only the digest and hexdigest methods are adapted, + # since these are the only functions used by Gems. + # + class DigestAdapter + + # Initialize a digest adapter. + def initialize(digest_class) + @digest_class = digest_class + end + + # Return a new digester. Since we are only implementing the stateless + # methods, we will return ourself as the instance. + def new + self + end + + # Return the digest of +string+ as a hex string. + def hexdigest(string) + @digest_class.new(string).hexdigest + end + + # Return the digest of +string+ as a binary string. + def digest(string) + @digest_class.new(string).digest + end + end +end
\ No newline at end of file diff --git a/lib/rubygems/digest/md5.rb b/lib/rubygems/digest/md5.rb new file mode 100755 index 0000000000..f924579c08 --- /dev/null +++ b/lib/rubygems/digest/md5.rb @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/md5' + +# :stopdoc: +module Gem + if RUBY_VERSION >= '1.8.6' + MD5 = Digest::MD5 + else + require 'rubygems/digest/digest_adapter' + MD5 = DigestAdapter.new(Digest::MD5) + def MD5.md5(string) + self.hexdigest(string) + end + end +end +# :startdoc: + diff --git a/lib/rubygems/digest/sha1.rb b/lib/rubygems/digest/sha1.rb new file mode 100755 index 0000000000..2a6245dcd9 --- /dev/null +++ b/lib/rubygems/digest/sha1.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/sha1' + +module Gem + if RUBY_VERSION >= '1.8.6' + SHA1 = Digest::SHA1 + else + require 'rubygems/digest/digest_adapter' + SHA1 = DigestAdapter.new(Digest::SHA1) + end +end
\ No newline at end of file diff --git a/lib/rubygems/digest/sha2.rb b/lib/rubygems/digest/sha2.rb new file mode 100755 index 0000000000..7bef16aed2 --- /dev/null +++ b/lib/rubygems/digest/sha2.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'digest/sha2' + +module Gem + if RUBY_VERSION >= '1.8.6' + SHA256 = Digest::SHA256 + else + require 'rubygems/digest/digest_adapter' + SHA256 = DigestAdapter.new(Digest::SHA256) + end +end diff --git a/lib/rubygems/doc_manager.rb b/lib/rubygems/doc_manager.rb new file mode 100644 index 0000000000..8d9b4a7b23 --- /dev/null +++ b/lib/rubygems/doc_manager.rb @@ -0,0 +1,161 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +module Gem + + class DocManager + + include UserInteraction + + # Create a document manager for the given gem spec. + # + # spec:: The Gem::Specification object representing the gem. + # rdoc_args:: Optional arguments for RDoc (template etc.) as a String. + # + def initialize(spec, rdoc_args="") + @spec = spec + @doc_dir = File.join(spec.installation_path, "doc", spec.full_name) + @rdoc_args = rdoc_args.nil? ? [] : rdoc_args.split + end + + # Is the RDoc documentation installed? + def rdoc_installed? + return File.exist?(File.join(@doc_dir, "rdoc")) + end + + # Generate the RI documents for this gem spec. + # + # Note that if both RI and RDoc documents are generated from the + # same process, the RI docs should be done first (a likely bug in + # RDoc will cause RI docs generation to fail if run after RDoc). + def generate_ri + if @spec.has_rdoc then + load_rdoc + install_ri # RDoc bug, ri goes first + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + end + + # Generate the RDoc documents for this gem spec. + # + # Note that if both RI and RDoc documents are generated from the + # same process, the RI docs should be done first (a likely bug in + # RDoc will cause RI docs generation to fail if run after RDoc). + def generate_rdoc + if @spec.has_rdoc then + load_rdoc + install_rdoc + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + end + + # Load the RDoc documentation generator library. + def load_rdoc + if File.exist?(@doc_dir) && !File.writable?(@doc_dir) then + raise Gem::FilePermissionError.new(@doc_dir) + end + + FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir) + + begin + require 'rdoc/rdoc' + rescue LoadError => e + raise Gem::DocumentError, + "ERROR: RDoc documentation generator not installed!" + end + end + + def install_rdoc + rdoc_dir = File.join @doc_dir, 'rdoc' + + FileUtils.rm_rf rdoc_dir + + say "Installing RDoc documentation for #{@spec.full_name}..." + run_rdoc '--op', rdoc_dir + end + + def install_ri + ri_dir = File.join @doc_dir, 'ri' + + FileUtils.rm_rf ri_dir + + say "Installing ri documentation for #{@spec.full_name}..." + run_rdoc '--ri', '--op', ri_dir + end + + def run_rdoc(*args) + args << @spec.rdoc_options + args << DocManager.configured_args + args << '--quiet' + args << @spec.require_paths.clone + args << @spec.extra_rdoc_files + args.flatten! + + r = RDoc::RDoc.new + + old_pwd = Dir.pwd + Dir.chdir(@spec.full_gem_path) + begin + r.document args + rescue Errno::EACCES => e + dirname = File.dirname e.message.split("-")[1].strip + raise Gem::FilePermissionError.new(dirname) + rescue RuntimeError => ex + alert_error "While generating documentation for #{@spec.full_name}" + ui.errs.puts "... MESSAGE: #{ex}" + ui.errs.puts "... RDOC args: #{args.join(' ')}" + ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if + Gem.configuration.backtrace + ui.errs.puts "(continuing with the rest of the installation)" + ensure + Dir.chdir(old_pwd) + end + end + + def uninstall_doc + raise Gem::FilePermissionError.new(@spec.installation_path) unless + File.writable? @spec.installation_path + + original_name = [ + @spec.name, @spec.version, @spec.original_platform].join '-' + + doc_dir = File.join @spec.installation_path, 'doc', @spec.full_name + unless File.directory? doc_dir then + doc_dir = File.join @spec.installation_path, 'doc', original_name + end + + FileUtils.rm_rf doc_dir + + ri_dir = File.join @spec.installation_path, 'ri', @spec.full_name + + unless File.directory? ri_dir then + ri_dir = File.join @spec.installation_path, 'ri', original_name + end + + FileUtils.rm_rf ri_dir + end + + class << self + def configured_args + @configured_args ||= [] + end + + def configured_args=(args) + case args + when Array + @configured_args = args + when String + @configured_args = args.split + end + end + end + + end +end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb new file mode 100644 index 0000000000..294dad5748 --- /dev/null +++ b/lib/rubygems/exceptions.rb @@ -0,0 +1,63 @@ +require 'rubygems' + +## +# Base exception class for RubyGems. All exception raised by RubyGems are a +# subclass of this one. +class Gem::Exception < RuntimeError; end + +class Gem::CommandLineError < Gem::Exception; end + +class Gem::DependencyError < Gem::Exception; end + +class Gem::DependencyRemovalException < Gem::Exception; end + +class Gem::DocumentError < Gem::Exception; end + +## +# Potentially raised when a specification is validated. +class Gem::EndOfYAMLException < Gem::Exception; end + +## +# Signals that a file permission error is preventing the user from +# installing in the requested directories. +class Gem::FilePermissionError < Gem::Exception + def initialize(path) + super("You don't have write permissions into the #{path} directory.") + end +end + +## +# Used to raise parsing and loading errors +class Gem::FormatException < Gem::Exception + attr_accessor :file_path +end + +class Gem::GemNotFoundException < Gem::Exception; end + +class Gem::InstallError < Gem::Exception; end + +## +# Potentially raised when a specification is validated. +class Gem::InvalidSpecificationException < Gem::Exception; end + +class Gem::OperationNotSupportedError < Gem::Exception; end + +## +# Signals that a remote operation cannot be conducted, probably due to not +# being connected (or just not finding host). +#-- +# TODO: create a method that tests connection to the preferred gems server. +# All code dealing with remote operations will want this. Failure in that +# method should raise this error. +class Gem::RemoteError < Gem::Exception; end + +class Gem::RemoteInstallationCancelled < Gem::Exception; end + +class Gem::RemoteInstallationSkipped < Gem::Exception; end + +## +# Represents an error communicating via HTTP. +class Gem::RemoteSourceException < Gem::Exception; end + +class Gem::VerificationError < Gem::Exception; end + diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb new file mode 100644 index 0000000000..97ee762a4a --- /dev/null +++ b/lib/rubygems/ext.rb @@ -0,0 +1,18 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# Classes for building C extensions live here. + +module Gem::Ext; end + +require 'rubygems/ext/builder' +require 'rubygems/ext/configure_builder' +require 'rubygems/ext/ext_conf_builder' +require 'rubygems/ext/rake_builder' + diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb new file mode 100644 index 0000000000..576951a566 --- /dev/null +++ b/lib/rubygems/ext/builder.rb @@ -0,0 +1,56 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext' + +class Gem::Ext::Builder + + def self.class_name + name =~ /Ext::(.*)Builder/ + $1.downcase + end + + def self.make(dest_path, results) + unless File.exist? 'Makefile' then + raise Gem::InstallError, "Makefile not found:\n\n#{results.join "\n"}" + end + + mf = File.read('Makefile') + mf = mf.gsub(/^RUBYARCHDIR\s*=\s*\$[^$]*/, "RUBYARCHDIR = #{dest_path}") + mf = mf.gsub(/^RUBYLIBDIR\s*=\s*\$[^$]*/, "RUBYLIBDIR = #{dest_path}") + + File.open('Makefile', 'wb') {|f| f.print mf} + + make_program = ENV['make'] + unless make_program then + make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' + end + + ['', ' install'].each do |target| + cmd = "#{make_program}#{target}" + results << cmd + results << `#{cmd} #{redirector}` + + raise Gem::InstallError, "make#{target} failed:\n\n#{results}" unless + $?.exitstatus.zero? + end + end + + def self.redirector + '2>&1' + end + + def self.run(command, results) + results << command + results << `#{command} #{redirector}` + + unless $?.exitstatus.zero? then + raise Gem::InstallError, "#{class_name} failed:\n\n#{results.join "\n"}" + end + end + +end + diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb new file mode 100644 index 0000000000..1cde6915a7 --- /dev/null +++ b/lib/rubygems/ext/configure_builder.rb @@ -0,0 +1,24 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + unless File.exist?('Makefile') then + cmd = "sh ./configure --prefix=#{dest_path}" + + run cmd, results + end + + make dest_path, results + + results + end + +end + diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb new file mode 100644 index 0000000000..cbe0e80821 --- /dev/null +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -0,0 +1,23 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + cmd = "#{Gem.ruby} #{File.basename extension}" + cmd << " #{ARGV.join ' '}" unless ARGV.empty? + + run cmd, results + + make dest_path, results + + results + end + +end + diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb new file mode 100644 index 0000000000..3772f6a00f --- /dev/null +++ b/lib/rubygems/ext/rake_builder.rb @@ -0,0 +1,27 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/ext/builder' + +class Gem::Ext::RakeBuilder < Gem::Ext::Builder + + def self.build(extension, directory, dest_path, results) + if File.basename(extension) =~ /mkrf_conf/i then + cmd = "#{Gem.ruby} #{File.basename extension}" + cmd << " #{ARGV.join " "}" unless ARGV.empty? + run cmd, results + end + + cmd = ENV['rake'] || 'rake' + cmd << " RUBYARCHDIR=#{dest_path} RUBYLIBDIR=#{dest_path}" + + run cmd, results + + results + end + +end + diff --git a/lib/rubygems/format.rb b/lib/rubygems/format.rb new file mode 100644 index 0000000000..378a93018c --- /dev/null +++ b/lib/rubygems/format.rb @@ -0,0 +1,81 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +require 'rubygems/package' + +module Gem + + ## + # The format class knows the guts of the RubyGem .gem file format + # and provides the capability to read gem files + # + class Format + attr_accessor :spec, :file_entries, :gem_path + extend Gem::UserInteraction + + ## + # Constructs an instance of a Format object, representing the gem's + # data structure. + # + # gem:: [String] The file name of the gem + # + def initialize(gem_path) + @gem_path = gem_path + end + + ## + # Reads the named gem file and returns a Format object, representing + # the data from the gem file + # + # file_path:: [String] Path to the gem file + # + def self.from_file_by_path(file_path, security_policy = nil) + format = nil + + unless File.exist?(file_path) + raise Gem::Exception, "Cannot load gem at [#{file_path}] in #{Dir.pwd}" + end + + # check for old version gem + if File.read(file_path, 20).include?("MD5SUM =") + #alert_warning "Gem #{file_path} is in old format." + require 'rubygems/old_format' + format = OldFormat.from_file_by_path(file_path) + else + begin + f = File.open(file_path, 'rb') + format = from_io(f, file_path, security_policy) + ensure + f.close unless f.closed? + end + end + + return format + end + + ## + # Reads a gem from an io stream and returns a Format object, representing + # the data from the gem file + # + # io:: [IO] Stream from which to read the gem + # + def self.from_io(io, gem_path="(io)", security_policy = nil) + format = self.new(gem_path) + Package.open_from_io(io, 'r', security_policy) do |pkg| + format.spec = pkg.metadata + format.file_entries = [] + pkg.each do |entry| + format.file_entries << [{"size" => entry.size, "mode" => entry.mode, + "path" => entry.full_name}, entry.read] + end + end + format + end + + end +end diff --git a/lib/rubygems/gem_open_uri.rb b/lib/rubygems/gem_open_uri.rb new file mode 100644 index 0000000000..6e35413b37 --- /dev/null +++ b/lib/rubygems/gem_open_uri.rb @@ -0,0 +1,7 @@ +#!/usr/bin/env ruby + +if RUBY_VERSION < "1.9" + require 'rubygems/open-uri' +else + require 'open-uri' +end diff --git a/lib/rubygems/gem_openssl.rb b/lib/rubygems/gem_openssl.rb new file mode 100644 index 0000000000..17e7d0f2bf --- /dev/null +++ b/lib/rubygems/gem_openssl.rb @@ -0,0 +1,83 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +# Some system might not have OpenSSL installed, therefore the core +# library file openssl might not be available. We localize testing +# for the presence of OpenSSL in this file. + +module Gem + class << self + # Is SSL (used by the signing commands) available on this + # platform? + def ssl_available? + require 'rubygems/gem_openssl' + @ssl_available + end + + # Set the value of the ssl_avilable flag. + attr_writer :ssl_available + + # Ensure that SSL is available. Throw an exception if it is not. + def ensure_ssl_available + unless ssl_available? + fail Gem::Exception, "SSL is not installed on this system" + end + end + end +end + +begin + require 'openssl' + + # Reference a constant defined in the .rb portion of ssl (just to + # make sure that part is loaded too). + + dummy = OpenSSL::Digest::SHA1 + + Gem.ssl_available = true + + class OpenSSL::X509::Certificate # :nodoc: + # Check the validity of this certificate. + def check_validity(issuer_cert = nil, time = Time.now) + ret = if @not_before && @not_before > time + [false, :expired, "not valid before '#@not_before'"] + elsif @not_after && @not_after < time + [false, :expired, "not valid after '#@not_after'"] + elsif issuer_cert && !verify(issuer_cert.public_key) + [false, :issuer, "#{issuer_cert.subject} is not issuer"] + else + [true, :ok, 'Valid certificate'] + end + + # return hash + { :is_valid => ret[0], :error => ret[1], :desc => ret[2] } + end + end + +rescue LoadError, StandardError + Gem.ssl_available = false +end + +module Gem::SSL + + # We make our own versions of the constants here. This allows us + # to reference the constants, even though some systems might not + # have SSL installed in the Ruby core package. + # + # These constants are only used during load time. At runtime, any + # method that makes a direct reference to SSL software must be + # protected with a Gem.ensure_ssl_available call. + # + if Gem.ssl_available? then + PKEY_RSA = OpenSSL::PKey::RSA + DIGEST_SHA1 = OpenSSL::Digest::SHA1 + else + PKEY_RSA = :rsa + DIGEST_SHA1 = :sha1 + end + +end + diff --git a/lib/rubygems/gem_path_searcher.rb b/lib/rubygems/gem_path_searcher.rb new file mode 100644 index 0000000000..dadad66289 --- /dev/null +++ b/lib/rubygems/gem_path_searcher.rb @@ -0,0 +1,84 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# +# GemPathSearcher has the capability to find loadable files inside +# gems. It generates data up front to speed up searches later. +# +class Gem::GemPathSearcher + + # + # Initialise the data we need to make searches later. + # + def initialize + # We want a record of all the installed gemspecs, in the order + # we wish to examine them. + @gemspecs = init_gemspecs + # Map gem spec to glob of full require_path directories. + # Preparing this information may speed up searches later. + @lib_dirs = {} + @gemspecs.each do |spec| + @lib_dirs[spec.object_id] = lib_dirs_for(spec) + end + end + + # + # Look in all the installed gems until a matching _path_ is found. + # Return the _gemspec_ of the gem where it was found. If no match + # is found, return nil. + # + # The gems are searched in alphabetical order, and in reverse + # version order. + # + # For example: + # + # find('log4r') # -> (log4r-1.1 spec) + # find('log4r.rb') # -> (log4r-1.1 spec) + # find('rake/rdoctask') # -> (rake-0.4.12 spec) + # find('foobarbaz') # -> nil + # + # Matching paths can have various suffixes ('.rb', '.so', and + # others), which may or may not already be attached to _file_. + # This method doesn't care about the full filename that matches; + # only that there is a match. + # + def find(path) + @gemspecs.each do |spec| + return spec if matching_file(spec, path) + end + nil + end + + private + + # Attempts to find a matching path using the require_paths of the + # given _spec_. + # + # Some of the intermediate results are cached in @lib_dirs for + # speed. + def matching_file(spec, path) # :doc: + glob = File.join @lib_dirs[spec.object_id], "#{path}#{Gem.suffix_pattern}" + return true unless Dir[glob].select { |f| File.file?(f.untaint) }.empty? + end + + # Return a list of all installed gemspecs, sorted by alphabetical + # order and in reverse version order. + def init_gemspecs + Gem.source_index.map { |_, spec| spec }.sort { |a,b| + (a.name <=> b.name).nonzero? || (b.version <=> a.version) + } + end + + # Returns library directories glob for a gemspec. For example, + # '/usr/local/lib/ruby/gems/1.8/gems/foobar-1.0/{lib,ext}' + def lib_dirs_for(spec) + "#{spec.full_gem_path}/{#{spec.require_paths.join(',')}}" + end + +end + diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb new file mode 100644 index 0000000000..5f91398b5b --- /dev/null +++ b/lib/rubygems/gem_runner.rb @@ -0,0 +1,58 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/command_manager' +require 'rubygems/config_file' +require 'rubygems/doc_manager' + +module Gem + + #################################################################### + # Run an instance of the gem program. + # + class GemRunner + + def initialize(options={}) + @command_manager_class = options[:command_manager] || Gem::CommandManager + @config_file_class = options[:config_file] || Gem::ConfigFile + @doc_manager_class = options[:doc_manager] || Gem::DocManager + end + + # Run the gem command with the following arguments. + def run(args) + start_time = Time.now + do_configuration(args) + cmd = @command_manager_class.instance + cmd.command_names.each do |command_name| + config_args = Gem.configuration[command_name] + config_args = case config_args + when String + config_args.split ' ' + else + Array(config_args) + end + Command.add_specific_extra_args command_name, config_args + end + cmd.run(Gem.configuration.args) + end_time = Time.now + if Gem.configuration.benchmark + printf "\nExecution time: %0.2f seconds.\n", end_time-start_time + puts "Press Enter to finish" + STDIN.gets + end + end + + private + + def do_configuration(args) + Gem.configuration = @config_file_class.new(args) + Gem.use_paths(Gem.configuration[:gemhome], Gem.configuration[:gempath]) + Gem::Command.extra_args = Gem.configuration[:gem] + @doc_manager_class.configured_args = Gem.configuration[:rdoc] + end + + end # class +end # module diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb new file mode 100644 index 0000000000..8cb7735c29 --- /dev/null +++ b/lib/rubygems/indexer.rb @@ -0,0 +1,171 @@ +require 'fileutils' +require 'tmpdir' + +require 'rubygems' +require 'rubygems/format' + +begin + require 'builder/xchar' +rescue LoadError +end + +## +# Top level class for building the gem repository index. +class Gem::Indexer + + include Gem::UserInteraction + + ## + # Index install location + + attr_reader :dest_directory + + ## + # Index build directory + + attr_reader :directory + + # Create an indexer that will index the gems in +directory+. + def initialize(directory) + unless ''.respond_to? :to_xs then + fail "Gem::Indexer requires that the XML Builder library be installed:" \ + "\n\tgem install builder" + end + + @dest_directory = directory + @directory = File.join Dir.tmpdir, "gem_generate_index_#{$$}" + + marshal_name = "Marshal.#{Gem.marshal_version}" + + @master_index = Gem::Indexer::MasterIndexBuilder.new "yaml", @directory + @marshal_index = Gem::Indexer::MarshalIndexBuilder.new marshal_name, @directory + @quick_index = Gem::Indexer::QuickIndexBuilder.new "index", @directory + end + + # Build the index. + def build_index + @master_index.build do + @quick_index.build do + @marshal_index.build do + progress = ui.progress_reporter gem_file_list.size, + "Generating index for #{gem_file_list.size} gems in #{@dest_directory}", + "Loaded all gems" + + gem_file_list.each do |gemfile| + if File.size(gemfile.to_s) == 0 then + alert_warning "Skipping zero-length gem: #{gemfile}" + next + end + + begin + spec = Gem::Format.from_file_by_path(gemfile).spec + + original_name = if spec.platform == Gem::Platform::RUBY or + spec.platform.nil? then + spec.full_name + else + "#{spec.name}-#{spec.version}-#{spec.original_platform}" + end + + unless gemfile =~ /\/#{Regexp.escape spec.full_name}.*\.gem\z/i or + gemfile =~ /\/#{Regexp.escape original_name}.*\.gem\z/i then + alert_warning "Skipping misnamed gem: #{gemfile} => #{spec.full_name} (#{original_name})" + next + end + + abbreviate spec + sanitize spec + + @master_index.add spec + @quick_index.add spec + @marshal_index.add spec + + progress.updated spec.full_name + + rescue SignalException => e + alert_error "Recieved signal, exiting" + raise + rescue Exception => e + alert_error "Unable to process #{gemfile}\n#{e.message} (#{e.class})\n\t#{e.backtrace.join "\n\t"}" + end + end + + progress.done + + say "Generating master indexes (this may take a while)" + end + end + end + end + + def install_index + verbose = Gem.configuration.really_verbose + + say "Moving index into production dir #{@dest_directory}" if verbose + + files = @master_index.files + @quick_index.files + @marshal_index.files + + files.each do |file| + relative_name = file[/\A#{@directory}.(.*)/, 1] + dest_name = File.join @dest_directory, relative_name + + FileUtils.rm_rf dest_name, :verbose => verbose + FileUtils.mv file, @dest_directory, :verbose => verbose + end + end + + def generate_index + FileUtils.rm_rf @directory + FileUtils.mkdir_p @directory, :mode => 0700 + + build_index + install_index + rescue SignalException + ensure + FileUtils.rm_rf @directory + end + + # List of gem file names to index. + def gem_file_list + Dir.glob(File.join(@dest_directory, "gems", "*.gem")) + end + + # Abbreviate the spec for downloading. Abbreviated specs are only + # used for searching, downloading and related activities and do not + # need deployment specific information (e.g. list of files). So we + # abbreviate the spec, making it much smaller for quicker downloads. + def abbreviate(spec) + spec.files = [] + spec.test_files = [] + spec.rdoc_options = [] + spec.extra_rdoc_files = [] + spec.cert_chain = [] + spec + end + + # Sanitize the descriptive fields in the spec. Sometimes non-ASCII + # characters will garble the site index. Non-ASCII characters will + # be replaced by their XML entity equivalent. + def sanitize(spec) + spec.summary = sanitize_string(spec.summary) + spec.description = sanitize_string(spec.description) + spec.post_install_message = sanitize_string(spec.post_install_message) + spec.authors = spec.authors.collect { |a| sanitize_string(a) } + spec + end + + # Sanitize a single string. + def sanitize_string(string) + # HACK the #to_s is in here because RSpec has an Array of Arrays of + # Strings for authors. Need a way to disallow bad values on gempsec + # generation. (Probably won't happen.) + string ? string.to_s.to_xs : string + end + +end + +require 'rubygems/indexer/abstract_index_builder' +require 'rubygems/indexer/master_index_builder' +require 'rubygems/indexer/quick_index_builder' +require 'rubygems/indexer/marshal_index_builder' + diff --git a/lib/rubygems/indexer/abstract_index_builder.rb b/lib/rubygems/indexer/abstract_index_builder.rb new file mode 100644 index 0000000000..f25f21707b --- /dev/null +++ b/lib/rubygems/indexer/abstract_index_builder.rb @@ -0,0 +1,80 @@ +require 'zlib' + +require 'rubygems/indexer' + +# Abstract base class for building gem indicies. Uses the template pattern +# with subclass specialization in the +begin_index+, +end_index+ and +cleanup+ +# methods. +class Gem::Indexer::AbstractIndexBuilder + + # Directory to put index files in + attr_reader :directory + + # File name of the generated index + attr_reader :filename + + # List of written files/directories to move into production + attr_reader :files + + def initialize(filename, directory) + @filename = filename + @directory = directory + @files = [] + end + + # Build a Gem index. Yields to block to handle the details of the + # actual building. Calls +begin_index+, +end_index+ and +cleanup+ at + # appropriate times to customize basic operations. + def build + FileUtils.mkdir_p @directory unless File.exist? @directory + raise "not a directory: #{@directory}" unless File.directory? @directory + + file_path = File.join @directory, @filename + + @files << file_path + + File.open file_path, "wb" do |file| + @file = file + start_index + yield + end_index + end + cleanup + ensure + @file = nil + end + + # Compress the given file. + def compress(filename, ext="rz") + zipped = zip(File.open(filename, 'rb'){ |fp| fp.read }) + File.open "#{filename}.#{ext}", "wb" do |file| + file.write zipped + end + end + + # Called immediately before the yield in build. The index file is open and + # available as @file. + def start_index + end + + # Called immediately after the yield in build. The index file is still open + # and available as @file. + def end_index + end + + # Called from within builder after the index file has been closed. + def cleanup + end + + # Return an uncompressed version of a compressed string. + def unzip(string) + Zlib::Inflate.inflate(string) + end + + # Return a compressed version of the given string. + def zip(string) + Zlib::Deflate.deflate(string) + end + +end + diff --git a/lib/rubygems/indexer/marshal_index_builder.rb b/lib/rubygems/indexer/marshal_index_builder.rb new file mode 100644 index 0000000000..5e3ba7f5b9 --- /dev/null +++ b/lib/rubygems/indexer/marshal_index_builder.rb @@ -0,0 +1,8 @@ +require 'rubygems/indexer' + +# Construct the master Gem index file. +class Gem::Indexer::MarshalIndexBuilder < Gem::Indexer::MasterIndexBuilder + def end_index + @file.write @index.dump + end +end diff --git a/lib/rubygems/indexer/master_index_builder.rb b/lib/rubygems/indexer/master_index_builder.rb new file mode 100644 index 0000000000..f435c44e41 --- /dev/null +++ b/lib/rubygems/indexer/master_index_builder.rb @@ -0,0 +1,44 @@ +require 'rubygems/indexer' + +# Construct the master Gem index file. +class Gem::Indexer::MasterIndexBuilder < Gem::Indexer::AbstractIndexBuilder + + def start_index + super + @index = Gem::SourceIndex.new + end + + def end_index + super + @file.puts @index.to_yaml + end + + def cleanup + super + + index_file_name = File.join @directory, @filename + + compress index_file_name, "Z" + compressed_file_name = "#{index_file_name}.Z" + + paranoid index_file_name, compressed_file_name + + @files << compressed_file_name + end + + def add(spec) + @index.add_spec(spec) + end + + private + + def paranoid(fn, compressed_fn) + data = File.open(fn, 'rb') do |fp| fp.read end + compressed_data = File.open(compressed_fn, 'rb') do |fp| fp.read end + + if data != unzip(compressed_data) then + fail "Compressed file #{compressed_fn} does not match uncompressed file #{fn}" + end + end + +end diff --git a/lib/rubygems/indexer/quick_index_builder.rb b/lib/rubygems/indexer/quick_index_builder.rb new file mode 100644 index 0000000000..8805f3fe38 --- /dev/null +++ b/lib/rubygems/indexer/quick_index_builder.rb @@ -0,0 +1,48 @@ +require 'rubygems/indexer' + +# Construct a quick index file and all of the individual specs to support +# incremental loading. +class Gem::Indexer::QuickIndexBuilder < Gem::Indexer::AbstractIndexBuilder + + def initialize(filename, directory) + directory = File.join directory, 'quick' + + super filename, directory + end + + def cleanup + super + + quick_index_file = File.join(@directory, @filename) + compress quick_index_file + + # the complete quick index is in a directory, so move it as a whole + @files.delete quick_index_file + @files << @directory + end + + def add(spec) + @file.puts spec.full_name + add_yaml(spec) + add_marshal(spec) + end + + def add_yaml(spec) + fn = File.join @directory, "#{spec.full_name}.gemspec.rz" + zipped = zip spec.to_yaml + File.open fn, "wb" do |gsfile| gsfile.write zipped end + end + + def add_marshal(spec) + # HACK why does this not work in #initialize? + FileUtils.mkdir_p File.join(@directory, "Marshal.#{Gem.marshal_version}") + + fn = File.join @directory, "Marshal.#{Gem.marshal_version}", + "#{spec.full_name}.gemspec.rz" + + zipped = zip Marshal.dump(spec) + File.open fn, "wb" do |gsfile| gsfile.write zipped end + end + +end + diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb new file mode 100644 index 0000000000..01c3a8af27 --- /dev/null +++ b/lib/rubygems/install_update_options.rb @@ -0,0 +1,87 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' +require 'rubygems/security' + +## +# Mixin methods for install and update options for Gem::Commands +module Gem::InstallUpdateOptions + + # Add the install/update options to the option parser. + def add_install_update_options + OptionParser.accept Gem::Security::Policy do |value| + value = Gem::Security::Policies[value] + raise OptionParser::InvalidArgument, value if value.nil? + value + end + + add_option(:"Install/Update", '-i', '--install-dir DIR', + 'Gem repository directory to get installed', + 'gems') do |value, options| + options[:install_dir] = File.expand_path(value) + end + + add_option(:"Install/Update", '-d', '--[no-]rdoc', + 'Generate RDoc documentation for the gem on', + 'install') do |value, options| + options[:generate_rdoc] = value + end + + add_option(:"Install/Update", '--[no-]ri', + 'Generate RI documentation for the gem on', + 'install') do |value, options| + options[:generate_ri] = value + end + + add_option(:"Install/Update", '-E', '--env-shebang', + "Rewrite the shebang line on installed", + "scripts to use /usr/bin/env") do |value, options| + options[:env_shebang] = value + end + + add_option(:"Install/Update", '-f', '--[no-]force', + 'Force gem to install, bypassing dependency', + 'checks') do |value, options| + options[:force] = value + end + + add_option(:"Install/Update", '-t', '--[no-]test', + 'Run unit tests prior to installation') do |value, options| + options[:test] = value + end + + add_option(:"Install/Update", '-w', '--[no-]wrappers', + 'Use bin wrappers for executables', + 'Not available on dosish platforms') do |value, options| + options[:wrappers] = value + end + + add_option(:"Install/Update", '-P', '--trust-policy POLICY', + Gem::Security::Policy, + 'Specify gem trust policy') do |value, options| + options[:security_policy] = value + end + + add_option(:"Install/Update", '--ignore-dependencies', + 'Do not install any required dependent gems') do |value, options| + options[:ignore_dependencies] = value + end + + add_option(:"Install/Update", '-y', '--include-dependencies', + 'Unconditionally install the required', + 'dependent gems') do |value, options| + options[:include_dependencies] = value + end + end + + # Default options for the gem install command. + def install_update_defaults_str + '--rdoc --no-force --no-test --wrappers' + end + +end + diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb new file mode 100644 index 0000000000..03f7c92828 --- /dev/null +++ b/lib/rubygems/installer.rb @@ -0,0 +1,421 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'pathname' +require 'rbconfig' + +require 'rubygems/format' +require 'rubygems/ext' + +## +# The installer class processes RubyGem .gem files and installs the +# files contained in the .gem into the Gem.path. +# +# Gem::Installer does the work of putting files in all the right places on the +# filesystem including unpacking the gem into its gem dir, installing the +# gemspec in the specifications dir, storing the cached gem in the cache dir, +# and installing either wrappers or symlinks for executables. +class Gem::Installer + + ## + # Raised when there is an error while building extensions. + # + class ExtensionBuildError < Gem::InstallError; end + + include Gem::UserInteraction + + ## + # Constructs an Installer instance that will install the gem located at + # +gem+. +options+ is a Hash with the following keys: + # + # :env_shebang:: Use /usr/bin/env in bin wrappers. + # :force:: Overrides all version checks and security policy checks, except + # for a signed-gems-only policy. + # :ignore_dependencies:: Don't raise if a dependency is missing. + # :install_dir:: The directory to install the gem into. + # :security_policy:: Use the specified security policy. See Gem::Security + # :wrappers:: Install wrappers if true, symlinks if false. + def initialize(gem, options={}) + @gem = gem + + options = { :force => false, :install_dir => Gem.dir }.merge options + + @env_shebang = options[:env_shebang] + @force = options[:force] + gem_home = options[:install_dir] + @gem_home = Pathname.new(gem_home).expand_path + @ignore_dependencies = options[:ignore_dependencies] + @security_policy = options[:security_policy] + @wrappers = options[:wrappers] + + begin + @format = Gem::Format.from_file_by_path @gem, @security_policy + rescue Gem::Package::FormatError + raise Gem::InstallError, "invalid gem format for #{@gem}" + end + + @spec = @format.spec + + @gem_dir = File.join(@gem_home, "gems", @spec.full_name).untaint + end + + ## + # Installs the gem and returns a loaded Gem::Specification for the installed + # gem. + # + # The gem will be installed with the following structure: + # + # @gem_home/ + # cache/<gem-version>.gem #=> a cached copy of the installed gem + # gems/<gem-version>/... #=> extracted files + # specifications/<gem-version>.gemspec #=> the Gem::Specification + def install + # If we're forcing the install then disable security unless the security + # policy says that we only install singed gems. + @security_policy = nil if @force and @security_policy and + not @security_policy.only_signed + + unless @force then + if rrv = @spec.required_ruby_version then + unless rrv.satisfied_by? Gem::Version.new(RUBY_VERSION) then + raise Gem::InstallError, "#{@spec.name} requires Ruby version #{rrv}" + end + end + + if rrgv = @spec.required_rubygems_version then + unless rrgv.satisfied_by? Gem::Version.new(Gem::RubyGemsVersion) then + raise Gem::InstallError, + "#{@spec.name} requires RubyGems version #{rrgv}" + end + end + + unless @ignore_dependencies then + @spec.dependencies.each do |dep_gem| + ensure_dependency @spec, dep_gem + end + end + end + + FileUtils.mkdir_p @gem_home unless File.directory? @gem_home + raise Gem::FilePermissionError, @gem_home unless File.writable? @gem_home + + Gem.ensure_gem_subdirectories @gem_home + + FileUtils.mkdir_p @gem_dir + + extract_files + generate_bin + build_extensions + write_spec + + # HACK remove? Isn't this done in multiple places? + cached_gem = File.join @gem_home, "cache", @gem.split(/\//).pop + unless File.exist? cached_gem then + FileUtils.cp @gem, File.join(@gem_home, "cache") + end + + say @spec.post_install_message unless @spec.post_install_message.nil? + + @spec.loaded_from = File.join(@gem_home, 'specifications', + "#{@spec.full_name}.gemspec") + + return @spec + rescue Zlib::GzipFile::Error + raise Gem::InstallError, "gzip error installing #{@gem}" + end + + ## + # Ensure that the dependency is satisfied by the current installation of + # gem. If it is not an exception is raised. + # + # spec :: Gem::Specification + # dependency :: Gem::Dependency + def ensure_dependency(spec, dependency) + unless installation_satisfies_dependency? dependency then + raise Gem::InstallError, "#{spec.name} requires #{dependency}" + end + + true + end + + ## + # True if the current installed gems satisfy the given dependency. + # + # dependency :: Gem::Dependency + def installation_satisfies_dependency?(dependency) + current_index = Gem::SourceIndex.from_installed_gems + current_index.find_name(dependency.name, dependency.version_requirements).size > 0 + end + + ## + # Unpacks the gem into the given directory. + # + def unpack(directory) + @gem_dir = directory + @format = Gem::Format.from_file_by_path @gem, @security_policy + extract_files + end + + ## + # Writes the .gemspec specification (in Ruby) to the supplied + # spec_path. + # + # spec:: [Gem::Specification] The Gem specification to output + # spec_path:: [String] The location (path) to write the gemspec to + # + def write_spec + rubycode = @spec.to_ruby + + file_name = File.join @gem_home, 'specifications', + "#{@spec.full_name}.gemspec" + file_name.untaint + + File.open(file_name, "w") do |file| + file.puts rubycode + end + end + + ## + # Creates windows .bat files for easy running of commands + # + def generate_windows_script(bindir, filename) + if Gem.win_platform? then + script_name = filename + ".bat" + File.open(File.join(bindir, File.basename(script_name)), "w") do |file| + file.puts windows_stub_script(bindir, filename) + end + end + end + + def generate_bin + return if @spec.executables.nil? or @spec.executables.empty? + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + bindir = Gem.bindir @gem_home + + Dir.mkdir bindir unless File.exist? bindir + raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir + + @spec.executables.each do |filename| + filename.untaint + bin_path = File.join @gem_dir, 'bin', filename + mode = File.stat(bin_path).mode | 0111 + File.chmod mode, bin_path + + if @wrappers then + generate_bin_script filename, bindir + else + generate_bin_symlink filename, bindir + end + end + end + + ## + # Creates the scripts to run the applications in the gem. + #-- + # The Windows script is generated in addition to the regular one due to a + # bug or misfeature in the Windows shell's pipe. See + # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379 + # + def generate_bin_script(filename, bindir) + File.open(File.join(bindir, File.basename(filename)), "w", 0755) do |file| + file.print app_script_text(filename) + end + generate_windows_script bindir, filename + end + + ## + # Creates the symlinks to run the applications in the gem. Moves + # the symlink if the gem being installed has a newer version. + # + def generate_bin_symlink(filename, bindir) + if Config::CONFIG["arch"] =~ /dos|win32/i then + alert_warning "Unable to use symlinks on win32, installing wrapper" + generate_bin_script filename, bindir + return + end + + src = File.join @gem_dir, 'bin', filename + dst = File.join bindir, File.basename(filename) + + if File.exist? dst then + if File.symlink? dst then + link = File.readlink(dst).split File::SEPARATOR + cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) + return if @spec.version < cur_version + end + File.unlink dst + end + + File.symlink src, dst + end + + ## + # Generates a #! line for +bin_file_name+'s wrapper copying arguments if + # necessary. + def shebang(bin_file_name) + if @env_shebang then + "#!/usr/bin/env ruby" + else + path = File.join @gem_dir, @spec.bindir, bin_file_name + + File.open(path, "rb") do |file| + first_line = file.gets + if first_line =~ /^#!/ then + # Preserve extra words on shebang line, like "-w". Thanks RPA. + shebang = first_line.sub(/\A\#!.*?ruby\S*/, "#!#{Gem.ruby}") + else + # Create a plain shebang line. + shebang = "#!#{Gem.ruby}" + end + + shebang.strip # Avoid nasty ^M issues. + end + end + end + + # Return the text for an application file. + def app_script_text(bin_file_name) + <<-TEXT +#{shebang bin_file_name} +# +# This file was generated by RubyGems. +# +# The application '#{@spec.name}' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require 'rubygems' + +version = "#{Gem::Requirement.default}" + +if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then + version = $1 + ARGV.shift +end + +gem '#{@spec.name}', version +load '#{bin_file_name}' +TEXT + end + + # return the stub script text used to launch the true ruby script + def windows_stub_script(bindir, bin_file_name) + <<-TEXT +@ECHO OFF +IF NOT "%~f0" == "~f0" GOTO :WinNT +@"#{Gem.ruby}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9 +GOTO :EOF +:WinNT +"%~dp0ruby.exe" "%~dpn0" %* +TEXT + end + + # Builds extensions. Valid types of extensions are extconf.rb files, + # configure scripts and rakefiles or mkrf_conf files. + def build_extensions + return if @spec.extensions.empty? + say "Building native extensions. This could take a while..." + start_dir = Dir.pwd + dest_path = File.join @gem_dir, @spec.require_paths.first + ran_rake = false # only run rake once + + @spec.extensions.each do |extension| + break if ran_rake + results = [] + + builder = case extension + when /extconf/ then + Gem::Ext::ExtConfBuilder + when /configure/ then + Gem::Ext::ConfigureBuilder + when /rakefile/i, /mkrf_conf/i then + ran_rake = true + Gem::Ext::RakeBuilder + else + results = ["No builder for extension '#{extension}'"] + nil + end + + begin + Dir.chdir File.join(@gem_dir, File.dirname(extension)) + results = builder.build(extension, @gem_dir, dest_path, results) + rescue => ex + results = results.join "\n" + + File.open('gem_make.out', 'wb') { |f| f.puts results } + + message = <<-EOF +ERROR: Failed to build gem native extension. + +#{results} + +Gem files will remain installed in #{@gem_dir} for inspection. +Results logged to #{File.join(Dir.pwd, 'gem_make.out')} + EOF + + raise ExtensionBuildError, message + ensure + Dir.chdir start_dir + end + end + end + + ## + # Reads the file index and extracts each file into the gem directory. + # + # Ensures that files can't be installed outside the gem directory. + def extract_files + expand_and_validate_gem_dir + + raise ArgumentError, "format required to extract from" if @format.nil? + + @format.file_entries.each do |entry, file_data| + path = entry['path'].untaint + + if path =~ /\A\// then # for extra sanity + raise Gem::InstallError, + "attempt to install file into #{entry['path'].inspect}" + end + + path = File.expand_path File.join(@gem_dir, path) + + if path !~ /\A#{Regexp.escape @gem_dir}/ then + msg = "attempt to install file into %p under %p" % + [entry['path'], @gem_dir] + raise Gem::InstallError, msg + end + + FileUtils.mkdir_p File.dirname(path) + + File.open(path, "wb") do |out| + out.write file_data + end + end + end + + private + + # HACK Pathname is broken on windows. + def absolute_path? pathname + pathname.absolute? or (Gem.win_platform? and pathname.to_s =~ /\A[a-z]:/i) + end + + def expand_and_validate_gem_dir + @gem_dir = Pathname.new(@gem_dir).expand_path + + unless absolute_path?(@gem_dir) then # HACK is this possible after #expand_path? + raise ArgumentError, "install directory %p not absolute" % @gem_dir + end + + @gem_dir = @gem_dir.to_s + end + +end + diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb new file mode 100644 index 0000000000..1a5410bef7 --- /dev/null +++ b/lib/rubygems/local_remote_options.rb @@ -0,0 +1,106 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# Mixin methods for local and remote Gem::Command options. +module Gem::LocalRemoteOptions + + # Allows OptionParser to handle HTTP URIs. + def accept_uri_http + OptionParser.accept URI::HTTP do |value| + begin + value = URI.parse value + rescue URI::InvalidURIError + raise OptionParser::InvalidArgument, value + end + + raise OptionParser::InvalidArgument, value unless value.scheme == 'http' + + value + end + end + + # Add local/remote options to the command line parser. + def add_local_remote_options + add_option(:"Local/Remote", '-l', '--local', + 'Restrict operations to the LOCAL domain') do |value, options| + options[:domain] = :local + end + + add_option(:"Local/Remote", '-r', '--remote', + 'Restrict operations to the REMOTE domain') do |value, options| + options[:domain] = :remote + end + + add_option(:"Local/Remote", '-b', '--both', + 'Allow LOCAL and REMOTE operations') do |value, options| + options[:domain] = :both + end + + add_bulk_threshold_option + add_source_option + add_proxy_option + add_update_sources_option + end + + # Add the --bulk-threshold option + def add_bulk_threshold_option + add_option(:"Local/Remote", '-B', '--bulk-threshold COUNT', + "Threshold for switching to bulk", + "synchronization (default #{Gem.configuration.bulk_threshold})") do + |value, options| + Gem.configuration.bulk_threshold = value.to_i + end + end + + # Add the --http-proxy option + def add_proxy_option + accept_uri_http + + add_option(:"Local/Remote", '-p', '--[no-]http-proxy [URL]', URI::HTTP, + 'Use HTTP proxy for remote operations') do |value, options| + options[:http_proxy] = (value == false) ? :no_proxy : value + Gem.configuration[:http_proxy] = options[:http_proxy] + end + end + + # Add the --source option + def add_source_option + accept_uri_http + + add_option(:"Local/Remote", '--source URL', URI::HTTP, + 'Use URL as the remote source for gems') do |value, options| + if options[:added_source] then + Gem.sources << value + else + options[:added_source] = true + Gem.sources.replace [value] + end + end + end + + # Add the --source option + def add_update_sources_option + + add_option(:"Local/Remote", '-u', '--[no-]update-sources', + 'Update local source cache') do |value, options| + Gem.configuration.update_sources = value + end + end + + # Is local fetching enabled? + def local? + options[:domain] == :local || options[:domain] == :both + end + + # Is remote fetching enabled? + def remote? + options[:domain] == :remote || options[:domain] == :both + end + +end + diff --git a/lib/rubygems/old_format.rb b/lib/rubygems/old_format.rb new file mode 100644 index 0000000000..ef5d621f52 --- /dev/null +++ b/lib/rubygems/old_format.rb @@ -0,0 +1,148 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' +require 'yaml' +require 'zlib' + +module Gem + + ## + # The format class knows the guts of the RubyGem .gem file format + # and provides the capability to read gem files + # + class OldFormat + attr_accessor :spec, :file_entries, :gem_path + + ## + # Constructs an instance of a Format object, representing the gem's + # data structure. + # + # gem:: [String] The file name of the gem + # + def initialize(gem_path) + @gem_path = gem_path + end + + ## + # Reads the named gem file and returns a Format object, representing + # the data from the gem file + # + # file_path:: [String] Path to the gem file + # + def self.from_file_by_path(file_path) + unless File.exist?(file_path) + raise Gem::Exception, "Cannot load gem file [#{file_path}]" + end + File.open(file_path, 'rb') do |file| + from_io(file, file_path) + end + end + + ## + # Reads a gem from an io stream and returns a Format object, representing + # the data from the gem file + # + # io:: [IO] Stream from which to read the gem + # + def self.from_io(io, gem_path="(io)") + format = self.new(gem_path) + skip_ruby(io) + format.spec = read_spec(io) + format.file_entries = [] + read_files_from_gem(io) do |entry, file_data| + format.file_entries << [entry, file_data] + end + format + end + + private + ## + # Skips the Ruby self-install header. After calling this method, the + # IO index will be set after the Ruby code. + # + # file:: [IO] The IO to process (skip the Ruby code) + # + def self.skip_ruby(file) + end_seen = false + loop { + line = file.gets + if(line == nil || line.chomp == "__END__") then + end_seen = true + break + end + } + if(end_seen == false) then + raise Gem::Exception.new("Failed to find end of ruby script while reading gem") + end + end + + ## + # Reads the specification YAML from the supplied IO and constructs + # a Gem::Specification from it. After calling this method, the + # IO index will be set after the specification header. + # + # file:: [IO] The IO to process + # + def self.read_spec(file) + yaml = '' + begin + read_until_dashes(file) do |line| + yaml << line + end + Specification.from_yaml(yaml) + rescue YAML::Error => e + raise Gem::Exception.new("Failed to parse gem specification out of gem file") + rescue ArgumentError => e + raise Gem::Exception.new("Failed to parse gem specification out of gem file") + end + end + + ## + # Reads lines from the supplied IO until a end-of-yaml (---) is + # reached + # + # file:: [IO] The IO to process + # block:: [String] The read line + # + def self.read_until_dashes(file) + while((line = file.gets) && line.chomp.strip != "---") do + yield line + end + end + + + ## + # Reads the embedded file data from a gem file, yielding an entry + # containing metadata about the file and the file contents themselves + # for each file that's archived in the gem. + # NOTE: Many of these methods should be extracted into some kind of + # Gem file read/writer + # + # gem_file:: [IO] The IO to process + # + def self.read_files_from_gem(gem_file) + errstr = "Error reading files from gem" + header_yaml = '' + begin + self.read_until_dashes(gem_file) do |line| + header_yaml << line + end + header = YAML.load(header_yaml) + raise Gem::Exception.new(errstr) unless header + header.each do |entry| + file_data = '' + self.read_until_dashes(gem_file) do |line| + file_data << line + end + yield [entry, Zlib::Inflate.inflate(file_data.strip.unpack("m")[0])] + end + rescue Exception,Zlib::DataError => e + raise Gem::Exception.new(errstr) + end + end + end +end diff --git a/lib/rubygems/open-uri.rb b/lib/rubygems/open-uri.rb new file mode 100644 index 0000000000..ffc8e48571 --- /dev/null +++ b/lib/rubygems/open-uri.rb @@ -0,0 +1,773 @@ +require 'uri' +require 'stringio' +require 'time' + +# :stopdoc: +module Kernel + private + alias rubygems_open_uri_original_open open # :nodoc: + + # makes possible to open various resources including URIs. + # If the first argument respond to `open' method, + # the method is called with the rest arguments. + # + # If the first argument is a string which begins with xxx://, + # it is parsed by URI.parse. If the parsed object respond to `open' method, + # the method is called with the rest arguments. + # + # Otherwise original open is called. + # + # Since open-uri.rb provides URI::HTTP#open, URI::HTTPS#open and + # URI::FTP#open, + # Kernel[#.]open can accepts such URIs and strings which begins with + # http://, https:// and ftp://. + # In these case, the opened file object is extended by OpenURI::Meta. + def open(name, *rest, &block) # :doc: + if name.respond_to?(:open) + name.open(*rest, &block) + elsif name.respond_to?(:to_str) && + %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name && + (uri = URI.parse(name)).respond_to?(:open) + uri.open(*rest, &block) + else + rubygems_open_uri_original_open(name, *rest, &block) + end + end + module_function :open +end + +# OpenURI is an easy-to-use wrapper for net/http, net/https and net/ftp. +# +#== Example +# +# It is possible to open http/https/ftp URL as usual like opening a file: +# +# open("http://www.ruby-lang.org/") {|f| +# f.each_line {|line| p line} +# } +# +# The opened file has several methods for meta information as follows since +# it is extended by OpenURI::Meta. +# +# open("http://www.ruby-lang.org/en") {|f| +# f.each_line {|line| p line} +# p f.base_uri # <URI::HTTP:0x40e6ef2 URL:http://www.ruby-lang.org/en/> +# p f.content_type # "text/html" +# p f.charset # "iso-8859-1" +# p f.content_encoding # [] +# p f.last_modified # Thu Dec 05 02:45:02 UTC 2002 +# } +# +# Additional header fields can be specified by an optional hash argument. +# +# open("http://www.ruby-lang.org/en/", +# "User-Agent" => "Ruby/#{RUBY_VERSION}", +# "From" => "foo@bar.invalid", +# "Referer" => "http://www.ruby-lang.org/") {|f| +# # ... +# } +# +# The environment variables such as http_proxy, https_proxy and ftp_proxy +# are in effect by default. :proxy => nil disables proxy. +# +# open("http://www.ruby-lang.org/en/raa.html", :proxy => nil) {|f| +# # ... +# } +# +# URI objects can be opened in a similar way. +# +# uri = URI.parse("http://www.ruby-lang.org/en/") +# uri.open {|f| +# # ... +# } +# +# URI objects can be read directly. The returned string is also extended by +# OpenURI::Meta. +# +# str = uri.read +# p str.base_uri +# +# Author:: Tanaka Akira <akr@m17n.org> + +module OpenURI + Options = { + :proxy => true, + :proxy_http_basic_authentication => true, + :progress_proc => true, + :content_length_proc => true, + :http_basic_authentication => true, + :read_timeout => true, + :ssl_ca_cert => nil, + :ssl_verify_mode => nil, + } + + def OpenURI.check_options(options) # :nodoc: + options.each {|k, v| + next unless Symbol === k + unless Options.include? k + raise ArgumentError, "unrecognized option: #{k}" + end + } + end + + def OpenURI.scan_open_optional_arguments(*rest) # :nodoc: + if !rest.empty? && (String === rest.first || Integer === rest.first) + mode = rest.shift + if !rest.empty? && Integer === rest.first + perm = rest.shift + end + end + return mode, perm, rest + end + + def OpenURI.open_uri(name, *rest) # :nodoc: + uri = URI::Generic === name ? name : URI.parse(name) + mode, perm, rest = OpenURI.scan_open_optional_arguments(*rest) + options = rest.shift if !rest.empty? && Hash === rest.first + raise ArgumentError.new("extra arguments") if !rest.empty? + options ||= {} + OpenURI.check_options(options) + + unless mode == nil || + mode == 'r' || mode == 'rb' || + mode == File::RDONLY + raise ArgumentError.new("invalid access mode #{mode} (#{uri.class} resource is read only.)") + end + + io = open_loop(uri, options) + if block_given? + begin + yield io + ensure + io.close + end + else + io + end + end + + def OpenURI.open_loop(uri, options) # :nodoc: + proxy_opts = [] + proxy_opts << :proxy_http_basic_authentication if options.include? :proxy_http_basic_authentication + proxy_opts << :proxy if options.include? :proxy + proxy_opts.compact! + if 1 < proxy_opts.length + raise ArgumentError, "multiple proxy options specified" + end + case proxy_opts.first + when :proxy_http_basic_authentication + opt_proxy, proxy_user, proxy_pass = options.fetch(:proxy_http_basic_authentication) + proxy_user = proxy_user.to_str + proxy_pass = proxy_pass.to_str + if opt_proxy == true + raise ArgumentError.new("Invalid authenticated proxy option: #{options[:proxy_http_basic_authentication].inspect}") + end + when :proxy + opt_proxy = options.fetch(:proxy) + proxy_user = nil + proxy_pass = nil + when nil + opt_proxy = true + proxy_user = nil + proxy_pass = nil + end + case opt_proxy + when true + find_proxy = lambda {|u| pxy = u.find_proxy; pxy ? [pxy, nil, nil] : nil} + when nil, false + find_proxy = lambda {|u| nil} + when String + opt_proxy = URI.parse(opt_proxy) + find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]} + when URI::Generic + find_proxy = lambda {|u| [opt_proxy, proxy_user, proxy_pass]} + else + raise ArgumentError.new("Invalid proxy option: #{opt_proxy}") + end + + uri_set = {} + buf = nil + while true + redirect = catch(:open_uri_redirect) { + buf = Buffer.new + uri.buffer_open(buf, find_proxy.call(uri), options) + nil + } + if redirect + if redirect.relative? + # Although it violates RFC2616, Location: field may have relative + # URI. It is converted to absolute URI using uri as a base URI. + redirect = uri + redirect + end + unless OpenURI.redirectable?(uri, redirect) + raise "redirection forbidden: #{uri} -> #{redirect}" + end + if options.include? :http_basic_authentication + # send authentication only for the URI directly specified. + options = options.dup + options.delete :http_basic_authentication + end + uri = redirect + raise "HTTP redirection loop: #{uri}" if uri_set.include? uri.to_s + uri_set[uri.to_s] = true + else + break + end + end + io = buf.io + io.base_uri = uri + io + end + + def OpenURI.redirectable?(uri1, uri2) # :nodoc: + # This test is intended to forbid a redirection from http://... to + # file:///etc/passwd. + # However this is ad hoc. It should be extensible/configurable. + uri1.scheme.downcase == uri2.scheme.downcase || + (/\A(?:http|ftp)\z/i =~ uri1.scheme && /\A(?:http|ftp)\z/i =~ uri2.scheme) + end + + def OpenURI.open_http(buf, target, proxy, options) # :nodoc: + if proxy + proxy_uri, proxy_user, proxy_pass = proxy + raise "Non-HTTP proxy URI: #{proxy_uri}" if proxy_uri.class != URI::HTTP + end + + if target.userinfo && "1.9.0" <= RUBY_VERSION + # don't raise for 1.8 because compatibility. + raise ArgumentError, "userinfo not supported. [RFC3986]" + end + + header = {} + options.each {|k, v| header[k] = v if String === k } + + require 'net/http' + klass = Net::HTTP + if URI::HTTP === target + # HTTP or HTTPS + if proxy + if proxy_user && proxy_pass + klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_user, proxy_pass) + else + klass = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port) + end + end + target_host = target.host + target_port = target.port + request_uri = target.request_uri + else + # FTP over HTTP proxy + target_host = proxy_uri.host + target_port = proxy_uri.port + request_uri = target.to_s + if proxy_user && proxy_pass + header["Proxy-Authorization"] = 'Basic ' + ["#{proxy_user}:#{proxy_pass}"].pack('m').delete("\r\n") + end + end + + http = klass.new(target_host, target_port) + if target.class == URI::HTTPS + require 'net/https' + http.use_ssl = true + http.verify_mode = options[:ssl_verify_mode] || OpenSSL::SSL::VERIFY_PEER + store = OpenSSL::X509::Store.new + if options[:ssl_ca_cert] + if File.directory? options[:ssl_ca_cert] + store.add_path options[:ssl_ca_cert] + else + store.add_file options[:ssl_ca_cert] + end + else + store.set_default_paths + end + store.set_default_paths + http.cert_store = store + end + if options.include? :read_timeout + http.read_timeout = options[:read_timeout] + end + + resp = nil + http.start { + if target.class == URI::HTTPS + # xxx: information hiding violation + sock = http.instance_variable_get(:@socket) + if sock.respond_to?(:io) + sock = sock.io # 1.9 + else + sock = sock.instance_variable_get(:@socket) # 1.8 + end + sock.post_connection_check(target_host) + end + req = Net::HTTP::Get.new(request_uri, header) + if options.include? :http_basic_authentication + user, pass = options[:http_basic_authentication] + req.basic_auth user, pass + end + http.request(req) {|response| + resp = response + if options[:content_length_proc] && Net::HTTPSuccess === resp + if resp.key?('Content-Length') + options[:content_length_proc].call(resp['Content-Length'].to_i) + else + options[:content_length_proc].call(nil) + end + end + resp.read_body {|str| + buf << str + if options[:progress_proc] && Net::HTTPSuccess === resp + options[:progress_proc].call(buf.size) + end + } + } + } + io = buf.io + io.rewind + io.status = [resp.code, resp.message] + resp.each {|name,value| buf.io.meta_add_field name, value } + case resp + when Net::HTTPSuccess + when Net::HTTPMovedPermanently, # 301 + Net::HTTPFound, # 302 + Net::HTTPSeeOther, # 303 + Net::HTTPTemporaryRedirect # 307 + throw :open_uri_redirect, URI.parse(resp['location']) + else + raise OpenURI::HTTPError.new(io.status.join(' '), io) + end + end + + class HTTPError < StandardError + def initialize(message, io) + super(message) + @io = io + end + attr_reader :io + end + + class Buffer # :nodoc: + def initialize + @io = StringIO.new + @size = 0 + end + attr_reader :size + + StringMax = 10240 + def <<(str) + @io << str + @size += str.length + if StringIO === @io && StringMax < @size + require 'tempfile' + io = Tempfile.new('open-uri') + io.binmode + Meta.init io, @io if @io.respond_to? :meta + io << @io.string + @io = io + end + end + + def io + Meta.init @io unless @io.respond_to? :meta + @io + end + end + + # Mixin for holding meta-information. + module Meta + def Meta.init(obj, src=nil) # :nodoc: + obj.extend Meta + obj.instance_eval { + @base_uri = nil + @meta = {} + } + if src + obj.status = src.status + obj.base_uri = src.base_uri + src.meta.each {|name, value| + obj.meta_add_field(name, value) + } + end + end + + # returns an Array which consists status code and message. + attr_accessor :status + + # returns a URI which is base of relative URIs in the data. + # It may differ from the URI supplied by a user because redirection. + attr_accessor :base_uri + + # returns a Hash which represents header fields. + # The Hash keys are downcased for canonicalization. + attr_reader :meta + + def meta_add_field(name, value) # :nodoc: + @meta[name.downcase] = value + end + + # returns a Time which represents Last-Modified field. + def last_modified + if v = @meta['last-modified'] + Time.httpdate(v) + else + nil + end + end + + RE_LWS = /[\r\n\t ]+/n + RE_TOKEN = %r{[^\x00- ()<>@,;:\\"/\[\]?={}\x7f]+}n + RE_QUOTED_STRING = %r{"(?:[\r\n\t !#-\[\]-~\x80-\xff]|\\[\x00-\x7f])*"}n + RE_PARAMETERS = %r{(?:;#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?=#{RE_LWS}?(?:#{RE_TOKEN}|#{RE_QUOTED_STRING})#{RE_LWS}?)*}n + + def content_type_parse # :nodoc: + v = @meta['content-type'] + # The last (?:;#{RE_LWS}?)? matches extra ";" which violates RFC2045. + if v && %r{\A#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?/(#{RE_TOKEN})#{RE_LWS}?(#{RE_PARAMETERS})(?:;#{RE_LWS}?)?\z}no =~ v + type = $1.downcase + subtype = $2.downcase + parameters = [] + $3.scan(/;#{RE_LWS}?(#{RE_TOKEN})#{RE_LWS}?=#{RE_LWS}?(?:(#{RE_TOKEN})|(#{RE_QUOTED_STRING}))/no) {|att, val, qval| + val = qval.gsub(/[\r\n\t !#-\[\]-~\x80-\xff]+|(\\[\x00-\x7f])/) { $1 ? $1[1,1] : $& } if qval + parameters << [att.downcase, val] + } + ["#{type}/#{subtype}", *parameters] + else + nil + end + end + + # returns "type/subtype" which is MIME Content-Type. + # It is downcased for canonicalization. + # Content-Type parameters are stripped. + def content_type + type, *parameters = content_type_parse + type || 'application/octet-stream' + end + + # returns a charset parameter in Content-Type field. + # It is downcased for canonicalization. + # + # If charset parameter is not given but a block is given, + # the block is called and its result is returned. + # It can be used to guess charset. + # + # If charset parameter and block is not given, + # nil is returned except text type in HTTP. + # In that case, "iso-8859-1" is returned as defined by RFC2616 3.7.1. + def charset + type, *parameters = content_type_parse + if pair = parameters.assoc('charset') + pair.last.downcase + elsif block_given? + yield + elsif type && %r{\Atext/} =~ type && + @base_uri && /\Ahttp\z/i =~ @base_uri.scheme + "iso-8859-1" # RFC2616 3.7.1 + else + nil + end + end + + # returns a list of encodings in Content-Encoding field + # as an Array of String. + # The encodings are downcased for canonicalization. + def content_encoding + v = @meta['content-encoding'] + if v && %r{\A#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?(?:,#{RE_LWS}?#{RE_TOKEN}#{RE_LWS}?)*}o =~ v + v.scan(RE_TOKEN).map {|content_coding| content_coding.downcase} + else + [] + end + end + end + + # Mixin for HTTP and FTP URIs. + module OpenRead + # OpenURI::OpenRead#open provides `open' for URI::HTTP and URI::FTP. + # + # OpenURI::OpenRead#open takes optional 3 arguments as: + # OpenURI::OpenRead#open([mode [, perm]] [, options]) [{|io| ... }] + # + # `mode', `perm' is same as Kernel#open. + # + # However, `mode' must be read mode because OpenURI::OpenRead#open doesn't + # support write mode (yet). + # Also `perm' is just ignored because it is meaningful only for file + # creation. + # + # `options' must be a hash. + # + # Each pairs which key is a string in the hash specify a extra header + # field for HTTP. + # I.e. it is ignored for FTP without HTTP proxy. + # + # The hash may include other options which key is a symbol: + # + # [:proxy] + # Synopsis: + # :proxy => "http://proxy.foo.com:8000/" + # :proxy => URI.parse("http://proxy.foo.com:8000/") + # :proxy => true + # :proxy => false + # :proxy => nil + # + # If :proxy option is specified, the value should be String, URI, + # boolean or nil. + # When String or URI is given, it is treated as proxy URI. + # When true is given or the option itself is not specified, + # environment variable `scheme_proxy' is examined. + # `scheme' is replaced by `http', `https' or `ftp'. + # When false or nil is given, the environment variables are ignored and + # connection will be made to a server directly. + # + # [:proxy_http_basic_authentication] + # Synopsis: + # :proxy_http_basic_authentication => ["http://proxy.foo.com:8000/", "proxy-user", "proxy-password"] + # :proxy_http_basic_authentication => [URI.parse("http://proxy.foo.com:8000/"), "proxy-user", "proxy-password"] + # + # If :proxy option is specified, the value should be an Array with 3 elements. + # It should contain a proxy URI, a proxy user name and a proxy password. + # The proxy URI should be a String, an URI or nil. + # The proxy user name and password should be a String. + # + # If nil is given for the proxy URI, this option is just ignored. + # + # If :proxy and :proxy_http_basic_authentication is specified, + # ArgumentError is raised. + # + # [:http_basic_authentication] + # Synopsis: + # :http_basic_authentication=>[user, password] + # + # If :http_basic_authentication is specified, + # the value should be an array which contains 2 strings: + # username and password. + # It is used for HTTP Basic authentication defined by RFC 2617. + # + # [:content_length_proc] + # Synopsis: + # :content_length_proc => lambda {|content_length| ... } + # + # If :content_length_proc option is specified, the option value procedure + # is called before actual transfer is started. + # It takes one argument which is expected content length in bytes. + # + # If two or more transfer is done by HTTP redirection, the procedure + # is called only one for a last transfer. + # + # When expected content length is unknown, the procedure is called with + # nil. + # It is happen when HTTP response has no Content-Length header. + # + # [:progress_proc] + # Synopsis: + # :progress_proc => lambda {|size| ...} + # + # If :progress_proc option is specified, the proc is called with one + # argument each time when `open' gets content fragment from network. + # The argument `size' `size' is a accumulated transfered size in bytes. + # + # If two or more transfer is done by HTTP redirection, the procedure + # is called only one for a last transfer. + # + # :progress_proc and :content_length_proc are intended to be used for + # progress bar. + # For example, it can be implemented as follows using Ruby/ProgressBar. + # + # pbar = nil + # open("http://...", + # :content_length_proc => lambda {|t| + # if t && 0 < t + # pbar = ProgressBar.new("...", t) + # pbar.file_transfer_mode + # end + # }, + # :progress_proc => lambda {|s| + # pbar.set s if pbar + # }) {|f| ... } + # + # [:read_timeout] + # Synopsis: + # :read_timeout=>nil (no timeout) + # :read_timeout=>10 (10 second) + # + # :read_timeout option specifies a timeout of read for http connections. + # + # [:ssl_ca_cert] + # Synopsis: + # :ssl_ca_cert=>filename + # + # :ssl_ca_cert is used to specify CA certificate for SSL. + # If it is given, default certificates are not used. + # + # [:ssl_verify_mode] + # Synopsis: + # :ssl_verify_mode=>mode + # + # :ssl_verify_mode is used to specify openssl verify mode. + # + # OpenURI::OpenRead#open returns an IO like object if block is not given. + # Otherwise it yields the IO object and return the value of the block. + # The IO object is extended with OpenURI::Meta. + def open(*rest, &block) + OpenURI.open_uri(self, *rest, &block) + end + + # OpenURI::OpenRead#read([options]) reads a content referenced by self and + # returns the content as string. + # The string is extended with OpenURI::Meta. + # The argument `options' is same as OpenURI::OpenRead#open. + def read(options={}) + self.open(options) {|f| + str = f.read + Meta.init str, f + str + } + end + end +end + +module URI + class Generic + # returns a proxy URI. + # The proxy URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && ENV.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = ENV.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, v = pairs.shift + if k == 'http_proxy' && ENV[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = ENV[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = ENV[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = ENV["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + unless proxy_uri = ENV[name] + if proxy_uri = ENV[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Use http_proxy.' + end + end + else + proxy_uri = ENV[name] || ENV[name.upcase] + end + + if proxy_uri && self.host + require 'socket' + begin + addr = IPSocket.getaddress(self.host) + proxy_uri = nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + if proxy_uri + proxy_uri = URI.parse(proxy_uri) + name = 'no_proxy' + if no_proxy = ENV[name] || ENV[name.upcase] + no_proxy.scan(/([^:,]*)(?::(\d+))?/) {|host, port| + if /(\A|\.)#{Regexp.quote host}\z/i =~ self.host && + (!port || self.port == port.to_i) + proxy_uri = nil + break + end + } + end + proxy_uri + else + nil + end + end + end + + class HTTP + def buffer_open(buf, proxy, options) # :nodoc: + OpenURI.open_http(buf, self, proxy, options) + end + + include OpenURI::OpenRead + end + + class FTP + def buffer_open(buf, proxy, options) # :nodoc: + if proxy + OpenURI.open_http(buf, self, proxy, options) + return + end + require 'net/ftp' + + directories = self.path.split(%r{/}, -1) + directories.shift if directories[0] == '' # strip a field before leading slash + directories.each {|d| + d.gsub!(/%([0-9A-Fa-f][0-9A-Fa-f])/) { [$1].pack("H2") } + } + unless filename = directories.pop + raise ArgumentError, "no filename: #{self.inspect}" + end + directories.each {|d| + if /[\r\n]/ =~ d + raise ArgumentError, "invalid directory: #{d.inspect}" + end + } + if /[\r\n]/ =~ filename + raise ArgumentError, "invalid filename: #{filename.inspect}" + end + typecode = self.typecode + if typecode && /\A[aid]\z/ !~ typecode + raise ArgumentError, "invalid typecode: #{typecode.inspect}" + end + + # The access sequence is defined by RFC 1738 + ftp = Net::FTP.open(self.host) + # todo: extract user/passwd from .netrc. + user = 'anonymous' + passwd = nil + user, passwd = self.userinfo.split(/:/) if self.userinfo + ftp.login(user, passwd) + directories.each {|cwd| + ftp.voidcmd("CWD #{cwd}") + } + if typecode + # xxx: typecode D is not handled. + ftp.voidcmd("TYPE #{typecode.upcase}") + end + if options[:content_length_proc] + options[:content_length_proc].call(ftp.size(filename)) + end + ftp.retrbinary("RETR #{filename}", 4096) { |str| + buf << str + options[:progress_proc].call(buf.size) if options[:progress_proc] + } + ftp.close + buf.io.rewind + end + + include OpenURI::OpenRead + end +end +# :startdoc: diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb new file mode 100644 index 0000000000..fd75d188bd --- /dev/null +++ b/lib/rubygems/package.rb @@ -0,0 +1,851 @@ +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +require 'fileutils' +require 'find' +require 'stringio' +require 'yaml' +require 'zlib' + +require 'rubygems/digest/md5' +require 'rubygems/security' +require 'rubygems/specification' + +# Wrapper for FileUtils meant to provide logging and additional operations if +# needed. +class Gem::FileOperations + + def initialize(logger = nil) + @logger = logger + end + + def method_missing(meth, *args, &block) + case + when FileUtils.respond_to?(meth) + @logger.log "#{meth}: #{args}" if @logger + FileUtils.send meth, *args, &block + when Gem::FileOperations.respond_to?(meth) + @logger.log "#{meth}: #{args}" if @logger + Gem::FileOperations.send meth, *args, &block + else + super + end + end + +end + +module Gem::Package + + class Error < StandardError; end + class NonSeekableIO < Error; end + class ClosedIO < Error; end + class BadCheckSum < Error; end + class TooLongFileName < Error; end + class FormatError < Error; end + + module FSyncDir + private + def fsync_dir(dirname) + # make sure this hits the disc + begin + dir = open(dirname, "r") + dir.fsync + rescue # ignore IOError if it's an unpatched (old) Ruby + ensure + dir.close if dir rescue nil + end + end + end + + class TarHeader + FIELDS = [:name, :mode, :uid, :gid, :size, :mtime, :checksum, :typeflag, + :linkname, :magic, :version, :uname, :gname, :devmajor, + :devminor, :prefix] + FIELDS.each {|x| attr_reader x} + + def self.new_from_stream(stream) + data = stream.read(512) + fields = data.unpack("A100" + # record name + "A8A8A8" + # mode, uid, gid + "A12A12" + # size, mtime + "A8A" + # checksum, typeflag + "A100" + # linkname + "A6A2" + # magic, version + "A32" + # uname + "A32" + # gname + "A8A8" + # devmajor, devminor + "A155") # prefix + name = fields.shift + mode = fields.shift.oct + uid = fields.shift.oct + gid = fields.shift.oct + size = fields.shift.oct + mtime = fields.shift.oct + checksum = fields.shift.oct + typeflag = fields.shift + linkname = fields.shift + magic = fields.shift + version = fields.shift.oct + uname = fields.shift + gname = fields.shift + devmajor = fields.shift.oct + devminor = fields.shift.oct + prefix = fields.shift + + empty = (data == "\0" * 512) + + new(:name=>name, :mode=>mode, :uid=>uid, :gid=>gid, :size=>size, + :mtime=>mtime, :checksum=>checksum, :typeflag=>typeflag, + :magic=>magic, :version=>version, :uname=>uname, :gname=>gname, + :devmajor=>devmajor, :devminor=>devminor, :prefix=>prefix, + :empty => empty ) + end + + def initialize(vals) + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] + raise ArgumentError, ":name, :size, :prefix and :mode required" + end + vals[:uid] ||= 0 + vals[:gid] ||= 0 + vals[:mtime] ||= 0 + vals[:checksum] ||= "" + vals[:typeflag] ||= "0" + vals[:magic] ||= "ustar" + vals[:version] ||= "00" + vals[:uname] ||= "wheel" + vals[:gname] ||= "wheel" + vals[:devmajor] ||= 0 + vals[:devminor] ||= 0 + FIELDS.each {|x| instance_variable_set "@#{x.to_s}", vals[x]} + @empty = vals[:empty] + end + + def empty? + @empty + end + + def to_s + update_checksum + header(checksum) + end + + def update_checksum + h = header(" " * 8) + @checksum = oct(calculate_checksum(h), 6) + end + + private + def oct(num, len) + "%0#{len}o" % num + end + + def calculate_checksum(hdr) + hdr.unpack("C*").inject{|a,b| a+b} + end + + def header(chksum) + # struct tarfile_entry_posix { + # char name[100]; # ASCII + (Z unless filled) + # char mode[8]; # 0 padded, octal, null + # char uid[8]; # ditto + # char gid[8]; # ditto + # char size[12]; # 0 padded, octal, null + # char mtime[12]; # 0 padded, octal, null + # char checksum[8]; # 0 padded, octal, null, space + # char typeflag[1]; # file: "0" dir: "5" + # char linkname[100]; # ASCII + (Z unless filled) + # char magic[6]; # "ustar\0" + # char version[2]; # "00" + # char uname[32]; # ASCIIZ + # char gname[32]; # ASCIIZ + # char devmajor[8]; # 0 padded, octal, null + # char devminor[8]; # o padded, octal, null + # char prefix[155]; # ASCII + (Z unless filled) + # }; + arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11), + oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version, + uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix] + str = arr.pack("a100a8a8a8a12a12" + # name, mode, uid, gid, size, mtime + "a7aaa100a6a2" + # chksum, typeflag, linkname, magic, version + "a32a32a8a8a155") # uname, gname, devmajor, devminor, prefix + str + "\0" * ((512 - str.size) % 512) + end + end + + class TarWriter + class FileOverflow < StandardError; end + class BlockNeeded < StandardError; end + + class BoundedStream + attr_reader :limit, :written + def initialize(io, limit) + @io = io + @limit = limit + @written = 0 + end + + def write(data) + if data.size + @written > @limit + raise FileOverflow, + "You tried to feed more data than fits in the file." + end + @io.write data + @written += data.size + data.size + end + end + + class RestrictedStream + def initialize(anIO) + @io = anIO + end + + def write(data) + @io.write data + end + end + + def self.new(anIO) + writer = super(anIO) + return writer unless block_given? + begin + yield writer + ensure + writer.close + end + nil + end + + def initialize(anIO) + @io = anIO + @closed = false + end + + def add_file_simple(name, mode, size) + raise BlockNeeded unless block_given? + raise ClosedIO if @closed + name, prefix = split_name(name) + header = TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix).to_s + @io.write header + os = BoundedStream.new(@io, size) + yield os + #FIXME: what if an exception is raised in the block? + min_padding = size - os.written + @io.write("\0" * min_padding) + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + end + + def add_file(name, mode) + raise BlockNeeded unless block_given? + raise ClosedIO if @closed + raise NonSeekableIO unless @io.respond_to? :pos= + name, prefix = split_name(name) + init_pos = @io.pos + @io.write "\0" * 512 # placeholder for the header + yield RestrictedStream.new(@io) + #FIXME: what if an exception is raised in the block? + #FIXME: what if an exception is raised in the block? + size = @io.pos - init_pos - 512 + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + final_pos = @io.pos + @io.pos = init_pos + header = TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix).to_s + @io.write header + @io.pos = final_pos + end + + def mkdir(name, mode) + raise ClosedIO if @closed + name, prefix = split_name(name) + header = TarHeader.new(:name => name, :mode => mode, :typeflag => "5", + :size => 0, :prefix => prefix).to_s + @io.write header + nil + end + + def flush + raise ClosedIO if @closed + @io.flush if @io.respond_to? :flush + end + + def close + #raise ClosedIO if @closed + return if @closed + @io.write "\0" * 1024 + @closed = true + end + + private + def split_name name + raise TooLongFileName if name.size > 256 + if name.size <= 100 + prefix = "" + else + parts = name.split(/\//) + newname = parts.pop + nxt = "" + loop do + nxt = parts.pop + break if newname.size + 1 + nxt.size > 100 + newname = nxt + "/" + newname + end + prefix = (parts + [nxt]).join "/" + name = newname + raise TooLongFileName if name.size > 100 || prefix.size > 155 + end + return name, prefix + end + end + + class TarReader + + include Gem::Package + + class UnexpectedEOF < StandardError; end + + module InvalidEntry + def read(len=nil); raise ClosedIO; end + def getc; raise ClosedIO; end + def rewind; raise ClosedIO; end + end + + class Entry + TarHeader::FIELDS.each{|x| attr_reader x} + + def initialize(header, anIO) + @io = anIO + @name = header.name + @mode = header.mode + @uid = header.uid + @gid = header.gid + @size = header.size + @mtime = header.mtime + @checksum = header.checksum + @typeflag = header.typeflag + @linkname = header.linkname + @magic = header.magic + @version = header.version + @uname = header.uname + @gname = header.gname + @devmajor = header.devmajor + @devminor = header.devminor + @prefix = header.prefix + @read = 0 + @orig_pos = @io.pos + end + + def read(len = nil) + return nil if @read >= @size + len ||= @size - @read + max_read = [len, @size - @read].min + ret = @io.read(max_read) + @read += ret.size + ret + end + + def getc + return nil if @read >= @size + ret = @io.getc + @read += 1 if ret + ret + end + + def is_directory? + @typeflag == "5" + end + + def is_file? + @typeflag == "0" + end + + def eof? + @read >= @size + end + + def pos + @read + end + + def rewind + raise NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @orig_pos + @read = 0 + end + + alias_method :is_directory, :is_directory? + alias_method :is_file, :is_file + + def bytes_read + @read + end + + def full_name + if @prefix != "" + File.join(@prefix, @name) + else + @name + end + end + + def close + invalidate + end + + private + def invalidate + extend InvalidEntry + end + end + + def self.new(anIO) + reader = super(anIO) + return reader unless block_given? + begin + yield reader + ensure + reader.close + end + nil + end + + def initialize(anIO) + @io = anIO + @init_pos = anIO.pos + end + + def each(&block) + each_entry(&block) + end + + # do not call this during a #each or #each_entry iteration + def rewind + if @init_pos == 0 + raise NonSeekableIO unless @io.respond_to? :rewind + @io.rewind + else + raise NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @init_pos + end + end + + def each_entry + loop do + return if @io.eof? + header = TarHeader.new_from_stream(@io) + return if header.empty? + entry = Entry.new header, @io + size = entry.size + yield entry + skip = (512 - (size % 512)) % 512 + if @io.respond_to? :seek + # avoid reading... + @io.seek(size - entry.bytes_read, IO::SEEK_CUR) + else + pending = size - entry.bytes_read + while pending > 0 + bread = @io.read([pending, 4096].min).size + raise UnexpectedEOF if @io.eof? + pending -= bread + end + end + @io.read(skip) # discard trailing zeros + # make sure nobody can use #read, #getc or #rewind anymore + entry.close + end + end + + def close + end + + end + + class TarInput + + include FSyncDir + include Enumerable + + attr_reader :metadata + + class << self; private :new end + + def initialize(io, security_policy = nil) + @io = io + @tarreader = TarReader.new(@io) + has_meta = false + data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil + dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil + + @tarreader.each do |entry| + case entry.full_name + when "metadata" + @metadata = load_gemspec(entry.read) + has_meta = true + break + when "metadata.gz" + begin + # if we have a security_policy, then pre-read the metadata file + # and calculate it's digest + sio = nil + if security_policy + Gem.ensure_ssl_available + sio = StringIO.new(entry.read) + meta_dgst = dgst_algo.digest(sio.string) + sio.rewind + end + + gzis = Zlib::GzipReader.new(sio || entry) + # YAML wants an instance of IO + @metadata = load_gemspec(gzis) + has_meta = true + ensure + gzis.close unless gzis.nil? + end + when 'metadata.gz.sig' + meta_sig = entry.read + when 'data.tar.gz.sig' + data_sig = entry.read + when 'data.tar.gz' + if security_policy + Gem.ensure_ssl_available + data_dgst = dgst_algo.digest(entry.read) + end + end + end + + if security_policy then + Gem.ensure_ssl_available + + # map trust policy from string to actual class (or a serialized YAML + # file, if that exists) + if String === security_policy then + if Gem::Security::Policy.key? security_policy then + # load one of the pre-defined security policies + security_policy = Gem::Security::Policy[security_policy] + elsif File.exist? security_policy then + # FIXME: this doesn't work yet + security_policy = YAML.load File.read(security_policy) + else + raise Gem::Exception, "Unknown trust policy '#{security_policy}'" + end + end + + if data_sig && data_dgst && meta_sig && meta_dgst then + # the user has a trust policy, and we have a signed gem + # file, so use the trust policy to verify the gem signature + + begin + security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain) + rescue Exception => e + raise "Couldn't verify data signature: #{e}" + end + + begin + security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain) + rescue Exception => e + raise "Couldn't verify metadata signature: #{e}" + end + elsif security_policy.only_signed + raise Gem::Exception, "Unsigned gem" + else + # FIXME: should display warning here (trust policy, but + # either unsigned or badly signed gem file) + end + end + + @tarreader.rewind + @fileops = Gem::FileOperations.new + raise FormatError, "No metadata found!" unless has_meta + end + + # Attempt to YAML-load a gemspec from the given _io_ parameter. Return + # nil if it fails. + def load_gemspec(io) + Gem::Specification.from_yaml(io) + rescue Gem::Exception + nil + end + + def self.open(filename, security_policy = nil, &block) + open_from_io(File.open(filename, "rb"), security_policy, &block) + end + + def self.open_from_io(io, security_policy = nil, &block) + raise "Want a block" unless block_given? + begin + is = new(io, security_policy) + yield is + ensure + is.close if is + end + end + + def each(&block) + @tarreader.each do |entry| + next unless entry.full_name == "data.tar.gz" + is = zipped_stream(entry) + begin + TarReader.new(is) do |inner| + inner.each(&block) + end + ensure + is.close if is + end + end + @tarreader.rewind + end + + # Return an IO stream for the zipped entry. + # + # NOTE: Originally this method used two approaches, Return a GZipReader + # directly, or read the GZipReader into a string and return a StringIO on + # the string. The string IO approach was used for versions of ZLib before + # 1.2.1 to avoid buffer errors on windows machines. Then we found that + # errors happened with 1.2.1 as well, so we changed the condition. Then + # we discovered errors occurred with versions as late as 1.2.3. At this + # point (after some benchmarking to show we weren't seriously crippling + # the unpacking speed) we threw our hands in the air and declared that + # this method would use the String IO approach on all platforms at all + # times. And that's the way it is. + def zipped_stream(entry) + # This is Jamis Buck's ZLib workaround. The original code is + # commented out while we evaluate this patch. + entry.read(10) # skip the gzip header + zis = Zlib::Inflate.new(-Zlib::MAX_WBITS) + is = StringIO.new(zis.inflate(entry.read)) + # zis = Zlib::GzipReader.new entry + # dis = zis.read + # is = StringIO.new(dis) + ensure + zis.finish if zis + end + + def extract_entry(destdir, entry, expected_md5sum = nil) + if entry.is_directory? + dest = File.join(destdir, entry.full_name) + if file_class.dir? dest + @fileops.chmod entry.mode, dest, :verbose=>false + else + @fileops.mkdir_p(dest, :mode => entry.mode, :verbose=>false) + end + fsync_dir dest + fsync_dir File.join(dest, "..") + return + end + # it's a file + md5 = Digest::MD5.new if expected_md5sum + destdir = File.join(destdir, File.dirname(entry.full_name)) + @fileops.mkdir_p(destdir, :mode => 0755, :verbose=>false) + destfile = File.join(destdir, File.basename(entry.full_name)) + @fileops.chmod(0600, destfile, :verbose=>false) rescue nil # Errno::ENOENT + file_class.open(destfile, "wb", entry.mode) do |os| + loop do + data = entry.read(4096) + break unless data + md5 << data if expected_md5sum + os.write(data) + end + os.fsync + end + @fileops.chmod(entry.mode, destfile, :verbose=>false) + fsync_dir File.dirname(destfile) + fsync_dir File.join(File.dirname(destfile), "..") + if expected_md5sum && expected_md5sum != md5.hexdigest + raise BadCheckSum + end + end + + def close + @io.close + @tarreader.close + end + + private + + def file_class + File + end + end + + class TarOutput + + class << self; private :new end + + def initialize(io) + @io = io + @external = TarWriter.new @io + end + + def external_handle + @external + end + + def self.open(filename, signer = nil, &block) + io = File.open(filename, "wb") + open_from_io(io, signer, &block) + nil + end + + def self.open_from_io(io, signer = nil, &block) + outputter = new(io) + metadata = nil + set_meta = lambda{|x| metadata = x} + raise "Want a block" unless block_given? + begin + data_sig, meta_sig = nil, nil + + outputter.external_handle.add_file("data.tar.gz", 0644) do |inner| + begin + sio = signer ? StringIO.new : nil + os = Zlib::GzipWriter.new(sio || inner) + + TarWriter.new(os) do |inner_tar_stream| + klass = class << inner_tar_stream; self end + klass.send(:define_method, :metadata=, &set_meta) + block.call inner_tar_stream + end + ensure + os.flush + os.finish + #os.close + + # if we have a signing key, then sign the data + # digest and return the signature + data_sig = nil + if signer + dgst_algo = Gem::Security::OPT[:dgst_algo] + dig = dgst_algo.digest(sio.string) + data_sig = signer.sign(dig) + inner.write(sio.string) + end + end + end + + # if we have a data signature, then write it to the gem too + if data_sig + sig_file = 'data.tar.gz.sig' + outputter.external_handle.add_file(sig_file, 0644) do |os| + os.write(data_sig) + end + end + + outputter.external_handle.add_file("metadata.gz", 0644) do |os| + begin + sio = signer ? StringIO.new : nil + gzos = Zlib::GzipWriter.new(sio || os) + gzos.write metadata + ensure + gzos.flush + gzos.finish + + # if we have a signing key, then sign the metadata + # digest and return the signature + if signer + dgst_algo = Gem::Security::OPT[:dgst_algo] + dig = dgst_algo.digest(sio.string) + meta_sig = signer.sign(dig) + os.write(sio.string) + end + end + end + + # if we have a metadata signature, then write to the gem as + # well + if meta_sig + sig_file = 'metadata.gz.sig' + outputter.external_handle.add_file(sig_file, 0644) do |os| + os.write(meta_sig) + end + end + + ensure + outputter.close + end + nil + end + + def close + @external.close + @io.close + end + + end + + #FIXME: refactor the following 2 methods + + def self.open(dest, mode = "r", signer = nil, &block) + raise "Block needed" unless block_given? + + case mode + when "r" + security_policy = signer + TarInput.open(dest, security_policy, &block) + when "w" + TarOutput.open(dest, signer, &block) + else + raise "Unknown Package open mode" + end + end + + def self.open_from_io(io, mode = "r", signer = nil, &block) + raise "Block needed" unless block_given? + + case mode + when "r" + security_policy = signer + TarInput.open_from_io(io, security_policy, &block) + when "w" + TarOutput.open_from_io(io, signer, &block) + else + raise "Unknown Package open mode" + end + end + + def self.pack(src, destname, signer = nil) + TarOutput.open(destname, signer) do |outp| + dir_class.chdir(src) do + outp.metadata = (file_class.read("RPA/metadata") rescue nil) + find_class.find('.') do |entry| + case + when file_class.file?(entry) + entry.sub!(%r{\./}, "") + next if entry =~ /\ARPA\// + stat = File.stat(entry) + outp.add_file_simple(entry, stat.mode, stat.size) do |os| + file_class.open(entry, "rb") do |f| + os.write(f.read(4096)) until f.eof? + end + end + when file_class.dir?(entry) + entry.sub!(%r{\./}, "") + next if entry == "RPA" + outp.mkdir(entry, file_class.stat(entry).mode) + else + raise "Don't know how to pack this yet!" + end + end + end + end + end + + class << self + def file_class + File + end + + def dir_class + Dir + end + + def find_class # HACK kill me + Find + end + end + +end + diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb new file mode 100644 index 0000000000..f72f3a7684 --- /dev/null +++ b/lib/rubygems/platform.rb @@ -0,0 +1,187 @@ +require 'rubygems' + +# Available list of platforms for targeting Gem installations. +# +class Gem::Platform + + @local = nil + + attr_accessor :cpu + + attr_accessor :os + + attr_accessor :version + + def self.local + arch = Config::CONFIG['arch'] + arch = "#{arch}_60" if arch =~ /mswin32$/ + @local ||= new(arch) + end + + def self.match(platform) + Gem.platforms.any? do |local_platform| + platform.nil? or local_platform == platform or + (local_platform != Gem::Platform::RUBY and local_platform =~ platform) + end + end + + def self.new(arch) # :nodoc: + case arch + when Gem::Platform::RUBY, nil then + Gem::Platform::RUBY + else + super + end + end + + def initialize(arch) + case arch + when Array then + @cpu, @os, @version = arch + when String then + arch = arch.split '-' + + if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu + extra = arch.pop + arch.last << "-#{extra}" + end + + cpu = arch.shift + + @cpu = case cpu + when /i\d86/ then 'x86' + else cpu + end + + if arch.length == 2 and arch.last =~ /^\d+$/ then # for command-line + @os, @version = arch + return + end + + os, = arch + @cpu, os = nil, cpu if os.nil? # legacy jruby + + @os, @version = case os + when /aix(\d+)/ then [ 'aix', $1 ] + when /cygwin/ then [ 'cygwin', nil ] + when /darwin(\d+)?/ then [ 'darwin', $1 ] + when /freebsd(\d+)/ then [ 'freebsd', $1 ] + when /hpux(\d+)/ then [ 'hpux', $1 ] + when /^java$/, /^jruby$/ then [ 'java', nil ] + when /^java([\d.]*)/ then [ 'java', $1 ] + when /linux/ then [ 'linux', $1 ] + when /mingw32/ then [ 'mingw32', nil ] + when /(mswin\d+)(\_(\d+))?/ then [ $1, $3 ] + when /netbsdelf/ then [ 'netbsdelf', nil ] + when /openbsd(\d+\.\d+)/ then [ 'openbsd', $1 ] + when /solaris(\d+\.\d+)/ then [ 'solaris', $1 ] + # test + when /^(\w+_platform)(\d+)/ then [ $1, $2 ] + else [ 'unknown', nil ] + end + when Gem::Platform then + @cpu = arch.cpu + @os = arch.os + @version = arch.version + else + raise ArgumentError, "invalid argument #{arch.inspect}" + end + end + + def inspect + "#<%s:0x%x @cpu=%p, @os=%p, @version=%p>" % [self.class, object_id, *to_a] + end + + def to_a + [@cpu, @os, @version] + end + + def to_s + to_a.compact.join '-' + end + + def ==(other) + self.class === other and + @cpu == other.cpu and @os == other.os and @version == other.version + end + + def ===(other) + return nil unless Gem::Platform === other + + # cpu + (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and + + # os + @os == other.os and + + # version + (@version.nil? or other.version.nil? or @version == other.version) + end + + def =~(other) + case other + when Gem::Platform then # nop + when String then + # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007 + other = case other + when /^i686-darwin(\d)/ then ['x86', 'darwin', $1] + when /^i\d86-linux/ then ['x86', 'linux', nil] + when 'java', 'jruby' then [nil, 'java', nil] + when /mswin32(\_(\d+))?/ then ['x86', 'mswin32', $2] + when 'powerpc-darwin' then ['powerpc', 'darwin', nil] + when /powerpc-darwin(\d)/ then ['powerpc', 'darwin', $1] + when /sparc-solaris2.8/ then ['sparc', 'solaris', '2.8'] + when /universal-darwin(\d)/ then ['universal', 'darwin', $1] + else other + end + + other = Gem::Platform.new other + else + return nil + end + + self === other + end + + ## + # A pure-ruby gem that may use Gem::Specification#extensions to build + # binary files. + + RUBY = 'ruby' + + ## + # A platform-specific gem that is built for the packaging ruby's platform. + # This will be replaced with Gem::Platform::local. + + CURRENT = 'current' + + ## + # A One Click Installer-compatible gem, built with VC6 for 32 bit Windows. + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + MSWIN32 = new ['x86', 'mswin32', '60'] + + ## + # An x86 Linux-compatible gem + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + X86_LINUX = new ['x86', 'linux', nil] + + ## + # A PowerPC Darwin-compatible gem + # + # CURRENT is preferred over this constant, avoid its use at all costs. + + PPC_DARWIN = new ['ppc', 'darwin', nil] + + # :stopdoc: + # Here lie legacy constants. These are deprecated. + WIN32 = 'mswin32' + LINUX_586 = 'i586-linux' + DARWIN = 'powerpc-darwin' + # :startdoc: + +end + diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb new file mode 100644 index 0000000000..eac4ccaf01 --- /dev/null +++ b/lib/rubygems/remote_fetcher.rb @@ -0,0 +1,164 @@ +require 'net/http' +require 'uri' + +require 'rubygems' +require 'rubygems/gem_open_uri' + +## +# RemoteFetcher handles the details of fetching gems and gem information from +# a remote source. + +class Gem::RemoteFetcher + + class FetchError < Gem::Exception; end + + @fetcher = nil + + # Cached RemoteFetcher instance. + def self.fetcher + @fetcher ||= new Gem.configuration[:http_proxy] + end + + # Initialize a remote fetcher using the source URI and possible proxy + # information. + # + # +proxy+ + # * [String]: explicit specification of proxy; overrides any environment + # variable setting + # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, + # HTTP_PROXY_PASS) + # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy + def initialize(proxy) + @proxy_uri = + case proxy + when :no_proxy then nil + when nil then get_proxy_from_env + when URI::HTTP then proxy + else URI.parse(proxy) + end + end + + # Downloads +uri+. + def fetch_path(uri) + open_uri_or_path(uri) do |input| + input.read + end + rescue Timeout::Error + raise FetchError, "timed out fetching #{uri}" + rescue OpenURI::HTTPError, IOError, SocketError, SystemCallError => e + raise FetchError, "#{e.class}: #{e} reading #{uri}" + end + + # Returns the size of +uri+ in bytes. + def fetch_size(uri) + return File.size(get_file_uri_path(uri)) if file_uri? uri + + uri = URI.parse uri unless URI::Generic === uri + + raise ArgumentError, 'uri is not an HTTP URI' unless URI::HTTP === uri + + http = connect_to uri.host, uri.port + + request = Net::HTTP::Head.new uri.request_uri + + request.basic_auth unescape(uri.user), unescape(uri.password) unless + uri.user.nil? or uri.user.empty? + + resp = http.request request + + if resp.code !~ /^2/ then + raise Gem::RemoteSourceException, + "HTTP Response #{resp.code} fetching #{uri}" + end + + if resp['content-length'] then + return resp['content-length'].to_i + else + resp = http.get uri.request_uri + return resp.body.size + end + + rescue SocketError, SystemCallError, Timeout::Error => e + raise FetchError, "#{e.message} (#{e.class})" + end + + private + + def escape(str) + return unless str + URI.escape(str) + end + + def unescape(str) + return unless str + URI.unescape(str) + end + + # Returns an HTTP proxy URI if one is set in the environment variables. + def get_proxy_from_env + env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY'] + + return nil if env_proxy.nil? or env_proxy.empty? + + uri = URI.parse env_proxy + + if uri and uri.user.nil? and uri.password.nil? then + # Probably we have http_proxy_* variables? + uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER']) + uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS']) + end + + uri + end + + # Normalize the URI by adding "http://" if it is missing. + def normalize_uri(uri) + (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}" + end + + # Connect to the source host/port, using a proxy if needed. + def connect_to(host, port) + if @proxy_uri + Net::HTTP::Proxy(@proxy_uri.host, @proxy_uri.port, unescape(@proxy_uri.user), unescape(@proxy_uri.password)).new(host, port) + else + Net::HTTP.new(host, port) + end + end + + # Read the data from the (source based) URI, but if it is a file:// URI, + # read from the filesystem instead. + def open_uri_or_path(uri, &block) + if file_uri?(uri) + open(get_file_uri_path(uri), &block) + else + connection_options = { + "User-Agent" => "RubyGems/#{Gem::RubyGemsVersion} #{Gem::Platform.local}" + } + + if @proxy_uri + http_proxy_url = "#{@proxy_uri.scheme}://#{@proxy_uri.host}:#{@proxy_uri.port}" + connection_options[:proxy_http_basic_authentication] = [http_proxy_url, unescape(@proxy_uri.user)||'', unescape(@proxy_uri.password)||''] + end + + uri = URI.parse uri unless URI::Generic === uri + unless uri.nil? || uri.user.nil? || uri.user.empty? then + connection_options[:http_basic_authentication] = + [unescape(uri.user), unescape(uri.password)] + end + + open(uri, connection_options, &block) + end + end + + # Checks if the provided string is a file:// URI. + def file_uri?(uri) + uri =~ %r{\Afile://} + end + + # Given a file:// URI, returns its local path. + def get_file_uri_path(uri) + uri.sub(%r{\Afile://}, '') + end + +end + diff --git a/lib/rubygems/remote_installer.rb b/lib/rubygems/remote_installer.rb new file mode 100644 index 0000000000..e33fd548f2 --- /dev/null +++ b/lib/rubygems/remote_installer.rb @@ -0,0 +1,195 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'fileutils' + +require 'rubygems' +require 'rubygems/installer' +require 'rubygems/source_info_cache' + +module Gem + + class RemoteInstaller + + include UserInteraction + + # <tt>options[:http_proxy]</tt>:: + # * [String]: explicit specification of proxy; overrides any + # environment variable setting + # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER, HTTP_PROXY_PASS) + # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ + # use a proxy + # + # * <tt>:cache_dir</tt>: override where downloaded gems are cached. + def initialize(options={}) + @options = options + @source_index_hash = nil + end + + # This method will install package_name onto the local system. + # + # gem_name:: + # [String] Name of the Gem to install + # + # version_requirement:: + # [default = ">= 0"] Gem version requirement to install + # + # Returns:: + # an array of Gem::Specification objects, one for each gem installed. + # + def install(gem_name, version_requirement = Gem::Requirement.default, + force = false, install_dir = Gem.dir) + unless version_requirement.respond_to?(:satisfied_by?) + version_requirement = Gem::Requirement.new [version_requirement] + end + installed_gems = [] + begin + spec, source = find_gem_to_install(gem_name, version_requirement) + dependencies = find_dependencies_not_installed(spec.dependencies) + + installed_gems << install_dependencies(dependencies, force, install_dir) + + cache_dir = @options[:cache_dir] || File.join(install_dir, "cache") + destination_file = File.join(cache_dir, spec.full_name + ".gem") + + download_gem(destination_file, source, spec) + + installer = new_installer(destination_file) + installed_gems.unshift installer.install(force, install_dir) + rescue RemoteInstallationSkipped => e + alert_error e.message + end + installed_gems.flatten + end + + # Return a hash mapping the available source names to the source + # index of that source. + def source_index_hash + return @source_index_hash if @source_index_hash + @source_index_hash = {} + Gem::SourceInfoCache.cache_data.each do |source_uri, sic_entry| + @source_index_hash[source_uri] = sic_entry.source_index + end + @source_index_hash + end + + # Finds the Gem::Specification objects and the corresponding source URI + # for gems matching +gem_name+ and +version_requirement+ + def specs_n_sources_matching(gem_name, version_requirement) + specs_n_sources = [] + + source_index_hash.each do |source_uri, source_index| + specs = source_index.search(/^#{Regexp.escape gem_name}$/i, + version_requirement) + # TODO move to SourceIndex#search? + ruby_version = Gem::Version.new RUBY_VERSION + specs = specs.select do |spec| + spec.required_ruby_version.nil? or + spec.required_ruby_version.satisfied_by? ruby_version + end + specs.each { |spec| specs_n_sources << [spec, source_uri] } + end + + if specs_n_sources.empty? then + raise GemNotFoundException, "Could not find #{gem_name} (#{version_requirement}) in any repository" + end + + specs_n_sources = specs_n_sources.sort_by { |gs,| gs.version }.reverse + + specs_n_sources + end + + # Find a gem to be installed by interacting with the user. + def find_gem_to_install(gem_name, version_requirement) + specs_n_sources = specs_n_sources_matching gem_name, version_requirement + + top_3_versions = specs_n_sources.map{|gs| gs.first.version}.uniq[0..3] + specs_n_sources.reject!{|gs| !top_3_versions.include?(gs.first.version)} + + binary_gems = specs_n_sources.reject { |item| + item[0].platform.nil? || item[0].platform==Platform::RUBY + } + + # only non-binary gems...return latest + return specs_n_sources.first if binary_gems.empty? + + list = specs_n_sources.collect { |spec, source_uri| + "#{spec.name} #{spec.version} (#{spec.platform})" + } + + list << "Skip this gem" + list << "Cancel installation" + + string, index = choose_from_list( + "Select which gem to install for your platform (#{RUBY_PLATFORM})", + list) + + if index.nil? or index == (list.size - 1) then + raise RemoteInstallationCancelled, "Installation of #{gem_name} cancelled." + end + + if index == (list.size - 2) then + raise RemoteInstallationSkipped, "Installation of #{gem_name} skipped." + end + + specs_n_sources[index] + end + + def find_dependencies_not_installed(dependencies) + to_install = [] + dependencies.each do |dependency| + srcindex = Gem::SourceIndex.from_installed_gems + matches = srcindex.find_name(dependency.name, dependency.requirement_list) + to_install.push dependency if matches.empty? + end + to_install + end + + # Install all the given dependencies. Returns an array of + # Gem::Specification objects, one for each dependency installed. + # + # TODO: For now, we recursively install, but this is not the right + # way to do things (e.g. if a package fails to download, we + # shouldn't install anything). + def install_dependencies(dependencies, force, install_dir) + return if @options[:ignore_dependencies] + installed_gems = [] + dependencies.each do |dep| + if @options[:include_dependencies] || + ask_yes_no("Install required dependency #{dep.name}?", true) + remote_installer = RemoteInstaller.new @options + installed_gems << remote_installer.install(dep.name, + dep.version_requirements, + force, install_dir) + elsif force then + # ignore + else + raise DependencyError, "Required dependency #{dep.name} not installed" + end + end + installed_gems + end + + def download_gem(destination_file, source, spec) + return if File.exist? destination_file + uri = source + "/gems/#{spec.full_name}.gem" + response = Gem::RemoteFetcher.fetcher.fetch_path uri + write_gem_to_file response, destination_file + end + + def write_gem_to_file(body, destination_file) + FileUtils.mkdir_p(File.dirname(destination_file)) unless File.exist?(destination_file) + File.open(destination_file, 'wb') do |out| + out.write(body) + end + end + + def new_installer(gem) + return Installer.new(gem, @options) + end + end + +end diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb new file mode 100644 index 0000000000..4dfba4fa61 --- /dev/null +++ b/lib/rubygems/requirement.rb @@ -0,0 +1,157 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/version' + +## +# Requirement version includes a prefaced comparator in addition +# to a version number. +# +# A Requirement object can actually contain multiple, er, +# requirements, as in (> 1.2, < 2.0). +class Gem::Requirement + + include Comparable + + OPS = { + "=" => lambda { |v, r| v == r }, + "!=" => lambda { |v, r| v != r }, + ">" => lambda { |v, r| v > r }, + "<" => lambda { |v, r| v < r }, + ">=" => lambda { |v, r| v >= r }, + "<=" => lambda { |v, r| v <= r }, + "~>" => lambda { |v, r| v >= r && v < r.bump } + } + + OP_RE = /#{OPS.keys.map{ |k| Regexp.quote k }.join '|'}/o + + ## + # Factory method to create a Gem::Requirement object. Input may be a + # Version, a String, or nil. Intended to simplify client code. + # + # If the input is "weird", the default version requirement is returned. + # + def self.create(input) + case input + when Gem::Requirement then + input + when Gem::Version, Array then + new input + else + if input.respond_to? :to_str then + self.new [input.to_str] + else + self.default + end + end + end + + ## + # A default "version requirement" can surely _only_ be '>= 0'. + #-- + # This comment once said: + # + # "A default "version requirement" can surely _only_ be '> 0'." + def self.default + self.new ['>= 0'] + end + + ## + # Constructs a Requirement from +requirements+ which can be a String, a + # Gem::Version, or an Array of those. See parse for details on the + # formatting of requirement strings. + def initialize(requirements) + @requirements = case requirements + when Array then + requirements.map do |requirement| + parse(requirement) + end + else + [parse(requirements)] + end + @version = nil # Avoid warnings. + end + + # Marshal raw requirements, rather than the full object + def marshal_dump + [@requirements] + end + + # Load custom marshal format + def marshal_load(array) + @requirements = array[0] + @version = nil + end + + def to_s # :nodoc: + as_list.join(", ") + end + + def as_list + normalize + @requirements.collect { |req| + "#{req[0]} #{req[1]}" + } + end + + def normalize + return if not defined? @version or @version.nil? + @requirements = [parse(@version)] + @nums = nil + @version = nil + @op = nil + end + + ## + # Is the requirement satifised by +version+. + # + # version:: [Gem::Version] the version to compare against + # return:: [Boolean] true if this requirement is satisfied by + # the version, otherwise false + # + def satisfied_by?(version) + normalize + @requirements.all? { |op, rv| satisfy?(op, version, rv) } + end + + ## + # Is "version op required_version" satisfied? + # + def satisfy?(op, version, required_version) + OPS[op].call(version, required_version) + end + + ## + # Parse the version requirement obj returning the operator and version. + # + # The requirement can be a String or a Gem::Version. A String can be an + # operator (<, <=, =, =>, >, !=, ~>), a version number, or both, operator + # first. + def parse(obj) + case obj + when /^\s*(#{OP_RE})\s*([0-9.]+)\s*$/o then + [$1, Gem::Version.new($2)] + when /^\s*([0-9.]+)\s*$/ then + ['=', Gem::Version.new($1)] + when /^\s*(#{OP_RE})\s*$/o then + [$1, Gem::Version.new('0')] + when Gem::Version then + ['=', obj] + else + fail ArgumentError, "Illformed requirement [#{obj.inspect}]" + end + end + + def <=>(other) + to_s <=> other.to_s + end + + def hash # :nodoc: + to_s.hash + end + +end + diff --git a/lib/rubygems/rubygems_version.rb b/lib/rubygems/rubygems_version.rb new file mode 100644 index 0000000000..e01588ef2d --- /dev/null +++ b/lib/rubygems/rubygems_version.rb @@ -0,0 +1,6 @@ +# DO NOT EDIT +# This file is auto-generated by build scripts. +# See: rake update_version +module Gem + RubyGemsVersion = '0.9.4.6' +end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb new file mode 100644 index 0000000000..6f6586e9cf --- /dev/null +++ b/lib/rubygems/security.rb @@ -0,0 +1,785 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems/gem_openssl' + +# = Signed Gems README +# +# == Table of Contents +# * Overview +# * Walkthrough +# * Command-Line Options +# * OpenSSL Reference +# * Bugs/TODO +# * About the Author +# +# == Overview +# +# Gem::Security implements cryptographic signatures in RubyGems. The section +# below is a step-by-step guide to using signed gems and generating your own. +# +# == Walkthrough +# +# In order to start signing your gems, you'll need to build a private key and +# a self-signed certificate. Here's how: +# +# # build a private key and certificate for gemmaster@example.com +# $ gem cert --build gemmaster@example.com +# +# This could take anywhere from 5 seconds to 10 minutes, depending on the +# speed of your computer (public key algorithms aren't exactly the speediest +# crypto algorithms in the world). When it's finished, you'll see the files +# "gem-private_key.pem" and "gem-public_cert.pem" in the current directory. +# +# First things first: take the "gem-private_key.pem" file and move it +# somewhere private, preferably a directory only you have access to, a floppy +# (yuck!), a CD-ROM, or something comparably secure. Keep your private key +# hidden; if it's compromised, someone can sign packages as you (note: PKI has +# ways of mitigating the risk of stolen keys; more on that later). +# +# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but +# you can use whatever gem you'd like. Open up your existing gemspec file and +# add the following lines: +# +# # signing key and certificate chain +# s.signing_key = '/mnt/floppy/gem-private_key.pem' +# s.cert_chain = ['gem-public_cert.pem'] +# +# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private +# key). +# +# After that, go ahead and build your gem as usual. Congratulations, you've +# just built your first signed gem! If you peek inside your gem file, you'll +# see a couple of new files have been added: +# +# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem +# data.tar.gz +# data.tar.gz.sig +# metadata.gz +# metadata.gz.sig +# +# Now let's verify the signature. Go ahead and install the gem, but add the +# following options: "-P HighSecurity", like this: +# +# # install the gem with using the security policy "HighSecurity" +# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# +# The -P option sets your security policy -- we'll talk about that in just a +# minute. Eh, what's this? +# +# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem' +# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't +# verify data signature: Untrusted Signing Chain Root: cert = +# '/CN=gemmaster/DC=example/DC=com', error = 'path +# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem" +# does not exist' +# +# The culprit here is the security policy. RubyGems has several different +# security policies. Let's take a short break and go over the security +# policies. Here's a list of the available security policies, and a brief +# description of each one: +# +# * NoSecurity - Well, no security at all. Signed packages are treated like +# unsigned packages. +# * LowSecurity - Pretty much no security. If a package is signed then +# RubyGems will make sure the signature matches the signing +# certificate, and that the signing certificate hasn't expired, but +# that's it. A malicious user could easily circumvent this kind of +# security. +# * MediumSecurity - Better than LowSecurity and NoSecurity, but still +# fallible. Package contents are verified against the signing +# certificate, and the signing certificate is checked for validity, +# and checked against the rest of the certificate chain (if you don't +# know what a certificate chain is, stay tuned, we'll get to that). +# The biggest improvement over LowSecurity is that MediumSecurity +# won't install packages that are signed by untrusted sources. +# Unfortunately, MediumSecurity still isn't totally secure -- a +# malicious user can still unpack the gem, strip the signatures, and +# distribute the gem unsigned. +# * HighSecurity - Here's the bugger that got us into this mess. +# The HighSecurity policy is identical to the MediumSecurity policy, +# except that it does not allow unsigned gems. A malicious user +# doesn't have a whole lot of options here; he can't modify the +# package contents without invalidating the signature, and he can't +# modify or remove signature or the signing certificate chain, or +# RubyGems will simply refuse to install the package. Oh well, maybe +# he'll have better luck causing problems for CPAN users instead :). +# +# So, the reason RubyGems refused to install our shiny new signed gem was +# because it was from an untrusted source. Well, my code is infallible +# (hah!), so I'm going to add myself as a trusted source. +# +# Here's how: +# +# # add trusted certificate +# gem cert --add gem-public_cert.pem +# +# I've added my public certificate as a trusted source. Now I can install +# packages signed my private key without any hassle. Let's try the install +# command above again: +# +# # install the gem with using the HighSecurity policy (and this time +# # without any shenanigans) +# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity +# +# This time RubyGems should accept your signed package and begin installing. +# While you're waiting for RubyGems to work it's magic, have a look at some of +# the other security commands: +# +# Usage: gem cert [options] +# +# Options: +# -a, --add CERT Add a trusted certificate. +# -l, --list List trusted certificates. +# -r, --remove STRING Remove trusted certificates containing STRING. +# -b, --build EMAIL_ADDR Build private key and self-signed certificate +# for EMAIL_ADDR. +# -C, --certificate CERT Certificate for --sign command. +# -K, --private-key KEY Private key for --sign command. +# -s, --sign NEWCERT Sign a certificate with my key and certificate. +# +# (By the way, you can pull up this list any time you'd like by typing "gem +# cert --help") +# +# Hmm. We've already covered the "--build" option, and the "--add", "--list", +# and "--remove" commands seem fairly straightforward; they allow you to add, +# list, and remove the certificates in your trusted certificate list. But +# what's with this "--sign" option? +# +# To answer that question, let's take a look at "certificate chains", a +# concept I mentioned earlier. There are a couple of problems with +# self-signed certificates: first of all, self-signed certificates don't offer +# a whole lot of security. Sure, the certificate says Yukihiro Matsumoto, but +# how do I know it was actually generated and signed by matz himself unless he +# gave me the certificate in person? +# +# The second problem is scalability. Sure, if there are 50 gem authors, then +# I have 50 trusted certificates, no problem. What if there are 500 gem +# authors? 1000? Having to constantly add new trusted certificates is a +# pain, and it actually makes the trust system less secure by encouraging +# RubyGems users to blindly trust new certificates. +# +# Here's where certificate chains come in. A certificate chain establishes an +# arbitrarily long chain of trust between an issuing certificate and a child +# certificate. So instead of trusting certificates on a per-developer basis, +# we use the PKI concept of certificate chains to build a logical hierarchy of +# trust. Here's a hypothetical example of a trust hierarchy based (roughly) +# on geography: +# +# +# -------------------------- +# | rubygems@rubyforge.org | +# -------------------------- +# | +# ----------------------------------- +# | | +# ---------------------------- ----------------------------- +# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com | +# ---------------------------- ----------------------------- +# | | | | +# --------------- ---------------- ----------- -------------- +# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc | +# --------------- ---------------- ----------- -------------- +# +# +# Now, rather than having 4 trusted certificates (one for alf@seattle, +# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1 +# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works: +# +# I install "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've +# never heard of "alf@seattle", but his certificate has a valid signature from +# the "seattle.rb@zenspider.com" certificate, which in turn has a valid +# signature from the "rubygems@rubyforge.org" certificate. Voila! At this +# point, it's much more reasonable for me to trust a package signed by +# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org", +# which I do trust. +# +# And the "--sign" option allows all this to happen. A developer creates +# their build certificate with the "--build" option, then has their +# certificate signed by taking it with them to their next regional Ruby meetup +# (in our hypothetical example), and it's signed there by the person holding +# the regional RubyGems signing certificate, which is signed at the next +# RubyConf by the holder of the top-level RubyGems certificate. At each point +# the issuer runs the same command: +# +# # sign a certificate with the specified key and certificate +# # (note that this modifies client_cert.pem!) +# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem +# --sign client_cert.pem +# +# Then the holder of issued certificate (in this case, our buddy +# "alf@seattle"), can start using this signed certificate to sign RubyGems. +# By the way, in order to let everyone else know about his new fancy signed +# certificate, "alf@seattle" would change his gemspec file to look like this: +# +# # signing key (still kept in an undisclosed location!) +# s.signing_key = '/mnt/floppy/alf-private_key.pem' +# +# # certificate chain (includes the issuer certificate now too) +# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem', +# '/home/alf/doc/alf_at_seattle-public_cert.pem'] +# +# Obviously, this RubyGems trust infrastructure doesn't exist yet. Also, in +# the "real world" issuers actually generate the child certificate from a +# certificate request, rather than sign an existing certificate. And our +# hypothetical infrastructure is missing a certificate revocation system. +# These are that can be fixed in the future... +# +# I'm sure your new signed gem has finished installing by now (unless you're +# installing rails and all it's dependencies, that is ;D). At this point you +# should know how to do all of these new and interesting things: +# +# * build a gem signing key and certificate +# * modify your existing gems to support signing +# * adjust your security policy +# * modify your trusted certificate list +# * sign a certificate +# +# If you've got any questions, feel free to contact me at the email address +# below. The next couple of sections +# +# +# == Command-Line Options +# +# Here's a brief summary of the certificate-related command line options: +# +# gem install +# -P, --trust-policy POLICY Specify gem trust policy. +# +# gem cert +# -a, --add CERT Add a trusted certificate. +# -l, --list List trusted certificates. +# -r, --remove STRING Remove trusted certificates containing +# STRING. +# -b, --build EMAIL_ADDR Build private key and self-signed +# certificate for EMAIL_ADDR. +# -C, --certificate CERT Certificate for --sign command. +# -K, --private-key KEY Private key for --sign command. +# -s, --sign NEWCERT Sign a certificate with my key and +# certificate. +# +# A more detailed description of each options is available in the walkthrough +# above. +# +# +# == OpenSSL Reference +# +# The .pem files generated by --build and --sign are just basic OpenSSL PEM +# files. Here's a couple of useful commands for manipulating them: +# +# # convert a PEM format X509 certificate into DER format: +# # (note: Windows .cer files are X509 certificates in DER format) +# $ openssl x509 -in input.pem -outform der -out output.der +# +# # print out the certificate in a human-readable format: +# $ openssl x509 -in input.pem -noout -text +# +# And you can do the same thing with the private key file as well: +# +# # convert a PEM format RSA key into DER format: +# $ openssl rsa -in input_key.pem -outform der -out output_key.der +# +# # print out the key in a human readable format: +# $ openssl rsa -in input_key.pem -noout -text +# +# == Bugs/TODO +# +# * There's no way to define a system-wide trust list. +# * custom security policies (from a YAML file, etc) +# * Simple method to generate a signed certificate request +# * Support for OCSP, SCVP, CRLs, or some other form of cert +# status check (list is in order of preference) +# * Support for encrypted private keys +# * Some sort of semi-formal trust hierarchy (see long-winded explanation +# above) +# * Path discovery (for gem certificate chains that don't have a self-signed +# root) -- by the way, since we don't have this, THE ROOT OF THE CERTIFICATE +# CHAIN MUST BE SELF SIGNED if Policy#verify_root is true (and it is for the +# MediumSecurity and HighSecurity policies) +# * Better explanation of X509 naming (ie, we don't have to use email +# addresses) +# * Possible alternate signing mechanisms (eg, via PGP). this could be done +# pretty easily by adding a :signing_type attribute to the gemspec, then add +# the necessary support in other places +# * Honor AIA field (see note about OCSP above) +# * Maybe honor restriction extensions? +# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 +# file, instead of an array embedded in the metadata. ideas? +# * Possibly embed signature and key algorithms into metadata (right now +# they're assumed to be the same as what's set in Gem::Security::OPT) +# +# == About the Author +# +# Paul Duncan <pabs@pablotron.org> +# http://pablotron.org/ + +module Gem::Security + + class Exception < Exception; end + + # + # default options for most of the methods below + # + OPT = { + # private key options + :key_algo => Gem::SSL::PKEY_RSA, + :key_size => 2048, + + # public cert options + :cert_age => 365 * 24 * 3600, # 1 year + :dgst_algo => Gem::SSL::DIGEST_SHA1, + + # x509 certificate extensions + :cert_exts => { + 'basicConstraints' => 'CA:FALSE', + 'subjectKeyIdentifier' => 'hash', + 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature', + }, + + # save the key and cert to a file in build_self_signed_cert()? + :save_key => true, + :save_cert => true, + + # if you define either of these, then they'll be used instead of + # the output_fmt macro below + :save_key_path => nil, + :save_cert_path => nil, + + # output name format for self-signed certs + :output_fmt => 'gem-%s.pem', + :munge_re => Regexp.new(/[^a-z0-9_.-]+/), + + # output directory for trusted certificate checksums + :trust_dir => File::join(Gem.user_home, '.gem', 'trust'), + + # default permissions for trust directory and certs + :perms => { + :trust_dir => 0700, + :trusted_cert => 0600, + :signing_cert => 0600, + :signing_key => 0600, + }, + } + + # + # A Gem::Security::Policy object encapsulates the settings for verifying + # signed gem files. This is the base class. You can either declare an + # instance of this or use one of the preset security policies below. + # + class Policy + attr_accessor :verify_data, :verify_signer, :verify_chain, + :verify_root, :only_trusted, :only_signed + + # + # Create a new Gem::Security::Policy object with the given mode and + # options. + # + def initialize(policy = {}, opt = {}) + # set options + @opt = Gem::Security::OPT.merge(opt) + + # build policy + policy.each_pair do |key, val| + case key + when :verify_data then @verify_data = val + when :verify_signer then @verify_signer = val + when :verify_chain then @verify_chain = val + when :verify_root then @verify_root = val + when :only_trusted then @only_trusted = val + when :only_signed then @only_signed = val + end + end + end + + # + # Get the path to the file for this cert. + # + def self.trusted_cert_path(cert, opt = {}) + opt = Gem::Security::OPT.merge(opt) + + # get digest algorithm, calculate checksum of root.subject + algo = opt[:dgst_algo] + dgst = algo.hexdigest(cert.subject.to_s) + + # build path to trusted cert file + name = "cert-#{dgst}.pem" + + # join and return path components + File::join(opt[:trust_dir], name) + end + + # + # Verify that the gem data with the given signature and signing chain + # matched this security policy at the specified time. + # + def verify_gem(signature, data, chain, time = Time.now) + Gem.ensure_ssl_available + cert_class = OpenSSL::X509::Certificate + exc = Gem::Security::Exception + chain ||= [] + + chain = chain.map{ |str| cert_class.new(str) } + signer, ch_len = chain[-1], chain.size + + # make sure signature is valid + if @verify_data + # get digest algorithm (TODO: this should be configurable) + dgst = @opt[:dgst_algo] + + # verify the data signature (this is the most important part, so don't + # screw it up :D) + v = signer.public_key.verify(dgst.new, signature, data) + raise exc, "Invalid Gem Signature" unless v + + # make sure the signer is valid + if @verify_signer + # make sure the signing cert is valid right now + v = signer.check_validity(nil, time) + raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid] + end + end + + # make sure the certificate chain is valid + if @verify_chain + # iterate down over the chain and verify each certificate against it's + # issuer + (ch_len - 1).downto(1) do |i| + issuer, cert = chain[i - 1, 2] + v = cert.check_validity(issuer, time) + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Invalid Signing Chain', cert.subject, v[:desc] + ] unless v[:is_valid] + end + + # verify root of chain + if @verify_root + # make sure root is self-signed + root = chain[0] + raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [ + 'Invalid Signing Chain Root', + 'Subject does not match Issuer for Gem Signing Chain', + root.subject.to_s, + root.issuer.to_s, + ] unless root.issuer.to_s == root.subject.to_s + + # make sure root is valid + v = root.check_validity(root, time) + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Invalid Signing Chain Root', root.subject, v[:desc] + ] unless v[:is_valid] + + # verify that the chain root is trusted + if @only_trusted + # get digest algorithm, calculate checksum of root.subject + algo = @opt[:dgst_algo] + path = Gem::Security::Policy.trusted_cert_path(root, @opt) + + # check to make sure trusted path exists + raise exc, "%s: cert = '%s', error = '%s'" % [ + 'Untrusted Signing Chain Root', + root.subject.to_s, + "path \"#{path}\" does not exist", + ] unless File.exist?(path) + + # load calculate digest from saved cert file + save_cert = OpenSSL::X509::Certificate.new(File.read(path)) + save_dgst = algo.digest(save_cert.public_key.to_s) + + # create digest of public key + pkey_str = root.public_key.to_s + cert_dgst = algo.digest(pkey_str) + + # now compare the two digests, raise exception + # if they don't match + raise exc, "%s: %s (saved = '%s', root = '%s')" % [ + 'Invalid Signing Chain Root', + "Saved checksum doesn't match root checksum", + save_dgst, cert_dgst, + ] unless save_dgst == cert_dgst + end + end + + # return the signing chain + chain.map { |cert| cert.subject } + end + end + end + + # + # No security policy: all package signature checks are disabled. + # + NoSecurity = Policy.new( + :verify_data => false, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # AlmostNo security policy: only verify that the signing certificate is the + # one that actually signed the data. Make no attempt to verify the signing + # certificate chain. + # + # This policy is basically useless. better than nothing, but can still be + # easily spoofed, and is not recommended. + # + AlmostNoSecurity = Policy.new( + :verify_data => true, + :verify_signer => false, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # Low security policy: only verify that the signing certificate is actually + # the gem signer, and that the signing certificate is valid. + # + # This policy is better than nothing, but can still be easily spoofed, and + # is not recommended. + # + LowSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => false, + :verify_root => false, + :only_trusted => false, + :only_signed => false + ) + + # + # Medium security policy: verify the signing certificate, verify the signing + # certificate chain all the way to the root certificate, and only trust root + # certificates that we have explicity allowed trust for. + # + # This security policy is reasonable, but it allows unsigned packages, so a + # malicious person could simply delete the package signature and pass the + # gem off as unsigned. + # + MediumSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => false + ) + + # + # High security policy: only allow signed gems to be installed, verify the + # signing certificate, verify the signing certificate chain all the way to + # the root certificate, and only trust root certificates that we have + # explicity allowed trust for. + # + # This security policy is significantly more difficult to bypass, and offers + # a reasonable guarantee that the contents of the gem have not been altered. + # + HighSecurity = Policy.new( + :verify_data => true, + :verify_signer => true, + :verify_chain => true, + :verify_root => true, + :only_trusted => true, + :only_signed => true + ) + + # + # Hash of configured security policies + # + Policies = { + 'NoSecurity' => NoSecurity, + 'AlmostNoSecurity' => AlmostNoSecurity, + 'LowSecurity' => LowSecurity, + 'MediumSecurity' => MediumSecurity, + 'HighSecurity' => HighSecurity, + } + + # + # Sign the cert cert with @signing_key and @signing_cert, using the digest + # algorithm opt[:dgst_algo]. Returns the newly signed certificate. + # + def self.sign_cert(cert, signing_key, signing_cert, opt = {}) + opt = OPT.merge(opt) + + # set up issuer information + cert.issuer = signing_cert.subject + cert.sign(signing_key, opt[:dgst_algo].new) + + cert + end + + # + # Make sure the trust directory exists. If it does exist, make sure it's + # actually a directory. If not, then create it with the appropriate + # permissions. + # + def self.verify_trust_dir(path, perms) + # if the directory exists, then make sure it is in fact a directory. if + # it doesn't exist, then create it with the appropriate permissions + if File.exist?(path) + # verify that the trust directory is actually a directory + unless File.directory?(path) + err = "trust directory #{path} isn't a directory" + raise Gem::Security::Exception, err + end + else + # trust directory doesn't exist, so create it with permissions + FileUtils.mkdir_p(path) + FileUtils.chmod(perms, path) + end + end + + # + # Build a certificate from the given DN and private key. + # + def self.build_cert(name, key, opt = {}) + Gem.ensure_ssl_available + opt = OPT.merge(opt) + + # create new cert + ret = OpenSSL::X509::Certificate.new + + # populate cert attributes + ret.version = 2 + ret.serial = 0 + ret.public_key = key.public_key + ret.not_before = Time.now + ret.not_after = Time.now + opt[:cert_age] + ret.subject = name + + # add certificate extensions + ef = OpenSSL::X509::ExtensionFactory.new(nil, ret) + ret.extensions = opt[:cert_exts].map { |k, v| ef.create_extension(k, v) } + + # sign cert + i_key, i_cert = opt[:issuer_key] || key, opt[:issuer_cert] || ret + ret = sign_cert(ret, i_key, i_cert, opt) + + # return cert + ret + end + + # + # Build a self-signed certificate for the given email address. + # + def self.build_self_signed_cert(email_addr, opt = {}) + Gem.ensure_ssl_available + opt = OPT.merge(opt) + path = { :key => nil, :cert => nil } + + # split email address up + cn, dcs = email_addr.split('@') + dcs = dcs.split('.') + + # munge email CN and DCs + cn = cn.gsub(opt[:munge_re], '_') + dcs = dcs.map { |dc| dc.gsub(opt[:munge_re], '_') } + + # create DN + name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/') + name = OpenSSL::X509::Name::parse(name) + + # build private key + key = opt[:key_algo].new(opt[:key_size]) + + # method name pretty much says it all :) + verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + + # if we're saving the key, then write it out + if opt[:save_key] + path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key') + File.open(path[:key], 'wb') do |file| + file.chmod(opt[:perms][:signing_key]) + file.write(key.to_pem) + end + end + + # build self-signed public cert from key + cert = build_cert(name, key, opt) + + # if we're saving the cert, then write it out + if opt[:save_cert] + path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert') + File.open(path[:cert], 'wb') do |file| + file.chmod(opt[:perms][:signing_cert]) + file.write(cert.to_pem) + end + end + + # return key, cert, and paths (if applicable) + { :key => key, :cert => cert, + :key_path => path[:key], :cert_path => path[:cert] } + end + + # + # Add certificate to trusted cert list. + # + # Note: At the moment these are stored in OPT[:trust_dir], although that + # directory may change in the future. + # + def self.add_trusted_cert(cert, opt = {}) + opt = OPT.merge(opt) + + # get destination path + path = Gem::Security::Policy.trusted_cert_path(cert, opt) + + # verify trust directory (can't write to nowhere, you know) + verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir]) + + # write cert to output file + File.open(path, 'wb') do |file| + file.chmod(opt[:perms][:trusted_cert]) + file.write(cert.to_pem) + end + + # return nil + nil + end + + # + # Basic OpenSSL-based package signing class. + # + class Signer + attr_accessor :key, :cert_chain + + def initialize(key, cert_chain) + Gem.ensure_ssl_available + @algo = Gem::Security::OPT[:dgst_algo] + @key, @cert_chain = key, cert_chain + + # check key, if it's a file, and if it's key, leave it alone + if @key && !@key.kind_of?(OpenSSL::PKey::PKey) + @key = OpenSSL::PKey::RSA.new(File.read(@key)) + end + + # check cert chain, if it's a file, load it, if it's cert data, convert + # it into a cert object, and if it's a cert object, leave it alone + if @cert_chain + @cert_chain = @cert_chain.map do |cert| + # check cert, if it's a file, load it, if it's cert data, convert it + # into a cert object, and if it's a cert object, leave it alone + if cert && !cert.kind_of?(OpenSSL::X509::Certificate) + cert = File.read(cert) if File::exist?(cert) + cert = OpenSSL::X509::Certificate.new(cert) + end + cert + end + end + end + + # + # Sign data with given digest algorithm + # + def sign(data) + @key.sign(@algo.new, data) + end + + end +end + diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb new file mode 100644 index 0000000000..212ccc5f7e --- /dev/null +++ b/lib/rubygems/server.rb @@ -0,0 +1,504 @@ +require 'webrick' +require 'rdoc/template' +require 'yaml' +require 'zlib' + +require 'rubygems' + +## +# Gem::Server and allows users to serve gems for consumption by +# `gem --remote-install`. +# +# gem_server starts an HTTP server on the given port and serves the folowing: +# * "/" - Browsing of gem spec files for installed gems +# * "/Marshal" - Full SourceIndex dump of metadata for installed gems +# * "/yaml" - YAML dump of metadata for installed gems - deprecated +# * "/gems" - Direct access to download the installable gems +# +# == Usage +# +# gem server [-p portnum] [-d gem_path] +# +# port_num:: The TCP port the HTTP server will bind to +# gem_path:: +# Root gem directory containing both "cache" and "specifications" +# subdirectories. +class Gem::Server + + include Gem::UserInteraction + + DOC_TEMPLATE = <<-WEBPAGE +<?xml version="1.0" encoding="iso-8859-1"?> +<!DOCTYPE html + PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>RubyGems Documentation Index</title> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> + <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> +</head> +<body> + <div id="fileHeader"> + <h1>RubyGems Documentation Index</h1> + </div> + <!-- banner header --> + +<div id="bodyContent"> + <div id="contextContent"> + <div id="description"> + <h1>Summary</h1> +<p>There are %gem_count% gems installed:</p> +<p> +START:specs +IFNOT:is_last +<a href="#%name%">%name%</a>, +ENDIF:is_last +IF:is_last +<a href="#%name%">%name%</a>. +ENDIF:is_last +END:specs +<h1>Gems</h1> + +<dl> +START:specs +<dt> +IF:first_name_entry + <a name="%name%"></a> +ENDIF:first_name_entry +<b>%name% %version%</b> +IF:rdoc_installed + <a href="%doc_path%">[rdoc]</a> +ENDIF:rdoc_installed +IFNOT:rdoc_installed + <span title="rdoc not installed">[rdoc]</span> +ENDIF:rdoc_installed +IF:homepage +<a href="%homepage%" title="%homepage%">[www]</a> +ENDIF:homepage +IFNOT:homepage +<span title="no homepage available">[www]</span> +ENDIF:homepage +IF:has_deps + - depends on +START:dependencies +IFNOT:is_last +<a href="#%name%" title="%version%">%name%</a>, +ENDIF:is_last +IF:is_last +<a href="#%name%" title="%version%">%name%</a>. +ENDIF:is_last +END:dependencies +ENDIF:has_deps +</dt> +<dd> +%summary% +IF:executables + <br/> + +IF:only_one_executable + Executable is +ENDIF:only_one_executable + +IFNOT:only_one_executable + Executables are +ENDIF:only_one_executable + +START:executables +IFNOT:is_last + <span class="context-item-name">%executable%</span>, +ENDIF:is_last +IF:is_last + <span class="context-item-name">%executable%</span>. +ENDIF:is_last +END:executables +ENDIF:executables +<br/> +<br/> +</dd> +END:specs +</dl> + + </div> + </div> + </div> +<div id="validator-badges"> + <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> +</div> +</body> +</html> + WEBPAGE + + # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 + RDOC_CSS = <<-RDOCCSS +body { + font-family: Verdana,Arial,Helvetica,sans-serif; + font-size: 90%; + margin: 0; + margin-left: 40px; + padding: 0; + background: white; +} + +h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } +h1 { font-size: 150%; } +h2,h3,h4 { margin-top: 1em; } + +a { background: #eef; color: #039; text-decoration: none; } +a:hover { background: #039; color: #eef; } + +/* Override the base stylesheets Anchor inside a table cell */ +td > a { + background: transparent; + color: #039; + text-decoration: none; +} + +/* and inside a section title */ +.section-title > a { + background: transparent; + color: #eee; + text-decoration: none; +} + +/* === Structural elements =================================== */ + +div#index { + margin: 0; + margin-left: -40px; + padding: 0; + font-size: 90%; +} + + +div#index a { + margin-left: 0.7em; +} + +div#index .section-bar { + margin-left: 0px; + padding-left: 0.7em; + background: #ccc; + font-size: small; +} + + +div#classHeader, div#fileHeader { + width: auto; + color: white; + padding: 0.5em 1.5em 0.5em 1.5em; + margin: 0; + margin-left: -40px; + border-bottom: 3px solid #006; +} + +div#classHeader a, div#fileHeader a { + background: inherit; + color: white; +} + +div#classHeader td, div#fileHeader td { + background: inherit; + color: white; +} + + +div#fileHeader { + background: #057; +} + +div#classHeader { + background: #048; +} + + +.class-name-in-header { + font-size: 180%; + font-weight: bold; +} + + +div#bodyContent { + padding: 0 1.5em 0 1.5em; +} + +div#description { + padding: 0.5em 1.5em; + background: #efefef; + border: 1px dotted #999; +} + +div#description h1,h2,h3,h4,h5,h6 { + color: #125;; + background: transparent; +} + +div#validator-badges { + text-align: center; +} +div#validator-badges img { border: 0; } + +div#copyright { + color: #333; + background: #efefef; + font: 0.75em sans-serif; + margin-top: 5em; + margin-bottom: 0; + padding: 0.5em 2em; +} + + +/* === Classes =================================== */ + +table.header-table { + color: white; + font-size: small; +} + +.type-note { + font-size: small; + color: #DEDEDE; +} + +.xxsection-bar { + background: #eee; + color: #333; + padding: 3px; +} + +.section-bar { + color: #333; + border-bottom: 1px solid #999; + margin-left: -20px; +} + + +.section-title { + background: #79a; + color: #eee; + padding: 3px; + margin-top: 2em; + margin-left: -30px; + border: 1px solid #999; +} + +.top-aligned-row { vertical-align: top } +.bottom-aligned-row { vertical-align: bottom } + +/* --- Context section classes ----------------------- */ + +.context-row { } +.context-item-name { font-family: monospace; font-weight: bold; color: black; } +.context-item-value { font-size: small; color: #448; } +.context-item-desc { color: #333; padding-left: 2em; } + +/* --- Method classes -------------------------- */ +.method-detail { + background: #efefef; + padding: 0; + margin-top: 0.5em; + margin-bottom: 1em; + border: 1px dotted #ccc; +} +.method-heading { + color: black; + background: #ccc; + border-bottom: 1px solid #666; + padding: 0.2em 0.5em 0 0.5em; +} +.method-signature { color: black; background: inherit; } +.method-name { font-weight: bold; } +.method-args { font-style: italic; } +.method-description { padding: 0 0.5em 0 0.5em; } + +/* --- Source code sections -------------------- */ + +a.source-toggle { font-size: 90%; } +div.method-source-code { + background: #262626; + color: #ffdead; + margin: 1em; + padding: 0.5em; + border: 1px dashed #999; + overflow: hidden; +} + +div.method-source-code pre { color: #ffdead; overflow: hidden; } + +/* --- Ruby keyword styles --------------------- */ + +.standalone-code { background: #221111; color: #ffdead; overflow: hidden; } + +.ruby-constant { color: #7fffd4; background: transparent; } +.ruby-keyword { color: #00ffff; background: transparent; } +.ruby-ivar { color: #eedd82; background: transparent; } +.ruby-operator { color: #00ffee; background: transparent; } +.ruby-identifier { color: #ffdead; background: transparent; } +.ruby-node { color: #ffa07a; background: transparent; } +.ruby-comment { color: #b22222; font-weight: bold; background: transparent; } +.ruby-regexp { color: #ffa07a; background: transparent; } +.ruby-value { color: #7fffd4; background: transparent; } + RDOCCSS + + def self.run(options) + new(options[:gemdir], options[:port], options[:daemon]).run + end + + def initialize(gemdir, port, daemon) + Socket.do_not_reverse_lookup = true + + @gemdir = gemdir + @port = port + @daemon = daemon + logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL + @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger + + @spec_dir = File.join @gemdir, "specifications" + @source_index = Gem::SourceIndex.from_gems_in @spec_dir + end + + def quick(req, res) + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + + case req.request_uri.request_uri + when '/quick/index' then + res.body << @source_index.map { |name,_| name }.join("\n") + when '/quick/index.rz' then + index = @source_index.map { |name,_| name }.join("\n") + res.body << Zlib::Deflate.deflate(index) + when %r|^/quick/(.*)-([0-9.]+)\.gemspec(\.marshal)?\.rz$| then + specs = @source_index.search $1, $2 + if specs.empty? then + res.status = 404 + elsif specs.length > 1 then + res.status = 500 + elsif $3 # marshal quickindex instead of YAML + res.body << Zlib::Deflate.deflate(Marshal.dump(specs.first)) + else # deprecated YAML format + res.body << Zlib::Deflate.deflate(specs.first.to_yaml) + end + else + res.status = 404 + end + end + + def run + @server.listen nil, @port + + say "Starting gem server on http://localhost:#{@port}/" + + WEBrick::Daemon.start if @daemon + + @server.mount_proc("/yaml") do |req, res| + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + if req.request_method == 'HEAD' then + res['content-length'] = @source_index.to_yaml.length + else + res.body << @source_index.to_yaml + end + end + + @server.mount_proc("/Marshal") do |req, res| + res['content-type'] = 'text/plain' + res['date'] = File.stat(@spec_dir).mtime + if req.request_method == 'HEAD' then + res['content-length'] = Marshal.dump(@source_index).length + else + res.body << Marshal.dump(@source_index) + end + end + + @server.mount_proc("/quick/", &method(:quick)) + + @server.mount_proc("/gem-server-rdoc-style.css") do |req, res| + res['content-type'] = 'text/css' + res['date'] = File.stat(@spec_dir).mtime + res.body << RDOC_CSS + end + + @server.mount_proc("/") do |req, res| + specs = [] + total_file_count = 0 + + @source_index.each do |path, spec| + total_file_count += spec.files.size + deps = spec.dependencies.collect { |dep| + { "name" => dep.name, + "version" => dep.version_requirements.to_s, } + } + deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] } + deps.last["is_last"] = true unless deps.empty? + + # executables + executables = spec.executables.sort.collect { |exec| {"executable" => exec} } + executables = nil if executables.empty? + executables.last["is_last"] = true if executables + + specs << { + "authors" => spec.authors.sort.join(", "), + "date" => spec.date.to_s, + "dependencies" => deps, + "doc_path" => ('/doc_root/' + spec.full_name + '/rdoc/index.html'), + "executables" => executables, + "only_one_executable" => (executables && executables.size==1), + "full_name" => spec.full_name, + "has_deps" => !deps.empty?, + "homepage" => spec.homepage, + "name" => spec.name, + "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?, + "summary" => spec.summary, + "version" => spec.version.to_s, + } + end + + specs << { + "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others", + "dependencies" => [], + "doc_path" => "/doc_root/rubygems-#{Gem::RubyGemsVersion}/rdoc/index.html", + "executables" => [{"executable" => 'gem', "is_last" => true}], + "only_one_executable" => true, + "full_name" => "rubygems-#{Gem::RubyGemsVersion}", + "has_deps" => false, + "homepage" => "http://rubygems.org/", + "name" => 'rubygems', + "rdoc_installed" => true, + "summary" => "RubyGems itself", + "version" => Gem::RubyGemsVersion, + } + + specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] } + specs.last["is_last"] = true + + # tag all specs with first_name_entry + last_spec = nil + specs.each do |spec| + is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase) + spec["first_name_entry"] = is_first + last_spec = spec + end + + # create page from template + template = TemplatePage.new(DOC_TEMPLATE) + res['content-type'] = 'text/html' + template.write_html_on res.body, + "gem_count" => specs.size.to_s, "specs" => specs, + "total_file_count" => total_file_count.to_s + end + + paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" } + paths.each do |mount_point, mount_dir| + @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, + File.join(@gemdir, mount_dir), true) + end + + trap("INT") { @server.shutdown; exit! } + trap("TERM") { @server.shutdown; exit! } + + @server.start + end + +end + diff --git a/lib/rubygems/source_index.rb b/lib/rubygems/source_index.rb new file mode 100644 index 0000000000..759718d45c --- /dev/null +++ b/lib/rubygems/source_index.rb @@ -0,0 +1,446 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'forwardable' + +require 'rubygems' +require 'rubygems/user_interaction' +require 'rubygems/specification' + +module Gem + + # The SourceIndex object indexes all the gems available from a + # particular source (e.g. a list of gem directories, or a remote + # source). A SourceIndex maps a gem full name to a gem + # specification. + # + # NOTE:: The class used to be named Cache, but that became + # confusing when cached source fetchers where introduced. The + # constant Gem::Cache is an alias for this class to allow old + # YAMLized source index objects to load properly. + # + class SourceIndex + extend Forwardable + + include Enumerable + + include Gem::UserInteraction + + # Class Methods. ------------------------------------------------- + class << self + include Gem::UserInteraction + + # Factory method to construct a source index instance for a given + # path. + # + # deprecated:: + # If supplied, from_installed_gems will act just like + # +from_gems_in+. This argument is deprecated and is provided + # just for backwards compatibility, and should not generally + # be used. + # + # return:: + # SourceIndex instance + # + def from_installed_gems(*deprecated) + if deprecated.empty? + from_gems_in(*installed_spec_directories) + else + from_gems_in(*deprecated) + end + end + + # Return a list of directories in the current gem path that + # contain specifications. + # + # return:: + # List of directory paths (all ending in "../specifications"). + # + def installed_spec_directories + Gem.path.collect { |dir| File.join(dir, "specifications") } + end + + # Factory method to construct a source index instance for a + # given path. + # + # spec_dirs:: + # List of directories to search for specifications. Each + # directory should have a "specifications" subdirectory + # containing the gem specifications. + # + # return:: + # SourceIndex instance + # + def from_gems_in(*spec_dirs) + self.new.load_gems_in(*spec_dirs) + end + + # Load a specification from a file (eval'd Ruby code) + # + # file_name:: [String] The .gemspec file + # return:: Specification instance or nil if an error occurs + # + def load_specification(file_name) + begin + spec_code = File.read(file_name).untaint + gemspec = eval spec_code, binding, file_name + if gemspec.is_a?(Gem::Specification) + gemspec.loaded_from = file_name + return gemspec + end + alert_warning "File '#{file_name}' does not evaluate to a gem specification" + rescue SyntaxError => e + alert_warning e + alert_warning spec_code + rescue Exception => e + alert_warning(e.inspect.to_s + "\n" + spec_code) + alert_warning "Invalid .gemspec format in '#{file_name}'" + end + return nil + end + + end + + # Instance Methods ----------------------------------------------- + + # Constructs a source index instance from the provided + # specifications + # + # specifications:: + # [Hash] hash of [Gem name, Gem::Specification] pairs + # + def initialize(specifications={}) + @gems = specifications + end + + # Reconstruct the source index from the list of source + # directories. + def load_gems_in(*spec_dirs) + @gems.clear + specs = Dir.glob File.join("{#{spec_dirs.join(',')}}", "*.gemspec") + specs.each do |file_name| + gemspec = self.class.load_specification(file_name.untaint) + add_spec(gemspec) if gemspec + end + self + end + + # Returns a Hash of name => Specification of the latest versions of each + # gem in this index. + def latest_specs + result, latest = Hash.new { |h,k| h[k] = [] }, {} + + self.each do |_, spec| # SourceIndex is not a hash, so we're stuck with each + name = spec.name + curr_ver = spec.version + prev_ver = latest[name] + + next unless prev_ver.nil? or curr_ver >= prev_ver + + if prev_ver.nil? or curr_ver > prev_ver then + result[name].clear + latest[name] = curr_ver + end + + result[name] << spec + end + + result.values.flatten + end + + # Add a gem specification to the source index. + def add_spec(gem_spec) + @gems[gem_spec.full_name] = gem_spec + end + + # Remove a gem specification named +full_name+. + def remove_spec(full_name) + @gems.delete(full_name) + end + + # Iterate over the specifications in the source index. + def each(&block) # :yields: gem.full_name, gem + @gems.each(&block) + end + + # The gem specification given a full gem spec name. + def specification(full_name) + @gems[full_name] + end + + # The signature for the source index. Changes in the signature + # indicate a change in the index. + def index_signature + require 'rubygems/digest/sha2' + + Gem::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s + end + + # The signature for the given gem specification. + def gem_signature(gem_full_name) + require 'rubygems/digest/sha2' + + Gem::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s + end + + def_delegators :@gems, :size, :length + + # Find a gem by an exact match on the short name. + def find_name(gem_name, version_requirement = Gem::Requirement.default) + search(/^#{gem_name}$/, version_requirement) + end + + # Search for a gem by short name pattern and optional version + # + # gem_name:: + # [String] a partial for the (short) name of the gem, or + # [Regex] a pattern to match against the short name + # version_requirement:: + # [String | default=Gem::Requirement.default] version to + # find + # return:: + # [Array] list of Gem::Specification objects in sorted (version) + # order. Empty if not found. + # + def search(gem_pattern, platform_only_or_version_req = false) + version_requirement = nil + only_platform = false + + case gem_pattern + when Regexp then + version_requirement = platform_only_or_version_req || + Gem::Requirement.default + when Gem::Dependency then + only_platform = platform_only_or_version_req + version_requirement = gem_pattern.version_requirements + gem_pattern = gem_pattern.name.empty? ? // : /^#{gem_pattern.name}$/ + else + version_requirement = platform_only_or_version_req || + Gem::Requirement.default + gem_pattern = /#{gem_pattern}/i + end + + unless Gem::Requirement === version_requirement then + version_requirement = Gem::Requirement.create version_requirement + end + + specs = @gems.values.select do |spec| + spec.name =~ gem_pattern and + version_requirement.satisfied_by? spec.version + end + + if only_platform then + specs = specs.select do |spec| + Gem::Platform.match spec.platform + end + end + + specs.sort_by { |s| s.sort_obj } + end + + # Refresh the source index from the local file system. + # + # return:: Returns a pointer to itself. + # + def refresh! + load_gems_in(self.class.installed_spec_directories) + end + + # Returns an Array of Gem::Specifications that are not up to date. + # + def outdated + dep = Gem::Dependency.new '', Gem::Requirement.default + + remotes = Gem::SourceInfoCache.search dep, true + + outdateds = [] + + latest_specs.each do |local| + name = local.name + remote = remotes.select { |spec| spec.name == name }. + sort_by { |spec| spec.version.to_ints }. + last + outdateds << name if remote and local.version < remote.version + end + + outdateds + end + + def update(source_uri) + use_incremental = false + + begin + gem_names = fetch_quick_index source_uri + remove_extra gem_names + missing_gems = find_missing gem_names + + return false if missing_gems.size.zero? + + say "missing #{missing_gems.size} gems" if + missing_gems.size > 0 and Gem.configuration.really_verbose + + use_incremental = missing_gems.size <= Gem.configuration.bulk_threshold + rescue Gem::OperationNotSupportedError => ex + alert_error "Falling back to bulk fetch: #{ex.message}" if + Gem.configuration.really_verbose + use_incremental = false + end + + if use_incremental then + update_with_missing(source_uri, missing_gems) + else + new_index = fetch_bulk_index(source_uri) + @gems.replace(new_index.gems) + end + + true + end + + def ==(other) # :nodoc: + self.class === other and @gems == other.gems + end + + def dump + Marshal.dump(self) + end + + protected + + attr_reader :gems + + private + + def fetcher + require 'rubygems/remote_fetcher' + + Gem::RemoteFetcher.fetcher + end + + def fetch_index_from(source_uri) + @fetch_error = nil + + indexes = %W[ + Marshal.#{Gem.marshal_version}.Z + Marshal.#{Gem.marshal_version} + yaml.Z + yaml + ] + + indexes.each do |name| + spec_data = nil + begin + spec_data = fetcher.fetch_path("#{source_uri}/#{name}") + spec_data = unzip(spec_data) if name =~ /\.Z$/ + if name =~ /Marshal/ then + return Marshal.load(spec_data) + else + return YAML.load(spec_data) + end + rescue => e + if Gem.configuration.really_verbose then + alert_error "Unable to fetch #{name}: #{e.message}" + end + @fetch_error = e + end + end + nil + end + + def fetch_bulk_index(source_uri) + say "Bulk updating Gem source index for: #{source_uri}" + + index = fetch_index_from(source_uri) + if index.nil? then + raise Gem::RemoteSourceException, + "Error fetching remote gem cache: #{@fetch_error}" + end + @fetch_error = nil + index + end + + # Get the quick index needed for incremental updates. + def fetch_quick_index(source_uri) + zipped_index = fetcher.fetch_path source_uri + '/quick/index.rz' + unzip(zipped_index).split("\n") + rescue ::Exception => ex + raise Gem::OperationNotSupportedError, + "No quick index found: " + ex.message + end + + # Make a list of full names for all the missing gemspecs. + def find_missing(spec_names) + spec_names.find_all { |full_name| + specification(full_name).nil? + } + end + + def remove_extra(spec_names) + dictionary = spec_names.inject({}) { |h, k| h[k] = true; h } + each do |name, spec| + remove_spec name unless dictionary.include? name + end + end + + # Unzip the given string. + def unzip(string) + require 'zlib' + Zlib::Inflate.inflate(string) + end + + # Tries to fetch Marshal representation first, then YAML + def fetch_single_spec(source_uri, spec_name) + @fetch_error = nil + begin + marshal_uri = source_uri + "/quick/Marshal.#{Gem.marshal_version}/#{spec_name}.gemspec.rz" + zipped = fetcher.fetch_path marshal_uri + return Marshal.load(unzip(zipped)) + rescue => ex + @fetch_error = ex + if Gem.configuration.really_verbose then + say "unable to fetch marshal gemspec #{marshal_uri}: #{ex.class} - #{ex}" + end + end + + begin + yaml_uri = source_uri + "/quick/#{spec_name}.gemspec.rz" + zipped = fetcher.fetch_path yaml_uri + return YAML.load(unzip(zipped)) + rescue => ex + @fetch_error = ex + if Gem.configuration.really_verbose then + say "unable to fetch YAML gemspec #{yaml_uri}: #{ex.class} - #{ex}" + end + end + nil + end + + # Update the cached source index with the missing names. + def update_with_missing(source_uri, missing_names) + progress = ui.progress_reporter(missing_names.size, + "Updating metadata for #{missing_names.size} gems from #{source_uri}") + missing_names.each do |spec_name| + gemspec = fetch_single_spec(source_uri, spec_name) + if gemspec.nil? then + ui.say "Failed to download spec #{spec_name} from #{source_uri}:\n" \ + "\t#{@fetch_error.message}" + else + add_spec gemspec + progress.updated spec_name + end + @fetch_error = nil + end + progress.done + progress.count + end + + end + + # Cache is an alias for SourceIndex to allow older YAMLized source + # index objects to load properly. + Cache = SourceIndex + +end + diff --git a/lib/rubygems/source_info_cache.rb b/lib/rubygems/source_info_cache.rb new file mode 100644 index 0000000000..0498e895a4 --- /dev/null +++ b/lib/rubygems/source_info_cache.rb @@ -0,0 +1,232 @@ +require 'fileutils' + +require 'rubygems' +require 'rubygems/source_info_cache_entry' +require 'rubygems/user_interaction' + +# SourceInfoCache stores a copy of the gem index for each gem source. +# +# There are two possible cache locations, the system cache and the user cache: +# * The system cache is prefered if it is writable or can be created. +# * The user cache is used otherwise +# +# Once a cache is selected, it will be used for all operations. +# SourceInfoCache will not switch between cache files dynamically. +# +# Cache data is a Hash mapping a source URI to a SourceInfoCacheEntry. +# +#-- +# To keep things straight, this is how the cache objects all fit together: +# +# Gem::SourceInfoCache +# @cache_data = { +# source_uri => Gem::SourceInfoCacheEntry +# @size => source index size +# @source_index => Gem::SourceIndex +# ... +# } +# +class Gem::SourceInfoCache + + include Gem::UserInteraction + + @cache = nil + @system_cache_file = nil + @user_cache_file = nil + + def self.cache + return @cache if @cache + @cache = new + @cache.refresh if Gem.configuration.update_sources + @cache + end + + def self.cache_data + cache.cache_data + end + + # Search all source indexes for +pattern+. + def self.search(pattern, platform_only = false) + cache.search pattern, platform_only + end + + # Search all source indexes for +pattern+. Only returns gems matching + # Gem.platforms when +only_platform+ is true. See #search_with_source. + def self.search_with_source(pattern, only_platform = false) + cache.search_with_source(pattern, only_platform) + end + + def initialize # :nodoc: + @cache_data = nil + @cache_file = nil + @dirty = false + end + + # The most recent cache data. + def cache_data + return @cache_data if @cache_data + cache_file # HACK writable check + + begin + # Marshal loads 30-40% faster from a String, and 2MB on 20061116 is small + data = File.open cache_file, 'rb' do |fp| fp.read end + @cache_data = Marshal.load data + + @cache_data.each do |url, sice| + next unless sice.is_a?(Hash) + update + cache = sice['cache'] + size = sice['size'] + if cache.is_a?(Gem::SourceIndex) and size.is_a?(Numeric) then + new_sice = Gem::SourceInfoCacheEntry.new cache, size + @cache_data[url] = new_sice + else # irreperable, force refetch. + reset_cache_for(url) + end + end + @cache_data + rescue => e + if Gem.configuration.really_verbose then + say "Exception during cache_data handling: #{ex.class} - #{ex}" + say "Cache file was: #{cache_file}" + say "\t#{e.backtrace.join "\n\t"}" + end + reset_cache_data + end + end + + def reset_cache_for(url) + say "Reseting cache for #{url}" if Gem.configuration.really_verbose + + sice = Gem::SourceInfoCacheEntry.new Gem::SourceIndex.new, 0 + sice.refresh url # HACK may be unnecessary, see ::cache and #refresh + + @cache_data[url] = sice + @cache_data + end + + def reset_cache_data + @cache_data = {} + end + + # The name of the cache file to be read + def cache_file + return @cache_file if @cache_file + @cache_file = (try_file(system_cache_file) or + try_file(user_cache_file) or + raise "unable to locate a writable cache file") + end + + # Write the cache to a local file (if it is dirty). + def flush + write_cache if @dirty + @dirty = false + end + + # Refreshes each source in the cache from its repository. + def refresh + Gem.sources.each do |source_uri| + cache_entry = cache_data[source_uri] + if cache_entry.nil? then + cache_entry = Gem::SourceInfoCacheEntry.new nil, 0 + cache_data[source_uri] = cache_entry + end + + update if cache_entry.refresh source_uri + end + + flush + end + + # Searches all source indexes for +pattern+. + def search(pattern, platform_only = false) + cache_data.map do |source_uri, sic_entry| + next unless Gem.sources.include? source_uri + sic_entry.source_index.search pattern, platform_only + end.flatten.compact + end + + # Searches all source indexes for +pattern+. If +only_platform+ is true, + # only gems matching Gem.platforms will be selected. Returns an Array of + # pairs containing the Gem::Specification found and the source_uri it was + # found at. + def search_with_source(pattern, only_platform = false) + results = [] + + cache_data.map do |source_uri, sic_entry| + next unless Gem.sources.include? source_uri + + sic_entry.source_index.search(pattern, only_platform).each do |spec| + results << [spec, source_uri] + end + end + + results + end + + # Mark the cache as updated (i.e. dirty). + def update + @dirty = true + end + + # The name of the system cache file. + def system_cache_file + self.class.system_cache_file + end + + # The name of the system cache file. (class method) + def self.system_cache_file + @system_cache_file ||= File.join(Gem.dir, "source_cache") + end + + # The name of the user cache file. + def user_cache_file + self.class.user_cache_file + end + + # The name of the user cache file. (class method) + def self.user_cache_file + @user_cache_file ||= + ENV['GEMCACHE'] || File.join(Gem.user_home, ".gem", "source_cache") + end + + # Write data to the proper cache. + def write_cache + open cache_file, "wb" do |f| + f.write Marshal.dump(cache_data) + end + end + + # Set the source info cache data directly. This is mainly used for unit + # testing when we don't want to read a file system to grab the cached source + # index information. The +hash+ should map a source URL into a + # SourceInfoCacheEntry. + def set_cache_data(hash) + @cache_data = hash + update + end + + private + + # Determine if +fn+ is a candidate for a cache file. Return fn if + # it is. Return nil if it is not. + def try_file(fn) + return fn if File.writable?(fn) + return nil if File.exist?(fn) + dir = File.dirname(fn) + unless File.exist? dir then + begin + FileUtils.mkdir_p(dir) + rescue RuntimeError + return nil + end + end + if File.writable?(dir) + File.open(fn, "wb") { |f| f << Marshal.dump({}) } + return fn + end + nil + end + +end + diff --git a/lib/rubygems/source_info_cache_entry.rb b/lib/rubygems/source_info_cache_entry.rb new file mode 100644 index 0000000000..02e03ca9db --- /dev/null +++ b/lib/rubygems/source_info_cache_entry.rb @@ -0,0 +1,46 @@ +require 'rubygems' +require 'rubygems/source_index' +require 'rubygems/remote_fetcher' + +## +# Entrys held by a SourceInfoCache. + +class Gem::SourceInfoCacheEntry + + # The source index for this cache entry. + attr_reader :source_index + + # The size of the of the source entry. Used to determine if the + # source index has changed. + attr_reader :size + + # Create a cache entry. + def initialize(si, size) + @source_index = si || Gem::SourceIndex.new({}) + @size = size + end + + def refresh(source_uri) + begin + marshal_uri = URI.join source_uri.to_s, "Marshal.#{Gem.marshal_version}" + remote_size = Gem::RemoteFetcher.fetcher.fetch_size marshal_uri + rescue Gem::RemoteSourceException + yaml_uri = URI.join source_uri.to_s, 'yaml' + remote_size = Gem::RemoteFetcher.fetcher.fetch_size yaml_uri + end + + return false if @size == remote_size # TODO Use index_signature instead of size? + updated = @source_index.update source_uri + @size = remote_size + + updated + end + + def ==(other) # :nodoc: + self.class === other and + @size == other.size and + @source_index == other.source_index + end + +end + diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb new file mode 100644 index 0000000000..308ed717a4 --- /dev/null +++ b/lib/rubygems/specification.rb @@ -0,0 +1,905 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'time' +require 'rubygems' +require 'rubygems/version' +require 'rubygems/platform' + +# :stopdoc: +# Time::today has been deprecated in 0.9.5 and will be removed. +def Time.today + t = Time.now + t - ((t.to_i + t.gmt_offset) % 86400) +end unless defined? Time.today +# :startdoc: + +module Gem + + # == Gem::Specification + # + # The Specification class contains the metadata for a Gem. Typically + # defined in a .gemspec file or a Rakefile, and looks like this: + # + # spec = Gem::Specification.new do |s| + # s.name = 'rfoo' + # s.version = '1.0' + # s.summary = 'Example gem specification' + # ... + # end + # + # There are many <em>gemspec attributes</em>, and the best place to learn + # about them in the "Gemspec Reference" linked from the RubyGems wiki. + # + class Specification + + # Allows deinstallation of gems with legacy platforms. + attr_accessor :original_platform # :nodoc: + + # ------------------------- Specification version contstants. + + # The the version number of a specification that does not specify one + # (i.e. RubyGems 0.7 or earlier). + NONEXISTENT_SPECIFICATION_VERSION = -1 + + # The specification version applied to any new Specification instances + # created. This should be bumped whenever something in the spec format + # changes. + CURRENT_SPECIFICATION_VERSION = 2 + + # An informal list of changes to the specification. The highest-valued + # key should be equal to the CURRENT_SPECIFICATION_VERSION. + SPECIFICATION_VERSION_HISTORY = { + -1 => ['(RubyGems versions up to and including 0.7 did not have versioned specifications)'], + 1 => [ + 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"', + '"test_file=x" is a shortcut for "test_files=[x]"' + ], + 2 => [ + 'Added "required_rubygems_version"', + 'Now forward-compatible with future versions', + ], + } + + # :stopdoc: + MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16 } + + now = Time.at(Time.now.to_i) + TODAY = now - ((now.to_i + now.gmt_offset) % 86400) + # :startdoc: + + # ------------------------- Class variables. + + # List of Specification instances. + @@list = [] + + # Optional block used to gather newly defined instances. + @@gather = nil + + # List of attribute names: [:name, :version, ...] + @@required_attributes = [] + + # List of _all_ attributes and default values: [[:name, nil], [:bindir, 'bin'], ...] + @@attributes = [] + + @@nil_attributes = [] + @@non_nil_attributes = [:@original_platform] + + # List of array attributes + @@array_attributes = [] + + # Map of attribute names to default values. + @@default_value = {} + + # ------------------------- Convenience class methods. + + def self.attribute_names + @@attributes.map { |name, default| name } + end + + def self.attribute_defaults + @@attributes.dup + end + + def self.default_value(name) + @@default_value[name] + end + + def self.required_attributes + @@required_attributes.dup + end + + def self.required_attribute?(name) + @@required_attributes.include? name.to_sym + end + + def self.array_attributes + @@array_attributes.dup + end + + # ------------------------- Infrastructure class methods. + + # A list of Specification instances that have been defined in this Ruby instance. + def self.list + @@list + end + + # Used to specify the name and default value of a specification + # attribute. The side effects are: + # * the name and default value are added to the @@attributes list + # and @@default_value map + # * a standard _writer_ method (<tt>attribute=</tt>) is created + # * a non-standard _reader method (<tt>attribute</tt>) is created + # + # The reader method behaves like this: + # def attribute + # @attribute ||= (copy of default value) + # end + # + # This allows lazy initialization of attributes to their default + # values. + # + def self.attribute(name, default=nil) + ivar_name = "@#{name}".intern + if default.nil? then + @@nil_attributes << ivar_name + else + @@non_nil_attributes << [ivar_name, default] + end + + @@attributes << [name, default] + @@default_value[name] = default + attr_accessor(name) + end + + # Same as :attribute, but ensures that values assigned to the + # attribute are array values by applying :to_a to the value. + def self.array_attribute(name) + @@non_nil_attributes << ["@#{name}".intern, []] + + @@array_attributes << name + @@attributes << [name, []] + @@default_value[name] = [] + code = %{ + def #{name} + @#{name} ||= [] + end + def #{name}=(value) + @#{name} = Array(value) + end + } + + module_eval code, __FILE__, __LINE__ - 9 + end + + # Same as attribute above, but also records this attribute as mandatory. + def self.required_attribute(*args) + @@required_attributes << args.first + attribute(*args) + end + + # Sometimes we don't want the world to use a setter method for a particular attribute. + # +read_only+ makes it private so we can still use it internally. + def self.read_only(*names) + names.each do |name| + private "#{name}=" + end + end + + # Shortcut for creating several attributes at once (each with a default value of + # +nil+). + def self.attributes(*args) + args.each do |arg| + attribute(arg, nil) + end + end + + # Some attributes require special behaviour when they are accessed. This allows for + # that. + def self.overwrite_accessor(name, &block) + remove_method name + define_method(name, &block) + end + + # Defines a _singular_ version of an existing _plural_ attribute + # (i.e. one whose value is expected to be an array). This means + # just creating a helper method that takes a single value and + # appends it to the array. These are created for convenience, so + # that in a spec, one can write + # + # s.require_path = 'mylib' + # + # instead of + # + # s.require_paths = ['mylib'] + # + # That above convenience is available courtesy of + # + # attribute_alias_singular :require_path, :require_paths + # + def self.attribute_alias_singular(singular, plural) + define_method("#{singular}=") { |val| + send("#{plural}=", [val]) + } + define_method("#{singular}") { + val = send("#{plural}") + val.nil? ? nil : val.first + } + end + + # Dump only crucial instance variables. + # + # MAINTAIN ORDER! + def _dump(limit) # :nodoc: + Marshal.dump [ + @rubygems_version, + @specification_version, + @name, + @version, + (Time === @date ? @date : Time.parse(@date.to_s)), + @summary, + @required_ruby_version, + @required_rubygems_version, + @new_platform, + @dependencies, + @rubyforge_project, + @email, + @authors, + @description, + @homepage, + @has_rdoc + ] + end + + # Load custom marshal format, re-initializing defaults as needed + def self._load(str) + array = Marshal.load str + + spec = Gem::Specification.new + spec.instance_variable_set :@specification_version, array[1] + + current_version = CURRENT_SPECIFICATION_VERSION + + field_count = MARSHAL_FIELDS[spec.specification_version] + + if field_count.nil? or array.size < field_count then + raise TypeError, "invalid Gem::Specification format #{array.inspect}" + end + + spec.instance_variable_set :@rubygems_version, array[0] + # spec version + spec.instance_variable_set :@name, array[2] + spec.instance_variable_set :@version, array[3] + spec.instance_variable_set :@date, array[4] + spec.instance_variable_set :@summary, array[5] + spec.instance_variable_set :@required_ruby_version, array[6] + spec.instance_variable_set :@required_rubygems_version, array[7] + spec.instance_variable_set :@new_platform, array[8] + spec.instance_variable_set :@original_platform, array[8] + spec.instance_variable_set :@platform, array[8].to_s + spec.instance_variable_set :@dependencies, array[9] + spec.instance_variable_set :@rubyforge_project, array[10] + spec.instance_variable_set :@email, array[11] + spec.instance_variable_set :@authors, array[12] + spec.instance_variable_set :@description, array[13] + spec.instance_variable_set :@homepage, array[14] + spec.instance_variable_set :@has_rdoc, array[15] + spec.instance_variable_set :@loaded, false + + spec + end + + def warn_deprecated(old, new) + # How (if at all) to implement this? We only want to warn when + # a gem is being built, I should think. + end + + # REQUIRED gemspec attributes ------------------------------------ + + required_attribute :rubygems_version, RubyGemsVersion + required_attribute :specification_version, CURRENT_SPECIFICATION_VERSION + required_attribute :name + required_attribute :version + required_attribute :date, TODAY + required_attribute :summary + required_attribute :require_paths, ['lib'] + + # OPTIONAL gemspec attributes ------------------------------------ + + attributes :email, :homepage, :rubyforge_project, :description + attributes :autorequire, :default_executable + + attribute :bindir, 'bin' + attribute :has_rdoc, false + attribute :required_ruby_version, Gem::Requirement.default + attribute :required_rubygems_version, Gem::Requirement.default + attribute :platform, Gem::Platform::RUBY + + attribute :signing_key, nil + attribute :cert_chain, [] + attribute :post_install_message, nil + + array_attribute :authors + array_attribute :files + array_attribute :test_files + array_attribute :rdoc_options + array_attribute :extra_rdoc_files + array_attribute :executables + + # Array of extensions to build. See Gem::Installer#build_extensions for + # valid values. + + array_attribute :extensions + array_attribute :requirements + array_attribute :dependencies + + read_only :dependencies + + # ALIASED gemspec attributes ------------------------------------- + + attribute_alias_singular :executable, :executables + attribute_alias_singular :author, :authors + attribute_alias_singular :require_path, :require_paths + attribute_alias_singular :test_file, :test_files + + # DEPRECATED gemspec attributes ---------------------------------- + + def test_suite_file + warn_deprecated(:test_suite_file, :test_files) + test_files.first + end + + def test_suite_file=(val) + warn_deprecated(:test_suite_file, :test_files) + @test_files = [] unless defined? @test_files + @test_files << val + end + + # true when this gemspec has been loaded from a specifications directory. + # This attribute is not persisted. + + attr_writer :loaded + + # Path this gemspec was loaded from. This attribute is not persisted. + attr_accessor :loaded_from + + # Special accessor behaviours (overwriting default) -------------- + + overwrite_accessor :version= do |version| + @version = Version.create(version) + end + + overwrite_accessor :platform do + @new_platform + end + + overwrite_accessor :platform= do |platform| + @original_platform = platform if @original_platform.nil? + + case platform + when Gem::Platform::CURRENT then + @new_platform = Gem::Platform.local + + when Gem::Platform then + @new_platform = platform + + # legacy constants + when nil, Gem::Platform::RUBY then + @new_platform = Gem::Platform::RUBY + when Gem::Platform::WIN32 then + @new_platform = Gem::Platform::MSWIN32 + when Gem::Platform::LINUX_586 then + @new_platform = Gem::Platform::X86_LINUX + when Gem::Platform::DARWIN then + @new_platform = Gem::Platform::PPC_DARWIN + else + @new_platform = platform + end + + @platform = @new_platform.to_s + + @new_platform + end + + overwrite_accessor :required_ruby_version= do |value| + @required_ruby_version = Gem::Requirement.create(value) + end + + overwrite_accessor :required_rubygems_version= do |value| + @required_rubygems_version = Gem::Requirement.create(value) + end + + overwrite_accessor :date= do |date| + # We want to end up with a Time object with one-day resolution. + # This is the cleanest, most-readable, faster-than-using-Date + # way to do it. + case date + when String then + @date = Time.parse date + when Time then + @date = Time.parse date.strftime("%Y-%m-%d") + when Date then + @date = Time.parse date.to_s + else + @date = TODAY + end + end + + overwrite_accessor :date do + self.date = nil if @date.nil? # HACK Sets the default value for date + @date + end + + overwrite_accessor :summary= do |str| + @summary = if str then + str.strip. + gsub(/(\w-)\n[ \t]*(\w)/, '\1\2'). + gsub(/\n[ \t]*/, " ") + end + end + + overwrite_accessor :description= do |str| + @description = if str then + str.strip. + gsub(/(\w-)\n[ \t]*(\w)/, '\1\2'). + gsub(/\n[ \t]*/, " ") + end + end + + overwrite_accessor :default_executable do + begin + if defined? @default_executable and @default_executable + result = @default_executable + elsif @executables and @executables.size == 1 + result = Array(@executables).first + else + result = nil + end + result + rescue + nil + end + end + + def add_bindir(executables) + if not defined? @executables || @executables.nil? + return nil + end + + if defined? @bindir and @bindir then + Array(@executables).map {|e| File.join(@bindir, e) } + else + @executables + end + rescue + return nil + end + + overwrite_accessor :files do + result = [] + result.push(*@files) if defined?(@files) + result.push(*@test_files) if defined?(@test_files) + result.push(*(add_bindir(@executables))) + result.push(*@extra_rdoc_files) if defined?(@extra_rdoc_files) + result.push(*@extensions) if defined?(@extensions) + result.uniq.compact + end + + # Files in the Gem under one of the require_paths + def lib_files + @files.select do |file| + require_paths.any? do |path| + file.index(path) == 0 + end + end + end + + overwrite_accessor :test_files do + # Handle the possibility that we have @test_suite_file but not + # @test_files. This will happen when an old gem is loaded via + # YAML. + if defined? @test_suite_file then + @test_files = [@test_suite_file].flatten + @test_suite_file = nil + end + if defined? @test_files and @test_files then + @test_files + else + @test_files = [] + end + end + + # Predicates ----------------------------------------------------- + + def loaded?; @loaded ? true : false ; end + def has_rdoc?; has_rdoc ? true : false ; end + def has_unit_tests?; not test_files.empty?; end + alias has_test_suite? has_unit_tests? # (deprecated) + + # Constructors --------------------------------------------------- + + # Specification constructor. Assigns the default values to the + # attributes, adds this spec to the list of loaded specs (see + # Specification.list), and yields itself for further initialization. + # + def initialize + @new_platform = nil + assign_defaults + @loaded = false + @@list << self + + yield self if block_given? + + @@gather.call(self) if @@gather + end + + # Each attribute has a default value (possibly nil). Here, we + # initialize all attributes to their default value. This is + # done through the accessor methods, so special behaviours will + # be honored. Furthermore, we take a _copy_ of the default so + # each specification instance has its own empty arrays, etc. + def assign_defaults + @@nil_attributes.each do |name| + instance_variable_set name, nil + end + + @@non_nil_attributes.each do |name, default| + value = case default + when Time, Numeric, Symbol, true, false, nil then default + else default.dup + end + + instance_variable_set name, value + end + + # HACK + instance_variable_set :@new_platform, Gem::Platform::RUBY + end + + # Special loader for YAML files. When a Specification object is + # loaded from a YAML file, it bypasses the normal Ruby object + # initialization routine (#initialize). This method makes up for + # that and deals with gems of different ages. + # + # 'input' can be anything that YAML.load() accepts: String or IO. + # + def self.from_yaml(input) + input = normalize_yaml_input input + spec = YAML.load input + + if spec && spec.class == FalseClass then + raise Gem::EndOfYAMLException + end + + unless Gem::Specification === spec then + raise Gem::Exception, "YAML data doesn't evaluate to gem specification" + end + + unless (spec.instance_variables.include? '@specification_version' or + spec.instance_variables.include? :@specification_version) and + spec.instance_variable_get :@specification_version + spec.instance_variable_set :@specification_version, + NONEXISTENT_SPECIFICATION_VERSION + end + + spec + end + + def self.load(filename) + gemspec = nil + fail "NESTED Specification.load calls not allowed!" if @@gather + @@gather = proc { |gs| gemspec = gs } + data = File.read(filename) + eval(data) + gemspec + ensure + @@gather = nil + end + + # Make sure the yaml specification is properly formatted with dashes. + def self.normalize_yaml_input(input) + result = input.respond_to?(:read) ? input.read : input + result = "--- " + result unless result =~ /^--- / + result + end + + # Instance methods ----------------------------------------------- + + # Sets the rubygems_version to Gem::RubyGemsVersion. + # + def mark_version + @rubygems_version = RubyGemsVersion + end + + # Ignore unknown attributes if the + def method_missing(sym, *a, &b) # :nodoc: + if @specification_version > CURRENT_SPECIFICATION_VERSION and + sym.to_s =~ /=$/ then + warn "ignoring #{sym} loading #{full_name}" if $DEBUG + else + super + end + end + + # Adds a dependency to this Gem. For example, + # + # spec.add_dependency('jabber4r', '> 0.1', '<= 0.5') + # + # gem:: [String or Gem::Dependency] The Gem name/dependency. + # requirements:: [default=">= 0"] The version requirements. + # + def add_dependency(gem, *requirements) + requirements = if requirements.empty? then + Gem::Requirement.default + else + requirements.flatten + end + + unless gem.respond_to?(:name) && gem.respond_to?(:version_requirements) + gem = Dependency.new(gem, requirements) + end + + dependencies << gem + end + + # Returns the full name (name-version) of this Gem. Platform information + # is included (name-version-platform) if it is specified (and not the + # default Ruby platform). + # + def full_name + if platform == Gem::Platform::RUBY or platform.nil? then + "#{@name}-#{@version}" + else + "#{@name}-#{@version}-#{platform}" + end + end + + # The full path to the gem (install path + full name). + # + # return:: [String] the full gem path + # + def full_gem_path + path = File.join installation_path, 'gems', full_name + return path if File.directory? path + File.join installation_path, 'gems', + "#{name}-#{version}-#{@original_platform}" + end + + # The default (generated) file name of the gem. + def file_name + full_name + ".gem" + end + + # The root directory that the gem was installed into. + # + # return:: [String] the installation path + # + def installation_path + (File.dirname(@loaded_from).split(File::SEPARATOR)[0..-2]). + join(File::SEPARATOR) + end + + # Checks if this Specification meets the requirement of the supplied + # dependency. + # + # dependency:: [Gem::Dependency] the dependency to check + # return:: [Boolean] true if dependency is met, otherwise false + # + def satisfies_requirement?(dependency) + return @name == dependency.name && + dependency.version_requirements.satisfied_by?(@version) + end + + # Comparison methods --------------------------------------------- + + def sort_obj + [@name, @version.to_ints, @new_platform == Gem::Platform::RUBY ? -1 : 1] + end + + def <=>(other) # :nodoc: + sort_obj <=> other.sort_obj + end + + # Tests specs for equality (across all attributes). + def ==(other) # :nodoc: + self.class === other && same_attributes?(other) + end + + alias eql? == # :nodoc: + + def same_attributes?(other) + @@attributes.each do |name, default| + return false unless self.send(name) == other.send(name) + end + true + end + private :same_attributes? + + def hash # :nodoc: + @@attributes.inject(0) { |hash_code, (name, default_value)| + n = self.send(name).hash + hash_code + n + } + end + + # Export methods (YAML and Ruby code) ---------------------------- + + # Returns an array of attribute names to be used when generating a + # YAML representation of this object. If an attribute still has + # its default value, it is omitted. + def to_yaml_properties + mark_version + @@attributes.map { |name, default| "@#{name}" } + end + + def yaml_initialize(tag, vals) + vals.each do |ivar, val| + instance_variable_set "@#{ivar}", val + end + + @original_platform = @platform # for backwards compatibility + self.platform = Gem::Platform.new @platform + end + + # Returns a Ruby code representation of this specification, such that it + # can be eval'ed and reconstruct the same specification later. Attributes + # that still have their default values are omitted. + def to_ruby + mark_version + result = [] + result << "Gem::Specification.new do |s|" + + result << " s.name = #{ruby_code name}" + result << " s.version = #{ruby_code version}" + result << "" + result << " s.specification_version = #{specification_version} if s.respond_to? :specification_version=" + result << "" + result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version=" + + handled = [ + :dependencies, + :name, + :required_rubygems_version, + :specification_version, + :version, + ] + + attributes = @@attributes.sort_by { |name,| name.to_s } + + attributes.each do |name, default| + next if handled.include? name + current_value = self.send(name) + if current_value != default or self.class.required_attribute? name then + result << " s.#{name} = #{ruby_code current_value}" + end + end + + result << "" unless dependencies.empty? + + dependencies.each do |dep| + version_reqs_param = dep.requirements_list.inspect + result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})" + end + + result << "end" + result << "" + + result.join "\n" + end + + # Validation and normalization methods --------------------------- + + # Checks that the specification contains all required fields, and + # does a very basic sanity check. + # + # Raises InvalidSpecificationException if the spec does not pass + # the checks.. + def validate + normalize + + if rubygems_version != RubyGemsVersion then + raise Gem::InvalidSpecificationException, + "expected RubyGems version #{RubyGemsVersion}, was #{rubygems_version}" + end + + @@required_attributes.each do |symbol| + unless self.send symbol then + raise Gem::InvalidSpecificationException, + "missing value for attribute #{symbol}" + end + end + + if require_paths.empty? then + raise Gem::InvalidSpecificationException, + "specification must have at least one require_path" + end + + case platform + when Gem::Platform, Platform::RUBY then # ok + else + raise Gem::InvalidSpecificationException, + "invalid platform #{platform.inspect}, see Gem::Platform" + end + + true + end + + # Normalize the list of files so that: + # * All file lists have redundancies removed. + # * Files referenced in the extra_rdoc_files are included in the + # package file list. + # + # Also, the summary and description are converted to a normal + # format. + def normalize + if defined? @extra_rdoc_files and @extra_rdoc_files then + @extra_rdoc_files.uniq! + @files ||= [] + @files.concat(@extra_rdoc_files) + end + @files.uniq! if @files + end + + # Dependency methods --------------------------------------------- + + # Return a list of all gems that have a dependency on this + # gemspec. The list is structured with entries that conform to: + # + # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] + # + # return:: [Array] [[dependent_gem, dependency, [list_of_satisfiers]]] + # + def dependent_gems + out = [] + Gem.source_index.each do |name,gem| + gem.dependencies.each do |dep| + if self.satisfies_requirement?(dep) then + sats = [] + find_all_satisfiers(dep) do |sat| + sats << sat + end + out << [gem, dep, sats] + end + end + end + out + end + + def to_s + "#<Gem::Specification name=#{@name} version=#{@version}>" + end + + private + + def find_all_satisfiers(dep) + Gem.source_index.each do |name,gem| + if(gem.satisfies_requirement?(dep)) then + yield gem + end + end + end + + # Return a string containing a Ruby code representation of the + # given object. + def ruby_code(obj) + case obj + when String then '%q{' + obj + '}' + when Array then obj.inspect + when Gem::Version then obj.to_s.inspect + when Date then '%q{' + obj.strftime('%Y-%m-%d') + '}' + when Time then '%q{' + obj.strftime('%Y-%m-%d') + '}' + when Numeric then obj.inspect + when true, false, nil then obj.inspect + when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})" + when Gem::Requirement then "Gem::Requirement.new(#{obj.to_s.inspect})" + else raise Exception, "ruby_code case not handled: #{obj.class}" + end + end + + end + +end + diff --git a/lib/rubygems/timer.rb b/lib/rubygems/timer.rb new file mode 100755 index 0000000000..06250f26b5 --- /dev/null +++ b/lib/rubygems/timer.rb @@ -0,0 +1,25 @@ +# +# This file defines a $log variable for logging, and a time() method for recording timing +# information. +# +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + + +$log = Object.new +def $log.debug(str) + STDERR.puts str +end + +def time(msg, width=25) + t = Time.now + return_value = yield + elapsed = Time.now.to_f - t.to_f + elapsed = sprintf("%3.3f", elapsed) + $log.debug "#{msg.ljust(width)}: #{elapsed}s" + return_value +end + diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb new file mode 100644 index 0000000000..0f7edb048c --- /dev/null +++ b/lib/rubygems/uninstaller.rb @@ -0,0 +1,183 @@ +#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require 'fileutils'
+require 'rubygems'
+require 'rubygems/dependency_list'
+require 'rubygems/doc_manager'
+require 'rubygems/user_interaction'
+
+##
+# An Uninstaller.
+#
+class Gem::Uninstaller
+
+ include Gem::UserInteraction
+
+ ##
+ # Constructs an Uninstaller instance
+ #
+ # gem:: [String] The Gem name to uninstall
+ #
+ def initialize(gem, options)
+ @gem = gem
+ @version = options[:version] || Gem::Requirement.default
+ @force_executables = options[:executables]
+ @force_all = options[:all]
+ @force_ignore = options[:ignore]
+ end
+
+ ##
+ # Performs the uninstall of the Gem. This removes the spec, the
+ # Gem directory, and the cached .gem file,
+ #
+ def uninstall
+ list = Gem.source_index.search(/^#{@gem}$/, @version)
+
+ if list.empty? then
+ raise Gem::InstallError, "Unknown gem #{@gem}-#{@version}"
+ elsif list.size > 1 && @force_all
+ remove_all(list.dup)
+ remove_executables(list.last)
+ elsif list.size > 1
+ say
+ gem_names = list.collect {|gem| gem.full_name} + ["All versions"]
+ gem_name, index =
+ choose_from_list("Select gem to uninstall:", gem_names)
+ if index == list.size
+ remove_all(list.dup)
+ remove_executables(list.last)
+ elsif index >= 0 && index < list.size
+ to_remove = list[index]
+ remove(to_remove, list)
+ remove_executables(to_remove)
+ else
+ say "Error: must enter a number [1-#{list.size+1}]"
+ end
+ else
+ remove(list[0], list.dup)
+ remove_executables(list.last)
+ end
+ end
+
+ ##
+ # Remove executables and batch files (windows only) for the gem as
+ # it is being installed
+ #
+ # gemspec::[Specification] the gem whose executables need to be removed.
+ #
+ def remove_executables(gemspec)
+ return if gemspec.nil?
+ if(gemspec.executables.size > 0)
+ raise Gem::FilePermissionError.new(Gem.bindir) unless
+ File.writable?(Gem.bindir)
+ list = Gem.source_index.search(gemspec.name).delete_if { |spec|
+ spec.version == gemspec.version
+ }
+ executables = gemspec.executables.clone
+ list.each do |spec|
+ spec.executables.each do |exe_name|
+ executables.delete(exe_name)
+ end
+ end
+ return if executables.size == 0
+ answer = @force_executables || ask_yes_no(
+ "Remove executables and scripts for\n" +
+ "'#{gemspec.executables.join(", ")}' in addition to the gem?",
+ true) # " # appease ruby-mode - don't ask
+ unless answer
+ say "Executables and scripts will remain installed."
+ return
+ else
+ gemspec.executables.each do |exe_name|
+ say "Removing #{exe_name}"
+ File.unlink File.join(Gem.bindir, exe_name) rescue nil
+ File.unlink File.join(Gem.bindir, exe_name + ".bat") rescue nil
+ end
+ end
+ end
+ end
+
+ #
+ # list:: the list of all gems to remove
+ #
+ # Warning: this method modifies the +list+ parameter. Once it has
+ # uninstalled a gem, it is removed from that list.
+ #
+ def remove_all(list)
+ list.dup.each { |gem| remove(gem, list) }
+ end
+
+ #
+ # spec:: the spec of the gem to be uninstalled
+ # list:: the list of all such gems
+ #
+ # Warning: this method modifies the +list+ parameter. Once it has
+ # uninstalled a gem, it is removed from that list.
+ #
+ def remove(spec, list)
+ unless ok_to_remove? spec then
+ raise Gem::DependencyRemovalException,
+ "Uninstallation aborted due to dependent gem(s)"
+ end
+
+ raise Gem::FilePermissionError, spec.installation_path unless
+ File.writable?(spec.installation_path)
+
+ FileUtils.rm_rf spec.full_gem_path
+
+ original_platform_name = [
+ spec.name, spec.version, spec.original_platform].join '-'
+
+ spec_dir = File.join spec.installation_path, 'specifications'
+ gemspec = File.join spec_dir, "#{spec.full_name}.gemspec"
+
+ unless File.exist? gemspec then
+ gemspec = File.join spec_dir, "#{original_platform_name}.gemspec"
+ end
+
+ FileUtils.rm_rf gemspec
+
+ cache_dir = File.join spec.installation_path, 'cache'
+ gem = File.join cache_dir, "#{spec.full_name}.gem"
+
+ unless File.exist? gemspec then
+ gem = File.join cache_dir, "#{original_platform_name}.gem"
+ end
+
+ FileUtils.rm_rf gem
+
+ Gem::DocManager.new(spec).uninstall_doc
+
+ say "Successfully uninstalled #{spec.full_name}"
+
+ list.delete spec
+ end
+
+ def ok_to_remove?(spec)
+ return true if @force_ignore
+
+ srcindex = Gem::SourceIndex.from_installed_gems
+ deplist = Gem::DependencyList.from_source_index srcindex
+ deplist.ok_to_remove?(spec.full_name) || ask_if_ok(spec)
+ end
+
+ def ask_if_ok(spec)
+ msg = ['']
+ msg << 'You have requested to uninstall the gem:'
+ msg << "\t#{spec.full_name}"
+ spec.dependent_gems.each do |gem,dep,satlist|
+ msg <<
+ ("#{gem.name}-#{gem.version} depends on " +
+ "[#{dep.name} (#{dep.version_requirements})]")
+ end
+ msg << 'If you remove this gems, one or more dependencies will not be met.'
+ msg << 'Continue with Uninstall?'
+ return ask_yes_no(msg.join("\n"), true)
+ end
+
+end
+
diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb new file mode 100644 index 0000000000..7ff03eaadf --- /dev/null +++ b/lib/rubygems/user_interaction.rb @@ -0,0 +1,291 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +module Gem + + #################################################################### + # Module that defines the default UserInteraction. Any class + # including this module will have access to the +ui+ method that + # returns the default UI. + module DefaultUserInteraction + + # Return the default UI. + def ui + DefaultUserInteraction.ui + end + + # Set the default UI. If the default UI is never explicity set, a + # simple console based UserInteraction will be used automatically. + def ui=(new_ui) + DefaultUserInteraction.ui = new_ui + end + + def use_ui(new_ui, &block) + DefaultUserInteraction.use_ui(new_ui, &block) + end + + # The default UI is a class variable of the singleton class for + # this module. + + @ui = nil + + class << self + def ui + @ui ||= Gem::ConsoleUI.new + end + def ui=(new_ui) + @ui = new_ui + end + def use_ui(new_ui) + old_ui = @ui + @ui = new_ui + yield + ensure + @ui = old_ui + end + end + end + + #################################################################### + # Make the default UI accessable without the "ui." prefix. Classes + # including this module may use the interaction methods on the + # default UI directly. Classes may also reference the +ui+ and + # <tt>ui=</tt> methods. + # + # Example: + # + # class X + # include Gem::UserInteraction + # + # def get_answer + # n = ask("What is the meaning of life?") + # end + # end + module UserInteraction + include DefaultUserInteraction + [ + :choose_from_list, :ask, :ask_yes_no, :say, :alert, :alert_warning, + :alert_error, :terminate_interaction!, :terminate_interaction + ].each do |methname| + class_eval %{ + def #{methname}(*args) + ui.#{methname}(*args) + end + } + end + end + + #################################################################### + # StreamUI implements a simple stream based user interface. + class StreamUI + + attr_reader :ins, :outs, :errs + + def initialize(in_stream, out_stream, err_stream=STDERR) + @ins = in_stream + @outs = out_stream + @errs = err_stream + end + + # Choose from a list of options. +question+ is a prompt displayed + # above the list. +list+ is a list of option strings. Returns + # the pair [option_name, option_index]. + def choose_from_list(question, list) + @outs.puts question + list.each_with_index do |item, index| + @outs.puts " #{index+1}. #{item}" + end + @outs.print "> " + @outs.flush + + result = @ins.gets + + return nil, nil unless result + + result = result.strip.to_i - 1 + return list[result], result + end + + # Ask a question. Returns a true for yes, false for no. If not + # connected to a tty, raises an exception if default is nil, + # otherwise returns default. + def ask_yes_no(question, default=nil) + if not @ins.tty? then + if default.nil? then + raise( + Gem::OperationNotSupportedError, + "Not connected to a tty and no default specified") + else + return default + end + end + qstr = case default + when nil + 'yn' + when true + 'Yn' + else + 'yN' + end + result = nil + while result.nil? + result = ask("#{question} [#{qstr}]") + result = case result + when /^[Yy].*/ + true + when /^[Nn].*/ + false + when /^$/ + default + else + nil + end + end + return result + end + + # Ask a question. Returns an answer if connected to a tty, nil + # otherwise. + def ask(question) + return nil if not @ins.tty? + @outs.print(question + " ") + @outs.flush + result = @ins.gets + result.chomp! if result + result + end + + # Display a statement. + def say(statement="") + @outs.puts statement + end + + # Display an informational alert. + def alert(statement, question=nil) + @outs.puts "INFO: #{statement}" + return ask(question) if question + end + + # Display a warning in a location expected to get error messages. + def alert_warning(statement, question=nil) + @errs.puts "WARNING: #{statement}" + ask(question) if question + end + + # Display an error message in a location expected to get error + # messages. + def alert_error(statement, question=nil) + @errs.puts "ERROR: #{statement}" + ask(question) if question + end + + # Terminate the application immediately without running any exit + # handlers. + def terminate_interaction!(status=-1) + exit!(status) + end + + # Terminate the appliation normally, running any exit handlers + # that might have been defined. + def terminate_interaction(status=0) + exit(status) + end + + # Return a progress reporter object + def progress_reporter(*args) + case Gem.configuration.verbose + when nil, false + SilentProgressReporter.new(@outs, *args) + when true + SimpleProgressReporter.new(@outs, *args) + else + VerboseProgressReporter.new(@outs, *args) + end + end + + class SilentProgressReporter + attr_reader :count + + def initialize(out_stream, size, initial_message, terminal_message = nil) + end + + def updated(message) + end + + def done + end + end + + class SimpleProgressReporter + include DefaultUserInteraction + + attr_reader :count + + def initialize(out_stream, size, initial_message, + terminal_message = "complete") + @out = out_stream + @total = size + @count = 0 + @terminal_message = terminal_message + + @out.puts initial_message + end + + def updated(message) + @count += 1 + @out.print "." + @out.flush + end + + def done + @out.puts "\n#{@terminal_message}" + end + end + + class VerboseProgressReporter + include DefaultUserInteraction + + attr_reader :count + + def initialize(out_stream, size, initial_message, + terminal_message = 'complete') + @out = out_stream + @total = size + @count = 0 + @terminal_message = terminal_message + + @out.puts initial_message + end + + def updated(message) + @count += 1 + @out.puts "#{@count}/#{@total}: #{message}" + end + + def done + @out.puts @terminal_message + end + end + end + + #################################################################### + # Subclass of StreamUI that instantiates the user interaction using + # standard in, out and error. + class ConsoleUI < StreamUI + def initialize + super(STDIN, STDOUT, STDERR) + end + end + + #################################################################### + # SilentUI is a UI choice that is absolutely silent. + class SilentUI + def method_missing(sym, *args, &block) + self + end + end +end + diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb new file mode 100755 index 0000000000..8130f49bc8 --- /dev/null +++ b/lib/rubygems/validator.rb @@ -0,0 +1,185 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'find' + +require 'rubygems/digest/md5' +require 'rubygems/format' +require 'rubygems/installer' + +module Gem + + ## + # Validator performs various gem file and gem database validation + class Validator + include UserInteraction + + ## + # Given a gem file's contents, validates against its own MD5 checksum + # gem_data:: [String] Contents of the gem file + def verify_gem(gem_data) + raise VerificationError, 'empty gem file' if gem_data.size == 0 + + unless gem_data =~ /MD5SUM/ then + return # Don't worry about it...this sucks. Need to fix MD5 stuff for + # new format + # FIXME + end + + sum_data = gem_data.gsub(/MD5SUM = "([a-z0-9]+)"/, + "MD5SUM = \"#{"F" * 32}\"") + + unless Gem::MD5.hexdigest(sum_data) == $1.to_s then + raise VerificationError, 'invalid checksum for gem file' + end + end + + ## + # Given the path to a gem file, validates against its own MD5 checksum + # + # gem_path:: [String] Path to gem file + def verify_gem_file(gem_path) + File.open gem_path, 'rb' do |file| + gem_data = file.read + verify_gem gem_data + end + rescue Errno::ENOENT + raise Gem::VerificationError.new("missing gem file #{gem_path}") + end + + private + def find_files_for_gem(gem_directory) + installed_files = [] + Find.find(gem_directory) {|file_name| + fn = file_name.slice((gem_directory.size)..(file_name.size-1)).sub(/^\//, "") + if(!(fn =~ /CVS/ || File.directory?(fn) || fn == "")) then + installed_files << fn + end + + } + installed_files + end + + + public + ErrorData = Struct.new(:path, :problem) + + ## + # Checks the gem directory for the following potential + # inconsistencies/problems: + # * Checksum gem itself + # * For each file in each gem, check consistency of installed versions + # * Check for files that aren't part of the gem but are in the gems directory + # * 1 cache - 1 spec - 1 directory. + # + # returns a hash of ErrorData objects, keyed on the problem gem's name. + def alien + errors = {} + Gem::SourceIndex.from_installed_gems.each do |gem_name, gem_spec| + errors[gem_name] ||= [] + gem_path = File.join(Gem.dir, "cache", gem_spec.full_name) + ".gem" + spec_path = File.join(Gem.dir, "specifications", gem_spec.full_name) + ".gemspec" + gem_directory = File.join(Gem.dir, "gems", gem_spec.full_name) + installed_files = find_files_for_gem(gem_directory) + + if(!File.exist?(spec_path)) then + errors[gem_name] << ErrorData.new(spec_path, "Spec file doesn't exist for installed gem") + end + + begin + verify_gem_file(gem_path) + File.open(gem_path, 'rb') do |file| + format = Gem::Format.from_file_by_path(gem_path) + format.file_entries.each do |entry, data| + # Found this file. Delete it from list + installed_files.delete remove_leading_dot_dir(entry['path']) + + next unless data # HACK `gem check -a mkrf` + + File.open(File.join(gem_directory, entry['path']), 'rb') do |f| + unless Gem::MD5.hexdigest(f.read).to_s == + Gem::MD5.hexdigest(data).to_s then + errors[gem_name] << ErrorData.new(entry['path'], "installed file doesn't match original from gem") + end + end + end + end + rescue VerificationError => e + errors[gem_name] << ErrorData.new(gem_path, e.message) + end + # Clean out directories that weren't explicitly included in the gemspec + # FIXME: This still allows arbitrary incorrect directories. + installed_files.delete_if {|potential_directory| + File.directory?(File.join(gem_directory, potential_directory)) + } + if(installed_files.size > 0) then + errors[gem_name] << ErrorData.new(gem_path, "Unmanaged files in gem: #{installed_files.inspect}") + end + end + errors + end + + class TestRunner + def initialize(suite, ui) + @suite = suite + @ui = ui + end + + def self.run(suite, ui) + require 'test/unit/ui/testrunnermediator' + return new(suite, ui).start + end + + def start + @mediator = Test::Unit::UI::TestRunnerMediator.new(@suite) + @mediator.add_listener(Test::Unit::TestResult::FAULT, &method(:add_fault)) + return @mediator.run_suite + end + + def add_fault(fault) + if Gem.configuration.verbose then + @ui.say fault.long_display + end + end + end + + autoload :TestRunner, 'test/unit/ui/testrunnerutilities' + + ## + # Runs unit tests for a given gem specification + def unit_test(gem_spec) + start_dir = Dir.pwd + Dir.chdir(gem_spec.full_gem_path) + $: << File.join(Gem.dir, "gems", gem_spec.full_name) + # XXX: why do we need this gem_spec when we've already got 'spec'? + test_files = gem_spec.test_files + if test_files.empty? + say "There are no unit tests to run for #{gem_spec.name}-#{gem_spec.version}" + return + end + gem gem_spec.name, "= #{gem_spec.version.version}" + test_files.each do |f| require f end + suite = Test::Unit::TestSuite.new("#{gem_spec.name}-#{gem_spec.version}") + ObjectSpace.each_object(Class) do |klass| + suite << klass.suite if (klass < Test::Unit::TestCase) + end + result = TestRunner.run(suite, ui()) + unless result.passed? + alert_error(result.to_s) + #unless ask_yes_no(result.to_s + "...keep Gem?", true) then + #Gem::Uninstaller.new(gem_spec.name, gem_spec.version.version).uninstall + #end + end + result + ensure + Dir.chdir(start_dir) + end + + def remove_leading_dot_dir(path) + path.sub(/^\.\//, "") + end + end +end diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb new file mode 100644 index 0000000000..35dd60a74a --- /dev/null +++ b/lib/rubygems/version.rb @@ -0,0 +1,158 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +## +# The Version class processes string versions into comparable values +class Gem::Version + + include Comparable + + attr_reader :ints + + attr_reader :version + + ## + # Checks if version string is valid format + # + # str:: [String] the version string + # return:: [Boolean] true if the string format is correct, otherwise false + # + def self.correct?(version) + case version + when Integer, /\A\s*(\d+(\.\d+)*)*\s*\z/ then true + else false + end + end + + ## + # Factory method to create a Version object. Input may be a Version or a + # String. Intended to simplify client code. + # + # ver1 = Version.create('1.3.17') # -> (Version object) + # ver2 = Version.create(ver1) # -> (ver1) + # ver3 = Version.create(nil) # -> nil + # + def self.create(input) + if input.respond_to? :version then + input + elsif input.nil? then + nil + else + new input + end + end + + ## + # Constructs a version from the supplied string + # + # version:: [String] The version string. Format is digit.digit... + # + def initialize(version) + raise ArgumentError, "Malformed version number string #{version}" unless + self.class.correct?(version) + + self.version = version + end + + def inspect # :nodoc: + "#<#{self.class} #{@version.inspect}>" + end + + # Dump only the raw version string, not the complete object + def marshal_dump + [@version] + end + + # Load custom marshal format + def marshal_load(array) + self.version = array[0] + end + + # Strip ignored trailing zeros. + def normalize + @ints = @version.to_s.scan(/\d+/).map { |s| s.to_i } + + return if @ints.length == 1 + + @ints.pop while @ints.last == 0 + + @ints = [0] if @ints.empty? + end + + ## + # Returns the text representation of the version + # + # return:: [String] version as string + # + def to_s + @version + end + + ## + # Convert version to integer array + # + # return:: [Array] list of integers + # + def to_ints + normalize unless @ints + @ints + end + + def to_yaml_properties + ['@version'] + end + + def version=(version) + @version = version.to_s.strip + normalize + end + + def yaml_initialize(tag, values) + self.version = values['version'] + end + + ## + # Compares two versions + # + # other:: [Version or .ints] other version to compare to + # return:: [Fixnum] -1, 0, 1 + # + def <=>(other) + return 1 unless other + @ints <=> other.ints + end + + def hash + to_ints.inject { |hash_code, n| hash_code + n } + end + + # Return a new version object where the next to the last revision + # number is one greater. (e.g. 5.3.1 => 5.4) + def bump + ints = @ints.dup + ints.pop if ints.size > 1 + ints[-1] += 1 + self.class.new(ints.join(".")) + end + + #:stopdoc: + + require 'rubygems/requirement' + + # Gem::Requirement's original definition is nested in Version. + # Although an inappropriate place, current gems specs reference the nested + # class name explicitly. To remain compatible with old software loading + # gemspecs, we leave a copy of original definition in Version, but define an + # alias Gem::Requirement for use everywhere else. + + Requirement = ::Gem::Requirement + + # :startdoc: + +end + diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb new file mode 100644 index 0000000000..54f85188df --- /dev/null +++ b/lib/rubygems/version_option.rb @@ -0,0 +1,49 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +require 'rubygems' + +# Mixin methods for --version and --platform Gem::Command options. +module Gem::VersionOption + + # Add the --platform option to the option parser. + def add_platform_option(task = command, *wrap) + OptionParser.accept Gem::Platform do |value| + if value == Gem::Platform::RUBY then + value + else + Gem::Platform.new value + end + end + + add_option('--platform PLATFORM', Gem::Platform, + "Specify the platform of gem to #{task}", *wrap) do + |value, options| + unless options[:added_platform] then + Gem.platforms.clear + Gem.platforms << Gem::Platform::RUBY + options[:added_platform] = true + end + + Gem.platforms << value unless Gem.platforms.include? value + end + end + + # Add the --version option to the option parser. + def add_version_option(task = command, *wrap) + OptionParser.accept Gem::Requirement do |value| + Gem::Requirement.new value + end + + add_option('-v', '--version VERSION', Gem::Requirement, + "Specify version of gem to #{task}", *wrap) do + |value, options| + options[:version] = value + end + end + +end + |