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.
- data/COPYING +339 -0
- data/HISTORY +4 -0
- data/LICENSE +54 -0
- data/README +86 -0
- data/bin/stickler +58 -0
- data/data/stickler.yml +14 -0
- data/gemspec.rb +62 -0
- data/lib/stickler.rb +19 -0
- data/lib/stickler/cli.rb +302 -0
- data/lib/stickler/configuration.rb +74 -0
- data/lib/stickler/console.rb +72 -0
- data/lib/stickler/paths.rb +62 -0
- data/lib/stickler/repository.rb +502 -0
- data/lib/stickler/source.rb +76 -0
- data/lib/stickler/source_group.rb +365 -0
- data/lib/stickler/spec_lite.rb +48 -0
- data/lib/stickler/version.rb +36 -0
- data/spec/configuration_spec.rb +68 -0
- data/spec/paths_spec.rb +25 -0
- data/spec/repository_spec.rb +55 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/version_spec.rb +17 -0
- data/tasks/announce.rake +39 -0
- data/tasks/config.rb +107 -0
- data/tasks/distribution.rake +38 -0
- data/tasks/documentation.rake +31 -0
- data/tasks/rspec.rake +29 -0
- data/tasks/rubyforge.rake +51 -0
- data/tasks/utils.rb +80 -0
- metadata +156 -0
@@ -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
|