stickler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ require 'uri'
2
+ require 'base64'
3
+ require 'progressbar'
4
+ require 'zlib'
5
+
6
+ module Stickler
7
+ #
8
+ # The representation of an upstream source from which stickler pulls gems.
9
+ # This wraps up the contents of the upstream specs.4.8 file found in a
10
+ # rubygems 1.2 or greater repository.
11
+ #
12
+ class Source
13
+ class Error < ::StandardError; end
14
+
15
+ # the uri of the source
16
+ attr_reader :uri
17
+
18
+ # the source_group this source belongs to
19
+ attr_accessor :source_group
20
+
21
+ def self.normalize_uri( uri )
22
+ return uri if uri.kind_of?( URI::Generic )
23
+ path_parts = uri.split( "/" )
24
+ uri = path_parts.join( "/" ) + "/"
25
+ uri = ::URI.parse( uri )
26
+ end
27
+
28
+ #
29
+ # Create a new Source for a source_group.
30
+ # Try and load the source from the cache if it can and if not,
31
+ # load it from the uri
32
+ #
33
+ def initialize( uri, source_group )
34
+
35
+ begin
36
+ @uri = Source.normalize_uri( uri )
37
+ @source_group = source_group
38
+ load_source_specs
39
+
40
+ rescue ::URI::Error => e
41
+ raise Error, "Unable to create source from uri #{uri} : #{e}"
42
+ end
43
+ end
44
+
45
+ #
46
+ # find all matching gems and return their SpecLite
47
+ #
48
+ def search( dependency )
49
+ found = source_specs.select do | spec |
50
+ dependency =~ Gem::Dependency.new( spec.name, spec.version )
51
+ end
52
+ end
53
+
54
+ #
55
+ # load the upstream or cached specs.marshalversion file for the source.
56
+ #
57
+ def source_specs
58
+ unless @source_specs
59
+ Console.info " * loading #{uri}"
60
+ @source_specs = []
61
+ ::Gem::SpecFetcher.fetcher.load_specs( uri, 'specs' ).each do |name, version, platform|
62
+ @source_specs << SpecLite.new( name, version, platform )
63
+ end
64
+ end
65
+ return @source_specs
66
+ end
67
+
68
+ #
69
+ # force a load of the source_specs
70
+ #
71
+ def load_source_specs
72
+ @source_specs = nil
73
+ return source_specs
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,365 @@
1
+ module Stickler
2
+ #
3
+ # A source group contains a set of Source objects, and runs common operations
4
+ # across all of them.
5
+ #
6
+ class SourceGroup
7
+
8
+ # the repository this group belongs to
9
+ attr_reader :repository
10
+
11
+ def initialize( repository )
12
+ @repository = repository
13
+ @sources = {}
14
+
15
+ @fetcher = ::Gem::RemoteFetcher.new( nil )
16
+ @spec_fetcher = ::Gem::SpecFetcher.fetcher
17
+ end
18
+
19
+ #
20
+ # The root directory of the repository
21
+ #
22
+ def root_dir
23
+ @root_dir || repository.directory
24
+ end
25
+
26
+ #
27
+ # The specification directory in the repository
28
+ #
29
+ def specification_dir
30
+ @specification_dir ||= repository.specification_dir
31
+ end
32
+
33
+ #
34
+ # The specification behavior
35
+ #
36
+ def requirement_satisfaction_behavior
37
+ @requirement_satisfaction_behavior ||= repository.requirement_satisfaction_behavior
38
+ end
39
+
40
+ #
41
+ # The directory housing the actual .gem files
42
+ #
43
+ def gems_dir
44
+ @gems_dir ||= repository.gems_dir
45
+ end
46
+
47
+ #
48
+ # logger for this class
49
+ #
50
+ def logger
51
+ @logger ||= ::Logging::Logger[self]
52
+ end
53
+
54
+ #
55
+ # Add a source to the source group
56
+ #
57
+ def add_source( source_uri )
58
+ s = Source.new( source_uri, self )
59
+ @sources[s.uri] = s
60
+ end
61
+
62
+ #
63
+ # accessor for the available Source objects in this group
64
+ #
65
+ def sources
66
+ @sources.values
67
+ end
68
+
69
+ #
70
+ # Access all the gems that are in this gemspec This is a Hash of all gems in
71
+ # the source group. The keys are spec.full_name and the values are
72
+ # Gem::Specification instances
73
+ #
74
+ def gems
75
+ unless @gems
76
+ @gems = {}
77
+ Dir.glob( File.join( specification_dir, "*.gemspec" ) ).each do |spec_file|
78
+ begin
79
+ logger.info "Loading spec file #{spec_file}"
80
+ spec = eval( IO.read( spec_file ) )
81
+ @gems[ spec.full_name ] = spec
82
+ rescue => e
83
+ logger.error "Failure loading specfile #{File.basename( spec_file )} : #{e}"
84
+ end
85
+ end
86
+ end
87
+ return @gems
88
+ end
89
+
90
+ #
91
+ # Force a reload of the gem from the existing specs
92
+ #
93
+ def reload_gems!
94
+ @gems = nil
95
+ gems
96
+ end
97
+
98
+ #
99
+ # Return a list of Gem::Specification instances corresponding to the
100
+ # existing gems for a particular source
101
+ #
102
+ def existing_specs_for_source_uri( uri )
103
+ unless @existing_specs_for_source_uri
104
+ logger.debug "Loading existing_specs_for_source_uri"
105
+ @existing_specs_for_source_uri = Hash.new{ |h,k| h[k] = Array.new }
106
+ gems.values.each do |spec|
107
+ @existing_specs_for_source_uri[ source_uri_for_spec( spec ) ] << spec
108
+ end
109
+ end
110
+ @existing_specs_for_source_uri[ Source.normalize_uri( uri ) ]
111
+ end
112
+
113
+ #
114
+ # Search through all sources for all gems that satisfy the given
115
+ # Gem::Dependency
116
+ #
117
+ def search( dependency )
118
+ results = []
119
+ @sources.each_pair do |uri, src|
120
+ results.concat( src.search( dependency ) )
121
+ end
122
+ return results
123
+ end
124
+
125
+ #
126
+ # Search through all the existing specs for gemes that match the given
127
+ # Gem::Dependency
128
+ #
129
+ def search_existing( dependency )
130
+ results = gems.values.find_all do |spec|
131
+ dependency =~ Gem::Dependency.new( spec.name, spec.version )
132
+ end
133
+ end
134
+
135
+ #
136
+ # Add the gem that satisfies the dependency based upon the current
137
+ # satisfication method
138
+ #
139
+ def add_from_dependency( dep )
140
+ Console.info "Resolving gem dependencies for #{dep.to_s} ..."
141
+ specs_satisfying_dependency( dep ).each do |s|
142
+ add( s )
143
+ end
144
+ end
145
+
146
+ #
147
+ # Add the gem given by the spec and all of its dependencies.
148
+ #
149
+ def add( spec )
150
+ top_spec = spec
151
+ unless spec.instance_of?( ::Gem::Specification )
152
+ source_uri = source_uri_for_spec( spec )
153
+ top_spec = @spec_fetcher.fetch_spec( spec.to_a, source_uri )
154
+ end
155
+
156
+ add_list = []
157
+
158
+ todo = []
159
+ todo.push top_spec
160
+ seen = {}
161
+
162
+ until todo.empty? do
163
+ spec = todo.pop
164
+ next if seen[ spec.full_name ] or gems[ spec.full_name ]
165
+
166
+ logger.info "Queueing #{spec.full_name} for download"
167
+ add_list << spec
168
+
169
+ seen[ spec.full_name ] = true
170
+
171
+ deps = spec.runtime_dependencies
172
+ deps |= spec.development_dependencies
173
+
174
+ deps.each do |dep|
175
+ specs_satisfying_dependency( dep ).each do |s|
176
+ todo.push s
177
+ end
178
+ end
179
+ end
180
+
181
+ add_gems_and_specs( add_list )
182
+ reload_gems!
183
+ end
184
+
185
+
186
+ #
187
+ # unisntall the gem given by the spec and all gems that depend on it.
188
+ #
189
+ def remove( spec_or_list )
190
+ Console.info "Resolving remove dependencies..."
191
+
192
+ todo = [ spec_or_list ].flatten
193
+ remove_list = []
194
+
195
+ until todo.empty? do
196
+ spec = todo.pop
197
+ next if remove_list.include?( spec )
198
+
199
+ logger.info "queueing #{spec.full_name} for removal"
200
+ remove_list << spec
201
+
202
+ sibling_gems_of( spec ).each do |sspec|
203
+ Console.debug "pushing #{sspec.full_name} onto todo list"
204
+ todo.push sspec
205
+ end
206
+
207
+ specs_depending_on( spec ).each do |dspec|
208
+ Console.debug "pushing #{dspec.full_name} onto todo list"
209
+ todo.push dspec
210
+ end
211
+ end
212
+
213
+ remove_gems_and_specs( remove_list )
214
+ reload_gems!
215
+ end
216
+
217
+ #
218
+ # Return the list of existing Specifications that have the same name as
219
+ # then given spec
220
+ #
221
+ def sibling_gems_of( spec )
222
+ sibs = []
223
+ gems.values.each do |gspec|
224
+ if spec.name == gspec.name and spec.full_name != gspec.full_name then
225
+ sibs << gspec
226
+ end
227
+ end
228
+ return sibs
229
+ end
230
+
231
+ #
232
+ # Get the list of existing Specifications that have the input spec as
233
+ # either a runtime or development dependency
234
+ #
235
+ def specs_depending_on( spec )
236
+ deps = []
237
+ gems.values.each do |gspec|
238
+ gspec.dependencies.each do |dep|
239
+ if spec.satisfies_requirement?( dep ) then
240
+ deps << gspec
241
+ break
242
+ end
243
+ end
244
+ end
245
+ return deps
246
+ end
247
+
248
+ #
249
+ # Get the list of Specifications that satisfy the dependency
250
+ # based upon the current requirement satisfaction method
251
+ #
252
+ def specs_satisfying_dependency( dep )
253
+ unsorted = search( dep )
254
+ sorted = unsorted.sort_by { |s| s.version }
255
+
256
+ sorted.reverse! if requirement_satisfaction_behavior == :minimum
257
+
258
+ satisfies = []
259
+ matching_from_each_platform( sorted ).each do |spec|
260
+ su = source_uri_for_spec( spec )
261
+ satisfies << @spec_fetcher.fetch_spec( spec.to_a, su )
262
+ end
263
+
264
+ return satisfies
265
+ end
266
+
267
+ #
268
+ # collect the highest version from each distinct platform in the results and
269
+ # return that list
270
+ #
271
+ def matching_from_each_platform( results )
272
+ by_platform = {}
273
+ until results.empty?
274
+ spec = results.pop
275
+ if not by_platform.has_key?( spec.platform.to_s ) then
276
+ by_platform[ spec.platform.to_s ] = spec
277
+ end
278
+ end
279
+ return by_platform.values
280
+ end
281
+
282
+ #
283
+ # return the URI of the Source object that houses the upstream gem
284
+ #
285
+ def source_uri_for_spec( key_spec )
286
+ unless @source_uri_for_spec
287
+ logger.debug "Loading source_uri_to_spec"
288
+ @source_uri_for_spec = {}
289
+ @sources.each_pair do |uri, src|
290
+ src.source_specs.each do | s |
291
+ @source_uri_for_spec[ s.name_version ] = uri
292
+ end
293
+ end
294
+ end
295
+ @source_uri_for_spec[ key_spec.name_version ]
296
+ end
297
+
298
+
299
+ #
300
+ # remove the source from the source group
301
+ #
302
+ def remove_source( source_uri )
303
+ ulist = existing_specs_for_source_uri( source_uri )
304
+ remove( ulist )
305
+ @sources.delete( source_uri )
306
+ logger.info "removed #{source_uri}"
307
+ end
308
+
309
+ #
310
+ # Remove a list of gems from specifications
311
+ #
312
+ def remove_gems_and_specs( remove_list )
313
+ while spec = remove_list.pop do
314
+ Console.info "Removing #{ spec.full_name }"
315
+ delete_gem_files( spec )
316
+ end
317
+ end
318
+
319
+ #
320
+ # Remove all files from the repository related to this specification
321
+ #
322
+ def delete_gem_files( spec )
323
+ FileUtils.rm_f( File.join( gems_dir, "#{spec.full_name}.gem" ) )
324
+ FileUtils.rm_f( File.join( specification_dir, "#{spec.full_name}.gemspec" ))
325
+ end
326
+
327
+ #
328
+ # Add the gem represented by the spec
329
+ #
330
+ def add_gem( spec )
331
+
332
+ local_fetch_path = @fetcher.download( spec, source_uri_for_spec( spec ).to_s, root_dir )
333
+ dest_gem_path = File.join( gems_dir, File.basename( local_fetch_path ) )
334
+ logger.info "copying #{local_fetch_path} to #{dest_gem_path}"
335
+ FileUtils.cp local_fetch_path, dest_gem_path
336
+
337
+ return dest_gem_path
338
+ end
339
+
340
+
341
+ #
342
+ # Add the specification
343
+ #
344
+ def add_spec( spec )
345
+ rubycode = spec.to_ruby
346
+ file_name = File.join( specification_dir, "#{spec.full_name}.gemspec" )
347
+ logger.info "writing #{file_name}"
348
+ File.open( file_name, "wb" ) do |file|
349
+ file.puts rubycode
350
+ end
351
+ end
352
+
353
+
354
+ #
355
+ # Add all the gems and specifications in the add list
356
+ #
357
+ def add_gems_and_specs( add_list )
358
+ while spec = add_list.pop do
359
+ Console.info "Adding #{ spec.full_name }"
360
+ add_gem( spec )
361
+ add_spec( spec )
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,48 @@
1
+ module Stickler
2
+ #
3
+ # A lightweight version of a gemspec that only responds to name, version,
4
+ # platform and full_name. Many of the items in the rubygems world
5
+ # deal with the triplet [ name, verison, platform ] and this class
6
+ # encapsulates that.
7
+ #
8
+ class SpecLite
9
+ attr_reader :name
10
+ attr_reader :version
11
+ attr_reader :platform
12
+
13
+ def initialize( name, version, platform )
14
+ @name = name
15
+ @version = version
16
+ @new_platform = Gem::Platform.new( platform )
17
+ @platform = platform
18
+ end
19
+
20
+ def full_name
21
+ if platform == Gem::Platform::RUBY or platform.nil? then
22
+ "#{name}-#{version}"
23
+ else
24
+ "#{name}-#{version}-#{platform}"
25
+ end
26
+ end
27
+
28
+ def name_version
29
+ "#{name}-#{version}"
30
+ end
31
+
32
+ def to_a
33
+ [ name, version, platform.to_s ]
34
+ end
35
+
36
+ def to_s
37
+ full_name
38
+ end
39
+ end
40
+ end
41
+
42
+ module Gem
43
+ class Specification
44
+ def name_version
45
+ "#{name}-#{version}"
46
+ end
47
+ end
48
+ end