stickler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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