# TITLE: # # library.rb # # LICENSE: # # Copyright (c)2007 Thomas Sawyer, Ruby License # # TODO: # # - Commented out #import for now. Facets' module_require and module_load # are probably better. Implement those for Rolls instead. (Yes?) # - Maybe add autopackage install system? # - What about remotes? What about version tiers when remote requiring? require 'rbconfig' require 'fileutils' require 'roll/package' require 'roll/version' #require 'roll/sign' # We need to hold a copy of the original $LOAD_PATH # for specified "ruby: ..." loads. $RUBY_PATH = $LOAD_PATH.dup # Locations of rolls-ready libraries. # # We could use the site_ruby locations instead, but that creates an odd sort of overlap in # # the load paths, and would also promote putting the information file in the lib/ dir. So # # I think it's better to use a separate location. (After all site_ruby may just fade away.) # $LOAD_SITE = $LOAD_PATH - Config::CONFIG.values_at('rubylibdir', 'archdir') - ['.'] rolldir = 'ruby_library' sitedir = ::Config::CONFIG['sitedir'] version = ::Config::CONFIG['ruby_version'] $LOAD_SITE = [File.join(File.dirname(sitedir), rolldir, version)] + ENV['ROLL_PATH'].to_s.split(':') # = Library Class # # The Library class serves as an objecified location in Ruby's load paths. # # The Library qua class also serves as the library manager, storing a ledger of # available libraries. # # A library is roll-ready when it supplies a {name}-{verison}.roll file in either its # base directory or in the base's meta/ directory. The roll file name is specifically # designed to make library lookup fast. There is no need for Rolls to open the roll # file until an actual version is used. It also gives the most flexability in repository # layout. Rolls searches up to three subdirs deep looking for roll files. This is # suitable to non-versioned libs, versioned libs, non-versioned subprojects and subprojects, # including typical Subversion repository layouts. class Library # Dynamic link extension. def self.dlext @dlext ||= '.' + ::Config::CONFIG['DLEXT'] end # Location of rolls-ready libs. def self.load_site $LOAD_SITE end # def self.load_path $LOAD_PATH end # def self.ruby_path $RUBY_PATH end # Name of library. attr_reader :name # Version of library. This is a VersionNumber object. attr_reader :version # Path to library. attr_reader :location # New libray. def initialize(location, identity=nil) #name, version, location=nil) @location = location if identity @name = identity[:name] @version = identity[:version] @load_path = identity[:load_path] || identity[:load_paths] else identify(location) end raise unless @name raise unless @version @identity = identity # TODO Version number needs to be more flexiable in handling non-numeric tuples. @version = VersionNumber.new(@version) unless VersionNumber === @version end # Identify a library based on it's location. def identify(location) file = File.join(location,'{,meta/}*.roll') if file = Dir.glob(file).first @name, @version = file.chomp('.roll').split('-') end end # Inspection. def inspect if version "#" else "#" end end def roll_file find = "{,meta/}*.roll" # "{,meta/}#{name}-#{version}.roll" file = Dir.glob(File.join(location, find), File::FNM_CASEFOLD).first end # Read roll file. def load_roll return unless location # NEED TO DO THIS BETTER. THIS IS HERE FOR THE ONE 'ruby' CASE. raise LoadError unless File.directory?(location) glob = File.join(location, "{,meta/}*.roll") # "{,meta/}#{name}-#{version}.roll" file = Dir.glob(glob, File::FNM_CASEFOLD).first if file @roll_file = file @roll = Roll::Package.open(file, :name=>name, :version=>version.to_s) else raise "THIS SHOULD NOT POSSIBLE!" #@roll = false #Roll::Package.new # THIS IN NO LONGER POSSIBLE end #use #? return @roll end # Does this library have an roll file? #def rolled? # roll ? true : false #end # Does this library have a remote source? #def remote? # rolled? and source and pubkey #end # Meta information about the libray. def roll @roll = load_roll if @roll.nil? # If it's false, we don't have one. @roll end alias_method :info, :roll # Compare by version. def <=>(other) version <=> other.verison end # List of subdirectories that are searched when loading libs. # In general this should include all the libs internal load paths, # so long as there will be no name conflicts between directories. def lib_path return unless roll # NEED TO DO THIS BETTER. THIS IS HERE FOR THE ONE 'ruby' CASE. return @load_path if @load_path if roll.load_path @load_path = roll.load_path.collect{ |path| File.join(location, path) } else @load_path = ['lib'] # ["lib/#{name}"] end end alias_method :load_path, :lib_path alias_method :load_paths, :lib_path # List of subdirectories that are searched for binaries. #++ # TODO I think it would be better just to make this a fix # convention that bin/ is always the place to put these. # (Unlike lib/ which needs to be more flexable.) #-- def bin_path return @bin_path if @bin_path return [] unless location # NEED TO DO THIS BETTER. if roll.bin_path @bin_path = roll.bin_path.collect{ |path| File.join(location, path) } else if File.directory?(File.join(location, 'bin')) @bin_path = [File.join(location, 'bin')] else @bin_path = [] end end return @bin_path end # Return the path to the data directory associated with the given # package name. Normally this is just # "#{Config::CONFIG['datadir']}/#{package_name}", but may be # modified by packages like RubyGems and Rolls to handle # versioned data directories. def datadir(versionless=false) if version and not versionless File.join(Config::CONFIG['datadir'], name, version) else File.join(Config::CONFIG['datadir'], name) end end # Return the path to the configuration directory. #-- # Can or should configuration directories be versioned? #++ def confdir File.join(location, 'data') #if version # File.join(Config::CONFIG['confdir'], name, version) #else # File.join(Config::CONFIG['datadir'], name) #end end # Require find. def require_find(file) file = roll.index_file if (file.nil? or file.empty?) glob = File.join('{' + load_path.join(',') + '}', file + "{.rb,#{Library.dlext},}") Dir.glob(glob).first end # Load find. def load_find(file) file = roll.index_file if (file.nil? or file.empty?) glob = File.join('{' + load_path.join(',') + '}', file) Dir.glob(glob).first end # Library specific #require. def require(file) if path = require_find(file) Kernel.require(path) else raise LoadError, "no such file to load -- #{name}:#{file}" end end # Library specific load. def load(file, wrap=nil) if path = load_find(file) Kernel.load(path, wrap) else raise LoadError, "no such file to load -- #{name}:#{file}" end end # Library specific autoload. def autoload(base, file) if path = require_find(file) Kernel.autoload(base, file) else raise LoadError, "no such file to autoload -- #{name}:#{file}" end end # Require into module. def module_require(mod, file) if path = require_find(file) mod.module_require(path) # FIXME else raise LoadError, "no such file to load -- #{name}:#{file}" end end # Load into module. def module_load(mod, file) if path = load_find(file) mod.module_load(path) # FIXME else raise LoadError, "no such file to load -- #{name}:#{file}" end end # Library specific autoload for module. def module_autoload(mod, base, file) if path = require_find(file) mod.autoload_without_roll(base, file) else raise LoadError, "no such file to autoload -- #{name}:#{file}" end end # Put the libs load paths into the global lookup. #-- # TODO Maybe call 'import' instead? #++ def utilize lib_path.each do |path| Library.load_path.unshift(path) end Library.load_path.uniq! self end # Class instance variable @ledger stores the library references. @ledger = {} class << self # Scan the site locations for libraries. def scan # First we add Ruby core and standard libraries. @ledger['ruby'] = Library.new(nil, :name=>'ruby', :version=>VERSION, :load_path=>Library.ruby_path) scan_working() if $DEBUG projs1 = Dir.glob( '{' + $LOAD_SITE.join(',') + '}/*{/meta,}/*.roll', File::FNM_CASEFOLD ) projs2 = Dir.glob( '{' + $LOAD_SITE.join(',') + '}/*/*{/meta,}/*.roll', File::FNM_CASEFOLD ) projs3 = Dir.glob( '{' + $LOAD_SITE.join(',') + '}/*/*/*{/meta,}/*.roll', File::FNM_CASEFOLD ) projs = projs1 + projs2 + projs3 #dirs = projs.collect do |prj| # metafile = File.basename(prj) # dir = File.dirname(prj) # dir = File.dirname(dir) if dir =~ /meta$/ # dir #end projs.uniq! #dirs -= $LOAD_SITE projs.each do |proj| name, ver = *File.basename(proj).chomp('.roll').split('-') dir = File.dirname(proj) dir = File.dirname(dir) if File.basename(dir) == 'meta' name = name.downcase #if versions.empty? # @ledger[name] ||= Library.new(dir, :name=>name, :version=>'0') #Version.new('0', dir) #else @ledger[name] ||= [] @ledger[name] << Library.new(dir, :name=>name, :version=>ver) #end end end # Scan current working location to see if there's # a library. This will ascend from the current # working directy to one level below root looking # for a lib/ directory. #-- # TODO CHANGE TO LOOK FRO INDEX FILE. #++ def scan_working paths = Dir.pwd.split('/') (paths.size-1).downto(1) do |n| dir = File.join( *(paths.slice(0..n) << 'lib') ) if File.directory? dir $LOAD_SITE.unshift dir end end end # Return a list of library names. def list @ledger.keys end # Libraries are Singleton pattern. def instance(name, constraint=nil) name = name.to_s #raise "no library -- #{name}" unless @ledger.include?( name ) return nil unless @ledger.include?(name) case lib = @ledger[name] when Library return lib unless constraint raise VersionConflict, "previously selected library version" # -- #{lib.version}" #when Version # @ledger[name] = new(lib.location, :name=>name, :version=>lib) #new(name, lib, lib.location) when Array if constraint compare = VersionNumber.constrant_lambda(constraint) version = lib.select(&compare).max else version = lib.max end unless version raise VersionError, "no library version -- #{name} #{constraint}" end @ledger[name] = version #new(version.location, :name=>name, :version=>version) #new(name, version, version.location) else raise "this should never happen" end @ledger[name].roll # Make sure the roll file is loaded. @ledger[name] end # A shortcut for #instance. alias_method :[], :instance # Same as #instance but will raise and error if the library is not found. def open(name, constraint=nil, &yld) lib = instance(name, constraint) unless lib raise LoadError, "no library -- #{name}" end yield(lib) if yld lib end end # VersionError is raised when a requested version cannot be found. class VersionError < ::RangeError # :nodoc: end # VersionConflict is raised when selecting another version # of a library when a previous version has already been selected. class VersionConflict < ::LoadError # :nodoc: end end module ::Config # Return the path to the data directory associated with the given # package name. Normally this is just # "#{Config::CONFIG['datadir']}/#{package_name}", but may be # modified by packages like RubyGems and Rolls to handle # versioned data directories. def self.datadir(name, versionless=false) if lib = Library.instance( name ) lib.datadir( versionless ) else File.join(CONFIG['datadir'], name) end end # Return the path to the configuration directory. def self.confdir(name) if lib = Library.instance( name ) lib.confdir else File.join(CONFIG['datadir'], name) end end end module ::Kernel # Activate a library. def library(name, constraint=nil) Library.open(name, constraint) end module_function :library # Use library. This activates a library, and adds # it's load_path to the global $LOAD_PATH. #-- # Maybe call #import instead ? #++ def use(name, constraint=nil) Library.open(name, constraint).utilize end def self.parse_load_parameters(file) if must = file.index(':') name, path = file.split(':') else name, *rest = file.split('/') path = File.join(*rest) end name = nil if name == '' lib = name ? Library.instance(name) : nil raise LoadError, "no library found -- #{file}" if must && !lib return lib, path end # Rolls requires a modification to #require and #load. # So that it is not neccessary to make the library() call # if you just want the latest version. # # This would be a bit simpler if we mandated the # use of the ':' notation when specifying the library # name. Use of the ':' is robust. But we cannot do this # w/o loosing backward-compatability. Using '/' in its # place has the potential for pathname clashing, albeit # the likelihood is small. There are two ways to bypass # the problem if it arises. Use 'ruby:{path}' if the # conflicting lib is a ruby core or standard library. # Use ':{path}' to bypass Roll system altogether. # # Require script. # def require(file) # if file.index(':') # name, file = file.split(':') # if name == '' # Kernel.require(file) # #if lib == 'ruby' # # Ruby.require(file) # elsif lib = Library.instance(name) # lib.require(file) # else # raise LoadError, "no library found -- #{name}" # end # else # name, *rest = file.split('/') # if lib = Library.instance(name) # lib.require(File.join(*rest)) # else # Kernel.require(file) # end # end # end # Require script. def require(file) lib, path = *Kernel.parse_load_parameters(file) if lib lib.require(path) else Kernel.require(file) end end # # Load script. # def load(file, wrap=nil) # if file.index(':') # name, file = file.split(':') # if name == '' # Kernel.require(file) # #if lib == 'ruby' # # Ruby.load(file, safe) # elsif lib = Library.instance(name) # lib.load(file, wrap) # else # raise LoadError, "no library found -- #{name}" # end # else # name, *rest = file.split('/') # if lib = Library.instance(name) # lib.load(File.join(*rest)) # else # Kernel.load(file, wrap) # end # end # end # Require script. def load(file, wrap=false) lib, path = *Kernel.parse_load_parameters(file) if lib lib.load(path, wrap) else Kernel.load(file, wrap) end end # # # def autoload(base, file) # if file.index(':') # name, file = file.split(':') # if name == '' # Kernel.autoload(base, file) # elsif lib = Library.instance(name) # lib.autoload(base, file) # else # raise LoadError, "no library found -- #{name}" # end # else # name, *rest = file.split('/') # if lib = Library.instance(name) # lib.autoload(base, File.join(*rest)) # else # Kernel.autoload(base, file) # end # end # end =begin # Autoload script. def autoload(base, file) lib, path = *Kernel.parse_load_parameters(file) if lib lib.autoload(base, path) else Kernel.autoload(base, file) end end =end end # TODO class ::Module =begin alias_method :autoload_without_roll, :autoload # def autoload(base, file) lib, path = *Kernel.parse_load_parameters(file) if lib lib.module_autoload(self, base, path) else autoload_without_roll(base, file) end end =end # TODO # Adds importing to Module class called against +self+ as default. # def module_require() # end # def module_load() # end end # Prime the library ledger. Library.scan