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,74 @@
1
+ module Stickler
2
+ #
3
+ #
4
+ #
5
+ class Configuration
6
+
7
+ attr_reader :config_file_name
8
+
9
+ def initialize( config_file_name )
10
+ @config_file_name = config_file_name
11
+ @hash = YAML.load_file( config_file_name )
12
+ @hash['sources'] = @hash['sources'].collect do |uri|
13
+ p = uri.split("/")
14
+ p.join("/") + "/" # ensure a trailing slash
15
+ end
16
+ end
17
+
18
+ # the array of sources in this configuration
19
+ def sources
20
+ hash['sources']
21
+ end
22
+
23
+ # the downstream source this repository represents
24
+ def downstream_source
25
+ hash['downstream_source']
26
+ end
27
+
28
+ # The gem and version requirements for this repository
29
+ def gem_dependencies
30
+ unless @gem_dependencies
31
+ @gem_dependencies = []
32
+ if hash['gems'] then
33
+ hash['gems'].each do |name, reqs|
34
+ [ reqs ].flatten.each do |req|
35
+ @gem_dependencies << ::Gem::Dependency.new( name, req )
36
+ end
37
+ end
38
+ end
39
+ end
40
+ return @gem_dependencies
41
+ end
42
+
43
+ def write
44
+ File.open( config_file_name, 'w' ) do |f|
45
+ g = Hash.new { |h,k| h[k] = [] }
46
+ gem_dependencies.each do |d|
47
+ g[d.name] << d.requirement_list
48
+ g[d.name].flatten!
49
+ end
50
+ hash['gems'] = g
51
+ f.write hash.to_yaml
52
+ end
53
+ end
54
+
55
+ def keys
56
+ @hash.keys
57
+ end
58
+
59
+ def []( key )
60
+ @hash[ key.to_s ]
61
+ end
62
+
63
+ def []=( key, value )
64
+ @hash[ key.to_s ] = value
65
+ end
66
+
67
+ def ==( other )
68
+ self.class === other and hash == other.hash
69
+ end
70
+
71
+ protected
72
+ attr_reader :hash
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+
2
+ module Stickler
3
+ #
4
+ # containment for the output that a user should see. This uses a Logger so
5
+ # that it can be throttled based on level at some point
6
+ #
7
+ class Console
8
+ class << self
9
+ #
10
+ # Items that get logged to stderr for the user to see should use this logger
11
+ #
12
+ def logger
13
+ unless @logger
14
+ @logger = ::Logging::Logger['User']
15
+ @logger.level = :info
16
+ @logger.add_appenders(::Logging::Appender.stderr)
17
+ ::Logging::Appender.stderr.layout = Logging::Layouts::Pattern.new( :pattern => "%m\n" )
18
+ ::Logging::Appender.stderr.level = :info
19
+ end
20
+ return @logger
21
+ end
22
+
23
+ # force initialization
24
+ Console.logger
25
+
26
+ #
27
+ # default logging leve
28
+ #
29
+ def default_level
30
+ :info
31
+ end
32
+
33
+ #
34
+ # Quick wrappers around the log levels
35
+ #
36
+ ::Logging::LEVELS.keys.each do |l|
37
+ module_eval <<-code
38
+ def #{ l }( msg )
39
+ @logger.#{l} msg
40
+ end
41
+ code
42
+ end
43
+
44
+ #
45
+ # Turn off the logging
46
+ #
47
+ def silent!
48
+ logger.level = :off
49
+ end
50
+
51
+ #
52
+ # Resume logging
53
+ #
54
+ def resume!
55
+ logger.level = self.default_level
56
+ end
57
+
58
+
59
+ #
60
+ # Turn off logging for the execution of a block
61
+ #
62
+ def silent( &block )
63
+ begin
64
+ silent!
65
+ block.call
66
+ ensure
67
+ resume!
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,62 @@
1
+ #--
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. Licensed under the same terms as Ruby. No warranty is
4
+ # provided. See LICENSE and COPYING for details.
5
+ #++
6
+
7
+ module Stickler
8
+
9
+ # Paths module used by all the other modules and classes for
10
+ # determining paths and default values.
11
+ #
12
+ module Paths
13
+
14
+ #
15
+ # The root directory of the project is considered to be the parent directory
16
+ # of the 'lib' directory. returns the full expanded path of the parent
17
+ # directory of 'lib' going up the path from the current file. A trailing
18
+ # File::SEPARATOR is guaranteed.
19
+ #
20
+ def self.root_dir
21
+ unless @root_dir
22
+ path_parts = ::File.expand_path(__FILE__).split(::File::SEPARATOR)
23
+ lib_index = path_parts.rindex("lib")
24
+ @root_dir = path_parts[0...lib_index].join(::File::SEPARATOR) + ::File::SEPARATOR
25
+ end
26
+ return @root_dir
27
+ end
28
+
29
+ #
30
+ # return the full expanded path of the +config+ directory below +root_dir+.
31
+ # All parameters passed in are joined on to the result. a Trailing
32
+ # File::SEPARATOR is guaranteed if _args_ are *not* present
33
+ #
34
+ def self.config_path(*args)
35
+ self.sub_path("config", *args)
36
+ end
37
+
38
+ #
39
+ # return the full expanded path of the +data+ directory below +root_dir+.
40
+ # All parameters passed in are joined on to the result. a Trailing
41
+ # File::SEPARATOR is guaranteed if _args_ are *not* present
42
+ #
43
+ def self.data_path(*args)
44
+ self.sub_path("data", *args)
45
+ end
46
+
47
+ #
48
+ # return the full expanded path of the +lib+ directory below +root_dir+.
49
+ # All parameters passed in are joined on to the result. a Trailing
50
+ # File::SEPARATOR is guaranteed if _args_ are *not* present
51
+ #
52
+ def self.lib_path(*args)
53
+ self.sub_path("lib", *args)
54
+ end
55
+
56
+ private
57
+ def self.sub_path(sub,*args)
58
+ sp = ::File.join(root_dir, sub) + File::SEPARATOR
59
+ sp = ::File.join(sp, *args) if args
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,502 @@
1
+
2
+ # Copyright (c) 2008 Jeremy Hinegardner
3
+ # All rights reserved. Licensed under the same terms as Ruby. No warranty is
4
+ # provided. See LICENSE and COPYING for details.
5
+ #++
6
+
7
+ require 'stickler'
8
+ require 'fileutils'
9
+ require 'ostruct'
10
+ require 'highline'
11
+ require 'stickler/configuration'
12
+ require 'stickler/source_group'
13
+
14
+ module Stickler
15
+ #
16
+ # A Repository a directory with a particular layout used by stickler. It is
17
+ # is very similar to a GEM_HOME directory. The layout is as follows, this is
18
+ # all assumed to be under STICKLER_HOME.
19
+ #
20
+ # stickler.yml - the Stickler configuration file for this repository.
21
+ # The existence of this file indicates this is the root
22
+ # of a stickler repository
23
+ # gems/ - storage of the actual gem files
24
+ # cache/ - cache of gem files downloaded from upstream sources
25
+ # managed by the rubygems fetcher classes
26
+ # specifications/ - ruby gemspec files
27
+ # log/ - directory holding rotating logs files for stickler
28
+ # dist/ - directory holding the distributable gem index location,
29
+ # rsync this to your webserver, or serve from it directly.
30
+ #
31
+ #
32
+ class Repository
33
+ class Error < ::StandardError ; end
34
+
35
+ # The repository directory. the directory containing the stickler.yml file
36
+ attr_reader :directory
37
+
38
+ # The configuration
39
+ attr_reader :configuration
40
+
41
+ # Are requrements satisfied in a minimal or maximal approach
42
+ attr_reader :requirement_satisfaction_behavior
43
+
44
+ class << self
45
+ def other_dir_names
46
+ %w[ gems_dir log_dir specification_dir dist_dir cache_dir ]
47
+ end
48
+
49
+ def config_file_basename
50
+ "stickler.yml"
51
+ end
52
+
53
+ def basedir
54
+ "stickler"
55
+ end
56
+
57
+ #
58
+ # What should the default stickler directory be, this is one of two
59
+ # things. If there is a stickler.yml file in the current directory, then
60
+ # the default directory is the current directory, otherwise, it is the
61
+ # _stickler_ directory below the current_directory
62
+ #
63
+ def default_directory
64
+ defaults = [ File.join( Dir.pwd, config_file_basename ), File.join( Dir.pwd, basedir ) ]
65
+ defaults.each do |def_dir|
66
+ if File.exist?( def_dir ) then
67
+ return File.dirname( def_dir ) if File.file?( def_dir )
68
+ return def_dir
69
+ end
70
+ end
71
+ return defaults.last
72
+ end
73
+
74
+ #
75
+ # gem requirement information
76
+ #
77
+ def requirement_meta
78
+ [ [ "=" , "Equals version" ],
79
+ [ "!=" , "Not equal to version" ],
80
+ [ ">" , "Greater than version" ],
81
+ [ "<" , "Less than version" ],
82
+ [ ">=" , "Greater than or equal to" ],
83
+ [ "<=" , "Less than or equal to" ],
84
+ [ "~>" , "Approximately greater than" ]]
85
+ end
86
+ end
87
+
88
+ #
89
+ # Initialize a stickler repository
90
+ #
91
+ def initialize( opts )
92
+
93
+ @directory = File.expand_path( opts['directory'] )
94
+ @requirement_satisfaction_behavior = ( opts['requirements'] || "maximum" ).to_sym
95
+ enhance_logging( opts ) if File.directory?( log_dir )
96
+ @overwrite = opts['force']
97
+
98
+ @configuration = nil
99
+ load_configuration if File.exist?( config_file )
100
+
101
+ end
102
+
103
+ def configuration_loaded?
104
+ @configuration.nil?
105
+ end
106
+
107
+ #
108
+ # should existing items be overwritten
109
+ #
110
+ def overwrite?
111
+ @overwrite
112
+ end
113
+
114
+ #
115
+ # update logging by turning on a log file in the repository directory, and
116
+ # possibly turning off the stdout logger that is the default.
117
+ #
118
+ def enhance_logging( opts )
119
+
120
+ layout = ::Logging::Layouts::Pattern.new(
121
+ :pattern => "[%d] %c %6p %5l : %m\n",
122
+ :date_pattern => "%Y-%m-%d %H:%M:%S"
123
+ )
124
+ Logging::Logger.root.add_appenders ::Logging::Appenders::RollingFile.new( 'stickler_rolling_logfile',
125
+ { :filename => log_file,
126
+ :layout => layout,
127
+ # at 5MB roll the log
128
+ :size => 5 * (1024**2),
129
+ :keep => 5,
130
+ :safe => true,
131
+ :level => :debug
132
+ })
133
+ end
134
+
135
+ #
136
+ # return a handle to the repository configuration found in stickler.yml.
137
+ #
138
+ def load_configuration
139
+ begin
140
+ @configuration = Configuration.new( config_file )
141
+ source_group # force load
142
+ rescue => e
143
+ logger.error "Failure to load configuration #{e}"
144
+ exit 1
145
+ end
146
+ end
147
+
148
+ #
149
+ # The configuration file for the repository
150
+ #
151
+ def config_file
152
+ @config_file ||= File.join( directory, Repository.config_file_basename )
153
+ end
154
+
155
+ #
156
+ # The log directory
157
+ #
158
+ def log_dir
159
+ @log_dir ||= File.join( directory, 'log' )
160
+ end
161
+
162
+ #
163
+ # The log file
164
+ #
165
+ def log_file
166
+ @log_file ||= File.join( log_dir, 'stickler.log' )
167
+ end
168
+
169
+ #
170
+ # The gem storage directory.
171
+ #
172
+ # This holds the raw gem files downloaded from the sources. This is
173
+ # equivalent to a gem installations 'gems' directory.
174
+ #
175
+ def gems_dir
176
+ @gems_dir ||= File.join( directory, 'gems' )
177
+ end
178
+
179
+ #
180
+ # The Gem specification directory
181
+ #
182
+ def specification_dir
183
+ @specification_dir ||= File.join( directory, 'specifications' )
184
+ end
185
+
186
+ #
187
+ # The cache dir for the downloads
188
+ #
189
+ def cache_dir
190
+ @cache_dir ||= File.join( directory, 'cache' )
191
+ end
192
+
193
+ #
194
+ # The Distribution directory
195
+ #
196
+ # this is the document root for the webserver that will serve your rubygems.
197
+ # Or they can be served directly from this location
198
+ #
199
+ def dist_dir
200
+ @dist_dir ||= File.join( directory, 'dist' )
201
+ end
202
+
203
+ #
204
+ # logging handler
205
+ #
206
+ def logger
207
+ @logger ||= ::Logging::Logger[self]
208
+ end
209
+
210
+ #
211
+ # The SourceGroup containing all of the sources for this repository
212
+ #
213
+ def source_group
214
+ unless @source_group
215
+ sg = SourceGroup.new( self )
216
+ Console.info "Setting up sources"
217
+ configuration.sources.each do |source_uri|
218
+ sg.add_source( source_uri )
219
+ end
220
+ @source_group = sg
221
+ end
222
+ return @source_group
223
+ end
224
+
225
+ #
226
+ # Is the repository valid?
227
+ #
228
+ def valid?
229
+ if @valid.nil? then
230
+ begin
231
+ valid!
232
+ @valid = true
233
+ rescue StandardError => e
234
+ logger.error "Repository is not valid : #{e}"
235
+ @valid = false
236
+ end
237
+ end
238
+ return @valid
239
+ end
240
+
241
+ #
242
+ # raise an error if the repository is not valid
243
+ #
244
+ def valid!
245
+ raise Error, "#{directory} does not exist" unless File.exist?( directory )
246
+ raise Error, "#{directory} is not writable" unless File.writable?( directory )
247
+
248
+ raise Error, "#{config_file} does not exist" unless File.exist?( config_file )
249
+ raise Error, "#{config_file} is not loaded" unless configuration
250
+
251
+ Repository.other_dir_names.each do |method|
252
+ other_dir = self.send( method )
253
+ raise Error, "#{other_dir} does not exist" unless File.exist?( other_dir )
254
+ raise Error, "#{other_dir} is not writeable" unless File.writable?( other_dir )
255
+ end
256
+
257
+ if File.exist?( log_file ) then
258
+ raise Error, "#{log_file} is not writable" unless File.writable?( log_file )
259
+ end
260
+ end
261
+
262
+ #
263
+ # Setup the repository.
264
+ #
265
+ # This is executed with the 'setup' mode on the command line. Only those
266
+ # files and directories that do not already exist are created. Nothing is
267
+ # destroyed.
268
+ #
269
+ def setup
270
+ if File.exist?( directory ) then
271
+ Console.info "repository root already exists #{directory}"
272
+ else
273
+ FileUtils.mkdir_p( directory )
274
+ Console.info "created repository root #{directory}"
275
+ end
276
+
277
+ Repository.other_dir_names.each do |method|
278
+ d = self.send( method )
279
+ if File.exist?( d ) then
280
+ Console.info "directory #{d} already exists"
281
+ else
282
+ FileUtils.mkdir_p( d )
283
+ Console.info "created directory #{d}"
284
+ end
285
+ end
286
+
287
+ if overwrite? or not File.exist?( config_file ) then
288
+ FileUtils.cp Stickler::Paths.data_path( Repository.config_file_basename ), config_file
289
+ Console.info "copied in default configuration to #{config_file}"
290
+ else
291
+ Console.info "configuration file #{config_file} already exists"
292
+ end
293
+
294
+ # load the configuration for the repo
295
+ load_configuration
296
+
297
+ rescue => e
298
+ $stderr.puts "Unable to setup the respository"
299
+ $stderr.puts e
300
+ $stderr.puts e.backtrace.join("\n")
301
+ exit 1
302
+ end
303
+
304
+ #
305
+ # Report information about the repository. This is what is called from the
306
+ # 'info' mode on the commandline
307
+ #
308
+ def info
309
+ return unless valid?
310
+
311
+ Console.info ""
312
+ Console.info "Upstream Sources"
313
+ Console.info "----------------"
314
+ Console.info ""
315
+
316
+ max_width = configuration.sources.collect { |s| s.length }.max
317
+ source_group.sources.each do |source|
318
+ Console.info " #{source.uri.to_s.rjust( max_width )} : #{source.source_specs.size} gems available"
319
+ Console.info " #{" ".rjust( max_width )} : #{source_group.existing_specs_for_source_uri( source.uri ).size} gems existing"
320
+ end
321
+
322
+
323
+ Console.info ""
324
+ Console.info "Configured gems (in stickler.yml)"
325
+ Console.info "---------------------------------"
326
+ Console.info ""
327
+ configuration.gem_dependencies.sort.each do |dep|
328
+ Console.info "#{dep.name} : #{dep.version_requirements}"
329
+ end
330
+
331
+ Console.info ""
332
+ Console.info "Existing gems"
333
+ Console.info "-------------"
334
+ Console.info ""
335
+
336
+ source_group.gems.keys.sort.each do |g|
337
+ puts g
338
+ end
339
+
340
+
341
+ end
342
+
343
+ #
344
+ # Add a source to the repository
345
+ #
346
+ def add_source( source_uri )
347
+ load_configuration unless configuration_loaded?
348
+ if configuration.sources.include?( source_uri ) then
349
+ Console.info "#{source_uri} already in sources"
350
+ else
351
+ source_group.add_source( source_uri )
352
+ configuration.sources << source_uri
353
+ configuration.write
354
+ Console.info "#{source_uri} added to sources"
355
+ end
356
+ end
357
+
358
+ #
359
+ # Remove a source from the repository
360
+ #
361
+ def remove_source( source_uri )
362
+ load_configuration unless configuration_loaded?
363
+ uri = ::URI.parse source_uri
364
+ if configuration.sources.delete( source_uri ) then
365
+ source_group.remove_source( source_uri )
366
+ configuration.write
367
+ Console.info "#{source_uri} removed from sources"
368
+ else
369
+ Console.info "#{source_uri} is not one of your sources"
370
+ Console.info "Your sources are:"
371
+ configuration.sources.each do |src|
372
+ Console.info " #{src}"
373
+ end
374
+ end
375
+ end
376
+
377
+ #
378
+ # Add a gem to the repository
379
+ #
380
+ def add_gem( gem_name, version )
381
+
382
+ Console.info ""
383
+
384
+ ::HighLine.track_eof = false
385
+ hl = ::HighLine.new( STDIN, STDOUT, :auto)
386
+ hl.say("You need to pick the #{gem_name} Requirement to configure Stickler.")
387
+ hl.say("This involves picking one of the following Requirement operators")
388
+ hl.say('See http://docs.rubygems.org/read/chapter/16#page74 for operator info.')
389
+ hl.say("\nYou need to (1) pick an operator and (2) pick a requirement.")
390
+ hl.say("The most common operators are >=, > and ~>")
391
+
392
+ op = hl.choose(*Repository.requirement_meta.collect { |k,v| "#{k.ljust(3)}#{v}" } ) do |m|
393
+ m.prompt = "(1) Pick an operator ? "
394
+ end
395
+
396
+ op = op.split.first # get only the operator, not the trailing text
397
+
398
+ version = ::Gem::Requirement.default if version == :latest
399
+ search_pattern = ::Gem::Dependency.new( gem_name, version )
400
+ choices = []
401
+ source_group.search( search_pattern ).each do |spec|
402
+ choices << "#{op} #{spec.version.to_s}"
403
+ end
404
+ choices = choices.sort.reverse
405
+
406
+ hl.say("\nNow to pick a requirement. Based upon your chosen operator '#{op}',")
407
+ hl.say("These are the available version of the #{gem_name} gem.")
408
+ requirement = hl.choose do |m|
409
+ m.flow = :columns_down
410
+ m.prompt = "(2) Pick a requirement ? "
411
+ m.choices( *choices )
412
+ end
413
+
414
+ Console.info ""
415
+
416
+ dep = ::Gem::Dependency.new( gem_name, requirement )
417
+ if configuration.gem_dependencies.include?( dep ) then
418
+ Console.info "#{dep} is already in your list of gems"
419
+ else
420
+ source_group.add_from_dependency( dep )
421
+ configuration.gem_dependencies << dep
422
+ configuration.write
423
+ end
424
+ end
425
+
426
+ #
427
+ # Remove a gem from the repository
428
+ #
429
+ def remove_gem( gem_name, version )
430
+ Console.info ""
431
+ version = ::Gem::Requirement.default if version == :all
432
+ search_pattern = ::Gem::Dependency.new( gem_name, version )
433
+ ulist = source_group.search_existing( search_pattern )
434
+ source_group.search_existing( search_pattern ).each do |spec|
435
+ source_group.remove( spec )
436
+ configuration.gem_dependencies.reject! { |d| d.name == spec.name }
437
+ end
438
+ configuration.write
439
+ end
440
+
441
+ #
442
+ # Sync the repository
443
+ #
444
+ def sync( rebuild = false )
445
+ Console.info ""
446
+ Console.info "Making sure that all gems listed in configuration are available"
447
+ Console.info ""
448
+
449
+ if rebuild then
450
+ Console.info "Removing existing gems and specifications ... "
451
+ Dir[ File.join( gems_dir, "*.gem" ) ].each { |g| FileUtils.rm_f g }
452
+ Dir[ File.join( specification_dir , "*.gemspec" ) ].each { |s| FileUtils.rm_f s }
453
+ end
454
+ configuration.gem_dependencies.each do |dep|
455
+ source_group.add_from_dependency( dep )
456
+ end
457
+ end
458
+
459
+ #
460
+ # generate the system configuration to be used by rubygem clients of the
461
+ # repository that stickler managers
462
+ #
463
+ def generate_sysconfig( to = $stdout )
464
+ Console.info "Generating configuration to stdout"
465
+ txt = <<-cfg
466
+ #
467
+ # This is the system wide configuration to be used by
468
+ # rubygem clients that install gems from the repository
469
+ # located at :
470
+ #
471
+ # #{configuration.downstream_source}
472
+ #
473
+ # On Unix like machines install in
474
+ #
475
+ # /etc/gemrc
476
+ #
477
+ # On Windows machines install in
478
+ #
479
+ # C:\\Documents and Settings\\All Users\\Application Data\\gemrc
480
+ #
481
+ ---
482
+ :sources:
483
+ - #{configuration.downstream_source}
484
+ cfg
485
+ to.puts txt
486
+ end
487
+
488
+ #
489
+ # Generate the gem index that can be rsynced to another location
490
+ #
491
+ #
492
+ def generate_index
493
+ require 'rubygems/indexer'
494
+ Console.info "Generating rubygems index in #{dist_dir}"
495
+ FileUtils.rm_rf dist_dir
496
+ FileUtils.mkdir_p dist_dir
497
+ FileUtils.cp_r gems_dir, dist_dir
498
+ indexer = ::Gem::Indexer.new dist_dir
499
+ indexer.generate_index
500
+ end
501
+ end
502
+ end