summaryrefslogtreecommitdiff
path: root/lib/rubygems/request_set/lockfile.rb
blob: b79a37724855a8659a7c6decf5bab0a18e87db3c (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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
##
# Parses a gem.deps.rb.lock file and constructs a LockSet containing the
# dependencies found inside.  If the lock file is missing no LockSet is
# constructed.

class Gem::RequestSet::Lockfile
  ##
  # Raised when a lockfile cannot be parsed

  class ParseError < Gem::Exception

    ##
    # The column where the error was encountered

    attr_reader :column

    ##
    # The line where the error was encountered

    attr_reader :line

    ##
    # The location of the lock file

    attr_reader :path

    ##
    # Raises a ParseError with the given +message+ which was encountered at a
    # +line+ and +column+ while parsing.

    def initialize message, column, line, path
      @line   = line
      @column = column
      @path   = path
      super "#{message} (at line #{line} column #{column})"
    end
  end

  ##
  # Creates a new Lockfile for the given +request_set+ and +gem_deps_file+
  # location.

  def self.build request_set, gem_deps_file, dependencies = nil
    request_set.resolve
    dependencies ||= requests_to_deps request_set.sorted_requests
    new request_set, gem_deps_file, dependencies
  end

  def self.requests_to_deps requests # :nodoc:
    deps = {}

    requests.each do |request|
      spec        = request.spec
      name        = request.name
      requirement = request.request.dependency.requirement

      deps[name] = if [Gem::Resolver::VendorSpecification,
                       Gem::Resolver::GitSpecification].include? spec.class then
                     Gem::Requirement.source_set
                   else
                     requirement
                   end
    end

    deps
  end

  ##
  # The platforms for this Lockfile

  attr_reader :platforms

  def initialize request_set, gem_deps_file, dependencies
    @set           = request_set
    @dependencies  = dependencies
    @gem_deps_file = File.expand_path(gem_deps_file)
    @gem_deps_dir  = File.dirname(@gem_deps_file)

    @gem_deps_file.untaint unless gem_deps_file.tainted?

    @platforms      = []
  end

  def add_DEPENDENCIES out # :nodoc:
    out << "DEPENDENCIES"

    out.concat @dependencies.sort_by { |name,| name }.map { |name, requirement|
      "  #{name}#{requirement.for_lockfile}"
    }

    out << nil
  end

  def add_GEM out, spec_groups # :nodoc:
    return if spec_groups.empty?

    source_groups = spec_groups.values.flatten.group_by do |request|
      request.spec.source.uri
    end

    source_groups.sort_by { |group,| group.to_s }.map do |group, requests|
      out << "GEM"
      out << "  remote: #{group}"
      out << "  specs:"

      requests.sort_by { |request| request.name }.each do |request|
        next if request.spec.name == 'bundler'
        platform = "-#{request.spec.platform}" unless
          Gem::Platform::RUBY == request.spec.platform

        out << "    #{request.name} (#{request.version}#{platform})"

        request.full_spec.dependencies.sort.each do |dependency|
          next if dependency.type == :development

          requirement = dependency.requirement
          out << "      #{dependency.name}#{requirement.for_lockfile}"
        end
      end
      out << nil
    end
  end

  def add_GIT out, git_requests
    return if git_requests.empty?

    by_repository_revision = git_requests.group_by do |request|
      source = request.spec.source
      [source.repository, source.rev_parse]
    end

    out << "GIT"
    by_repository_revision.each do |(repository, revision), requests|
      out << "  remote: #{repository}"
      out << "  revision: #{revision}"
      out << "  specs:"

      requests.sort_by { |request| request.name }.each do |request|
        out << "    #{request.name} (#{request.version})"

        dependencies = request.spec.dependencies.sort_by { |dep| dep.name }
        dependencies.each do |dep|
          out << "      #{dep.name}#{dep.requirement.for_lockfile}"
        end
      end
    end

    out << nil
  end

  def relative_path_from dest, base # :nodoc:
    dest = File.expand_path(dest)
    base = File.expand_path(base)

    if dest.index(base) == 0 then
      offset = dest[base.size+1..-1]

      return '.' unless offset

      offset
    else
      dest
    end
  end

  def add_PATH out, path_requests # :nodoc:
    return if path_requests.empty?

    out << "PATH"
    path_requests.each do |request|
      directory = File.expand_path(request.spec.source.uri)

      out << "  remote: #{relative_path_from directory, @gem_deps_dir}"
      out << "  specs:"
      out << "    #{request.name} (#{request.version})"
    end

    out << nil
  end

  def add_PLATFORMS out # :nodoc:
    out << "PLATFORMS"

    platforms = requests.map { |request| request.spec.platform }.uniq

    platforms = platforms.sort_by { |platform| platform.to_s }

    platforms.each do |platform|
      out << "  #{platform}"
    end

    out << nil
  end

  def spec_groups
    requests.group_by { |request| request.spec.class }
  end

  ##
  # The contents of the lock file.

  def to_s
    out = []

    groups = spec_groups

    add_PATH out, groups.delete(Gem::Resolver::VendorSpecification) { [] }

    add_GIT out, groups.delete(Gem::Resolver::GitSpecification) { [] }

    add_GEM out, groups

    add_PLATFORMS out

    add_DEPENDENCIES out

    out.join "\n"
  end

  ##
  # Writes the lock file alongside the gem dependencies file

  def write
    content = to_s

    open "#{@gem_deps_file}.lock", 'w' do |io|
      io.write content
    end
  end

  private

  def requests
    @set.sorted_requests
  end
end

require 'rubygems/request_set/lockfile/tokenizer'