library 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,81 @@
1
+ require 'library' # this must be loaded in first
2
+
3
+ $RUBY_IGNORE_CALLERS ||= []
4
+ $RUBY_IGNORE_CALLERS << /#{__FILE__}/ # TODO: should this be more general, e.g. File.dirname(__FILE__) ?
5
+
6
+ module ::Kernel
7
+
8
+ class << self
9
+ alias __require__ require
10
+ alias __load__ load
11
+ end
12
+
13
+ alias __require__ require
14
+ alias __load__ load
15
+
16
+ #
17
+ # Acquire feature - This is Roll's modern require/load method.
18
+ # It differs from the usual `#require` or `#load` primarily by
19
+ # the fact that it will search the current loading library,
20
+ # i.e. the one belonging to the feature on the top of the
21
+ # #LOAD_STACK, before looking elsewhere. The reason we can't
22
+ # adjust `#require` to do this is becuase it could load a local
23
+ # feature when a non-local feature was intended. For example, if
24
+ # a library contained 'fileutils.rb' then this would be loaded
25
+ # rather the Ruby's standard library. When using `#acquire`,
26
+ # one would have to use the `ruby/` prefix to ensure the Ruby
27
+ # library gets loaded.
28
+ #
29
+ # @param pathname [String]
30
+ # The pathname of the feature to acquire.
31
+ #
32
+ # @param options [Hash]
33
+ # Load options are `:wrap`, `:load`, `:legacy` and `:search`.
34
+ #
35
+ # @return [true, false]
36
+ # Was the feature newly required or successfully loaded, depending
37
+ # on the `:load` option settings.
38
+ #
39
+ def acquire(pathname, options={}) #, &block)
40
+ Library.acquire(pathname, options) #, &block)
41
+ end
42
+
43
+ module_function :acquire
44
+
45
+ #
46
+ # Require feature - This is the same as acquire except that the
47
+ # `:legacy` option is fixed as `true`.
48
+ #
49
+ # @param pathname [String]
50
+ # The pathname of the feature to require.
51
+ #
52
+ # @param options [Hash]
53
+ # Load options can be `:wrap`, `:load` and `:search`.
54
+ #
55
+ # @return [true,false] if feature was newly required
56
+ #
57
+ def require(pathname, options={}) #, &block)
58
+ Library.require(pathname, options) #, &block)
59
+ end
60
+
61
+ module_function :require
62
+
63
+ #
64
+ # Load feature - This is the same as acquire except that the
65
+ # `:legacy` and `:load` options are fixed as `true`.
66
+ #
67
+ # @param pathname [String]
68
+ # The pathname of the feature to load.
69
+ #
70
+ # @param options [Hash]
71
+ # Load options can be :wrap and :search.
72
+ #
73
+ # @return [true, false] if feature was successfully loaded
74
+ #
75
+ def load(pathname, options={}) #, &block)
76
+ Library.load(pathname, options) #, &block)
77
+ end
78
+
79
+ module_function :load
80
+
81
+ end
@@ -0,0 +1,642 @@
1
+ class Library
2
+
3
+ # Ledger class track available libraries by library name.
4
+ # It is essentially a hash object, but with a special way
5
+ # of storing them to track versions. Each have key is the
6
+ # name of a library, as a String, and each value is either
7
+ # a Library object, if that particular version is active,
8
+ # or an Array of available versions of the library if inactive.
9
+ #
10
+ class Ledger
11
+
12
+ include Enumerable
13
+
14
+ #
15
+ # State of monitoring setting. This is used for debugging.
16
+ #
17
+ def monitor?
18
+ ENV['monitor'] || $MONITOR
19
+ end
20
+
21
+ #
22
+ def initialize
23
+ @table = Hash.new(){ |h,k| h[k] = [] }
24
+ end
25
+
26
+ #
27
+ # Add a library to the ledger.
28
+ #
29
+ # @param [String,Library]
30
+ # A path to a ruby library or a Library object.
31
+ #
32
+ # @return [Library] Added library object.
33
+ #
34
+ def add(lib)
35
+ case lib
36
+ when Library
37
+ add_library(lib)
38
+ else
39
+ add_location(lib)
40
+ end
41
+ end
42
+
43
+ alias_method :<<, :add
44
+
45
+ #
46
+ # Add library to ledger given a location.
47
+ #
48
+ # @return [Library] Added library object.
49
+ #
50
+ def add_location(location)
51
+ begin
52
+ library = Library.new(location)
53
+
54
+ entry = @table[library.name]
55
+
56
+ if Array === entry
57
+ entry << library unless entry.include?(library)
58
+ else
59
+ # todo: what to do here?
60
+ end
61
+ rescue Exception => error
62
+ warn error.message if ENV['debug']
63
+ end
64
+
65
+ library
66
+ end
67
+
68
+ #
69
+ # Add library to ledger given a Library object.
70
+ #
71
+ # @return [Library] Added library object.
72
+ #
73
+ def add_library(library)
74
+ #begin
75
+ raise TypeError unless Library === library
76
+
77
+ entry = @table[library.name]
78
+
79
+ if Array === entry
80
+ entry << library unless entry.include?(library)
81
+ end
82
+ #rescue Exception => error
83
+ # warn error.message if ENV['debug']
84
+ #end
85
+
86
+ library
87
+ end
88
+
89
+ #
90
+ # Get library or library version set by name.
91
+ #
92
+ # @param [String] name
93
+ # Name of library.
94
+ #
95
+ # @return [Library,Array] Library or lihbrary set referenced by name.
96
+ #
97
+ def [](name)
98
+ @table[name.to_s]
99
+ end
100
+
101
+ #
102
+ # Set ledger entry.
103
+ #
104
+ # @param [String] Name of library.
105
+ #
106
+ # @raise [TypeError] If library is not a Library object.
107
+ #
108
+ def []=(name, library)
109
+ raise TypeError unless Library === library
110
+
111
+ @table[name.to_s] = library
112
+ end
113
+
114
+ #
115
+ #
116
+ #
117
+ def replace(table)
118
+ initialize
119
+ table.each do |name, value|
120
+ @table[name.to_s] = value
121
+ end
122
+ end
123
+
124
+ #
125
+ # Iterate over each ledger entry.
126
+ #
127
+ def each(&block)
128
+ @table.each(&block)
129
+ end
130
+
131
+ #
132
+ # Size of the ledger is the number of libraries available.
133
+ #
134
+ # @return [Fixnum] Size of the ledger.
135
+ #
136
+ def size
137
+ @table.size
138
+ end
139
+
140
+ #
141
+ # Checks ledger for presents of library by name.
142
+ #
143
+ # @return [Boolean]
144
+ #
145
+ def key?(name)
146
+ @table.key?(name.to_s)
147
+ end
148
+
149
+ #
150
+ # Get a list of names of all libraries in the ledger.
151
+ #
152
+ # @return [Array<String>] list of library names
153
+ #
154
+ def keys
155
+ @table.keys
156
+ end
157
+
158
+ #
159
+ # Get a list of libraries and library version sets in the ledger.
160
+ #
161
+ # @return [Array<Library,Array>]
162
+ # List of libraries and library version sets.
163
+ #
164
+ def values
165
+ @table.values
166
+ end
167
+
168
+ #
169
+ # Inspection string.
170
+ #
171
+ # @return [String] Inspection string.
172
+ #
173
+ def inspect
174
+ @table.inspect
175
+ end
176
+
177
+ #
178
+ # Limit versions of a library to the given constraint.
179
+ # Unlike `#activate` this does not reduce the possible versions
180
+ # to a single library, but only reduces the number of possibilites.
181
+ #
182
+ # @param [String] name
183
+ # Name of library.
184
+ #
185
+ # @param [String] constraint
186
+ # Valid version constraint.
187
+ #
188
+ # @return [Array] List of conforming versions.
189
+ #
190
+ def constrain(name, contraint)
191
+ libraries = self[name]
192
+
193
+ return nil unless Array === libraries
194
+
195
+ vers = libraries.select do |library|
196
+ library.version.satisfy?(constraint)
197
+ end
198
+
199
+ self[name] = vers
200
+ end
201
+
202
+ #
203
+ # Activate a library, retrieving a Library instance by name, or name
204
+ # and version, and ensuring only that instance will be returned for
205
+ # all subsequent requests. Libraries are singleton, so once activated
206
+ # the same object is always returned.
207
+ #
208
+ # This method will raise a LoadError if the name is not found.
209
+ #
210
+ # Note that activating all runtime requirements of a library being
211
+ # activated was considered, but decided against. There's no reason
212
+ # to activatea library until it is actually needed. However this is
213
+ # not so when testing, or verifying available requirements, so other
214
+ # methods are provided such as `#activate_requirements`.
215
+ #
216
+ # @param [String] name
217
+ # Name of library.
218
+ #
219
+ # @param [String] constraint
220
+ # Valid version constraint.
221
+ #
222
+ # @return [Library]
223
+ # The activated Library object.
224
+ #
225
+ # @todo Should we also check $"? Eg. `return false if $".include?(path)`.
226
+ #
227
+ def activate(name, constraint=nil)
228
+ raise LoadError, "no such library -- #{name}" unless key?(name)
229
+
230
+ library = self[name]
231
+
232
+ if Library === library
233
+ if constraint
234
+ unless library.version.satisfy?(constraint)
235
+ raise Library::VersionConflict, library
236
+ end
237
+ end
238
+ else # library is an array of versions
239
+ if constraint
240
+ verscon = Version::Constraint.parse(constraint)
241
+ library = library.select{ |lib| verscon.compare(lib.version) }.max
242
+ else
243
+ library = library.max
244
+ end
245
+ unless library
246
+ raise VersionError, "no library version -- #{name} #{constraint}"
247
+ end
248
+
249
+ self[name] = library #constrain(library)
250
+ end
251
+
252
+ library
253
+ end
254
+
255
+ #
256
+ # Find matching library features. This is the "mac daddy" method used by
257
+ # the #require and #load methods to find the specified +path+ among
258
+ # the various libraries and their load paths.
259
+ #
260
+ def find_feature(path, options={})
261
+ path = path.to_s
262
+
263
+ #suffix = options[:suffix]
264
+ search = options[:search]
265
+ local = options[:local]
266
+ from = options[:from]
267
+
268
+ $stderr.print path if monitor? # debugging
269
+
270
+ # absolute, home or current path
271
+ #
272
+ # NOTE: Ideally we would try to find a matching path among avaliable libraries
273
+ # so that the library can be activated, however this would probably add a
274
+ # too much overhead and will by mostly a YAGNI, so we forgo any such
275
+ # functionality, at least for now.
276
+ case path[0,1]
277
+ when '/', '~', '.'
278
+ $stderr.puts " (absolute)" if monitor? # debugging
279
+ return nil
280
+ end
281
+
282
+ # from explicit library
283
+ if from
284
+ lib = library(from)
285
+ ftr = lib.find(path, options)
286
+ raise LoadError, "no such file to load -- #{path}" unless file
287
+ $stderr.puts " (direct)" if monitor? # debugging
288
+ return ftr
289
+ end
290
+
291
+ # check the load stack (TODO: just last or all?)
292
+ if local
293
+ if last = $LOAD_STACK.last
294
+ #$LOAD_STACK.reverse_each do |feature|
295
+ lib = last.library
296
+ if ftr = lib.find(path, options)
297
+ unless $LOAD_STACK.include?(ftr) # prevent recursive loading
298
+ $stderr.puts " (2 stack)" if monitor? # debugging
299
+ return ftr
300
+ end
301
+ end
302
+ end
303
+ end
304
+
305
+ name, fname = ::File.split_root(path)
306
+
307
+ # if the head of the path is the library
308
+ if fname
309
+ lib = Library[name]
310
+ if lib && ftr = lib.find(path, options) || lib.find(fname, options)
311
+ $stderr.puts " (3 indirect)" if monitor? # debugging
312
+ return ftr
313
+ end
314
+ end
315
+
316
+ # plain library name?
317
+ if !fname && lib = Library.instance(path)
318
+ if ftr = lib.default # default feature to load
319
+ $stderr.puts " (5 plain library name)" if monitor? # debugging
320
+ return ftr
321
+ end
322
+ end
323
+
324
+ # fallback to brute force search
325
+ #if search #or legacy
326
+ #options[:legacy] = true
327
+ if ftr = find_any(path, options)
328
+ $stderr.puts " (6 brute search)" if monitor? # debugging
329
+ return ftr
330
+ end
331
+ #end
332
+
333
+ $stderr.puts " (7 fallback)" if monitor? # debugging
334
+
335
+ nil
336
+ end
337
+
338
+ #
339
+ # Brute force variation of `#find` looks through all libraries for a
340
+ # matching features. This serves as the fallback method if `#find` comes
341
+ # up empty.
342
+ #
343
+ # @param [String] path
344
+ # path name for which to search
345
+ #
346
+ # @param [Hash] options
347
+ # Search options.
348
+ #
349
+ # @option options [Boolean] :latest
350
+ # Search only the active or most current version of any library.
351
+ #
352
+ # @option options [Boolean] :suffix
353
+ # Automatically try standard extensions if pathname has none.
354
+ #
355
+ # @option options [Boolean] :legacy
356
+ # Do not match within library's +name+ directory, eg. `lib/foo/*`.
357
+ #
358
+ # @return [Feature,Array] Matching feature(s).
359
+ #
360
+ def find_any(path, options={})
361
+ options = options.merge(:main=>true)
362
+
363
+ latest = options[:latest]
364
+
365
+ # TODO: Perhaps the selected and unselected should be kept in separate lists?
366
+ unselected, selected = *partition{ |name, libs| Array === libs }
367
+
368
+ # broad search of pre-selected libraries
369
+ selected.each do |(name, lib)|
370
+ if ftr = lib.find(path, options)
371
+ next if Library.load_stack.last == ftr
372
+ return ftr
373
+ end
374
+ end
375
+
376
+ # finally a broad search on unselected libraries
377
+ unselected.each do |(name, libs)|
378
+ libs = libs.sort
379
+ libs = [libs.first] if latest
380
+ libs.each do |lib|
381
+ ftr = lib.find(path, options)
382
+ return ftr if ftr
383
+ end
384
+ end
385
+
386
+ nil
387
+ end
388
+
389
+ #
390
+ # Brute force search looks through all libraries for matching features.
391
+ # This is the same as #find_any, but returns a list of matches rather
392
+ # then the first matching feature found.
393
+ #
394
+ # @param [String] path
395
+ # path name for which to search
396
+ #
397
+ # @param [Hash] options
398
+ # Search options.
399
+ #
400
+ # @option options [Boolean] :latest
401
+ # Search only the active or most current version of any library.
402
+ #
403
+ # @option options [Boolean] :suffix
404
+ # Automatically try standard extensions if pathname has none.
405
+ #
406
+ # @option options [Boolean] :legacy
407
+ # Do not match within library's +name+ directory, eg. `lib/foo/*`.
408
+ #
409
+ # @return [Feature,Array] Matching feature(s).
410
+ #
411
+ def search(path, options={})
412
+ options = options.merge(:main=>true)
413
+
414
+ latest = options[:latest]
415
+
416
+ matches = []
417
+
418
+ # TODO: Perhaps the selected and unselected should be kept in separate lists?
419
+ unselected, selected = *partition{ |name, libs| Array === libs }
420
+
421
+ # broad search of pre-selected libraries
422
+ selected.each do |(name, lib)|
423
+ if ftr = lib.find(path, options)
424
+ next if Library.load_stack.last == ftr
425
+ matches << ftr
426
+ end
427
+ end
428
+
429
+ # finally a broad search on unselected libraries
430
+ unselected.each do |(name, libs)|
431
+ libs = [libs.sort.first] if latest
432
+ libs.each do |lib|
433
+ ftr = lib.find(path, options)
434
+ matches << ftr if ftr
435
+ end
436
+ end
437
+
438
+ matches.uniq
439
+ end
440
+
441
+ #
442
+ # Search for all matching library files that match the given pattern.
443
+ # This could be of useful for plugin loader.
444
+ #
445
+ # @param [Hash] options
446
+ # Glob matching options.
447
+ #
448
+ # @option options [Boolean] :latest
449
+ # Search only activated libraries or the most recent version
450
+ # of a given library.
451
+ #
452
+ # @return [Array] Matching file paths.
453
+ #
454
+ # @todo Should this return list of Feature objects instead of file paths?
455
+ #
456
+ def glob(match, options={})
457
+ latest = options[:latest]
458
+
459
+ matches = []
460
+
461
+ each do |name, libs|
462
+ case libs
463
+ when Array
464
+ libs = libs.sort
465
+ libs = [libs.first] if latest
466
+ else
467
+ libs = [libs]
468
+ end
469
+
470
+ libs.each do |lib|
471
+ lib.loadpath.each do |path|
472
+ find = File.join(lib.location, path, match)
473
+ list = Dir.glob(find)
474
+ list = list.map{ |d| d.chomp('/') }
475
+ matches.concat(list)
476
+ end
477
+ end
478
+ end
479
+
480
+ matches
481
+ end
482
+
483
+ #
484
+ # Reduce the ledger to only those libraries the given library requires.
485
+ #
486
+ # @param [String] name
487
+ # The name of the primary library.
488
+ #
489
+ # @param [String] constraint
490
+ # The version constraint string.
491
+ #
492
+ # @return [Ledger] The ledger.
493
+ #
494
+ def isolate(name, constraint=nil)
495
+ library = activate(name, constraint)
496
+
497
+ # TODO: shouldn't this be done in #activate ?
498
+ acivate_requirements(library)
499
+
500
+ unused = []
501
+ each do |name, libs|
502
+ ununsed << name if Array === libs
503
+ end
504
+ unused.each{ |name| @table.delete(name) }
505
+
506
+ self
507
+ end
508
+
509
+ #
510
+ # Load up the ledger with a given set of paths and add an instance of
511
+ # the special `RubyLibrary` class after that.
512
+ #
513
+ # @param [Array] paths
514
+ #
515
+ # @option paths [Boolean] :expound
516
+ # Expound on path entires. See {#expound_paths}.
517
+ #
518
+ # @return [Ledger] The primed ledger.
519
+ #
520
+ def prime(*paths)
521
+ options = Hash === paths.last ? paths.pop : {}
522
+
523
+ paths = expound_paths(*paths) if options[:expound]
524
+
525
+ require 'library/rubylib'
526
+
527
+ paths.each do |path|
528
+ begin
529
+ add_location(path) if library_path?(path)
530
+ rescue => err
531
+ $stderr.puts err.message if ENV['debug']
532
+ end
533
+ end
534
+
535
+ add_library(RubyLibrary.new)
536
+
537
+ self
538
+ end
539
+
540
+ private
541
+
542
+ #
543
+ # For flexible priming, this method can be used to recursively
544
+ # lookup library locations.
545
+ #
546
+ # If a given path is a file, it will considered a lookup "roll",
547
+ # such that each line entry in the file is considered another
548
+ # path to be expounded upon.
549
+ #
550
+ # If a given path is a directory, it will be returned if it
551
+ # is a valid Ruby library location, otherwise each subdirectory
552
+ # will be checked to see if it is a valid Ruby library location,
553
+ # and returned if so.
554
+ #
555
+ # If, on the other hand, a given path is a file glob pattern,
556
+ # the pattern will be expanded and those paths will expounded
557
+ # upon in turn.
558
+ #
559
+ def expound_paths(*entries)
560
+ paths = []
561
+
562
+ entries.each do |entry|
563
+ entry = entry.strip
564
+
565
+ next if entry.empty?
566
+ next if entry.start_with?('#')
567
+
568
+ if File.directory?(entry)
569
+ if library_path?(entry)
570
+ paths << entry
571
+ else
572
+ subpaths = Dir.glob(File.join(entry, '*/'))
573
+ subpaths.each do |subpath|
574
+ paths << subpath if library_path?(subpath)
575
+ end
576
+ end
577
+ elsif File.file?(entry)
578
+ paths.concat(expound_paths(*File.readlines(entry)))
579
+ else
580
+ glob_paths = Dir.glob(entry)
581
+ if glob_paths.first != entry
582
+ paths.concat(expound_paths(*glob_paths))
583
+ end
584
+ end
585
+ end
586
+
587
+ paths
588
+ end
589
+
590
+ #
591
+ # Is a directory a Ruby library?
592
+ #
593
+ # @todo Support gem home location.
594
+ #
595
+ def library_path?(path)
596
+ dotruby?(path) || (ENV['RUBYLIBS_GEMSPEC'] && gemspec?(path))
597
+ end
598
+
599
+ # TODO: First recursively constrain the ledger, then activate. That way
600
+ # any missing libraries will cause an error. (hmmm... actually that's
601
+ # an imperfect way to resolve version dependencies). Ultimately we probably
602
+ # need a separate module to handle this.
603
+
604
+ #
605
+ # Activate library requirements.
606
+ #
607
+ # @todo: checklist is used to prevent possible infinite recursion, but
608
+ # it might be better to put a flag in Library instead.
609
+ #
610
+ def acivate_requirements(library, development=false, checklist={})
611
+ reqs = development ? library.requirements : library.runtime_requirements
612
+
613
+ checklist[library] = true
614
+
615
+ reqs.each do |req|
616
+ name = req['name']
617
+ vers = req['version']
618
+
619
+ library = activate(name, vers)
620
+
621
+ acivate_requirements(library, development, checklist) unless checklist[library]
622
+ end
623
+ end
624
+
625
+ #
626
+ # Does the directory have a `.ruby` file?
627
+ #
628
+ def dotruby?(path)
629
+ File.file?(File.join(path, '.ruby'))
630
+ end
631
+
632
+ #
633
+ # Does a path have a `.gemspec` file? This is fallback measure if a .ruby file is not found.
634
+ #
635
+ def gemspec?(path)
636
+ glob = File.file?(File.join(path, '{,*}.gemspec'))
637
+ Dir[glob].first
638
+ end
639
+
640
+ end
641
+
642
+ end