library 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,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