summaryrefslogtreecommitdiff
path: root/lib/rubygems/commands/unpack_command.rb
blob: 1091a55f374994b113fc25f6ca19a5598f46a12d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# frozen_string_literal: true
require 'rubygems/command'
require 'rubygems/version_option'
require 'rubygems/security_option'
require 'rubygems/remote_fetcher'
require 'rubygems/package'

# forward-declare

module Gem::Security # :nodoc:
  class Policy # :nodoc:
  end
end

class Gem::Commands::UnpackCommand < Gem::Command

  include Gem::VersionOption
  include Gem::SecurityOption

  def initialize
    require 'fileutils'

    super 'unpack', 'Unpack an installed gem to the current directory',
          :version => Gem::Requirement.default,
          :target  => Dir.pwd

    add_option('--target=DIR',
               'target directory for unpacking') do |value, options|
      options[:target] = value
    end

    add_option('--spec', 'unpack the gem specification') do |value, options|
      options[:spec] = true
    end

    add_security_option
    add_version_option
  end

  def arguments # :nodoc:
    "GEMNAME       name of gem to unpack"
  end

  def defaults_str # :nodoc:
    "--version '#{Gem::Requirement.default}'"
  end

  def description
    <<-EOF
The unpack command allows you to examine the contents of a gem or modify
them to help diagnose a bug.

You can add the contents of the unpacked gem to the load path using the
RUBYLIB environment variable or -I:

  $ gem unpack my_gem
  Unpacked gem: '.../my_gem-1.0'
  [edit my_gem-1.0/lib/my_gem.rb]
  $ ruby -Imy_gem-1.0/lib -S other_program

You can repackage an unpacked gem using the build command.  See the build
command help for an example.
    EOF
  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
    security_policy = options[:security_policy]

    get_all_gem_names.each do |name|
      dependency = Gem::Dependency.new name, options[:version]
      path = get_path dependency

      unless path
        alert_error "Gem '#{name}' not installed nor fetchable."
        next
      end

      if @options[:spec]
        spec, metadata = Gem::Package.raw_spec(path, security_policy)

        if metadata.nil?
          alert_error "--spec is unsupported on '#{name}' (old format gem)"
          next
        end

        spec_file = File.basename spec.spec_file

        FileUtils.mkdir_p @options[:target] if @options[:target]

        destination = begin
          if @options[:target]
            File.join @options[:target], spec_file
          else
            spec_file
          end
        end

        File.open destination, 'w' do |io|
          io.write metadata
        end
      else
        basename = File.basename path, '.gem'
        target_dir = File.expand_path basename, options[:target]

        package = Gem::Package.new path, security_policy
        package.extract_files target_dir

        say "Unpacked gem: '#{target_dir}'"
      end
    end
  end

  ##
  #
  # Find cached filename in Gem.path. Returns nil if the file cannot be found.
  #
  #--
  # TODO: see comments in get_path() about general service.

  def find_in_cache(filename)
    Gem.path.each do |path|
      this_path = File.join(path, "cache", filename)
      return this_path if File.exist? this_path
    end

    return nil
  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(dependency)
    return dependency.name if dependency.name =~ /\.gem$/i

    specs = dependency.matching_specs

    selected = specs.max_by {|s| s.version }

    return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless
      selected

    return unless dependency.name =~ /^#{selected.name}$/i

    # We expect to find (basename).gem in the 'cache' directory.  Furthermore,
    # the name match must be exact (ignoring case).

    path = find_in_cache File.basename selected.cache_file

    return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path

    path
  end

end