├── lib ├── library.yml ├── ubylibs.rb ├── library │ ├── core_ext.rb │ ├── errors.rb │ ├── errors │ │ ├── validation_error.rb │ │ └── load_error.rb │ ├── core_ext │ │ ├── file.rb │ │ ├── hash.rb │ │ ├── gem.rb │ │ └── rbconfig.rb │ ├── version.rb │ ├── autoload.rb │ ├── index.rb │ ├── legacy_feature.rb │ ├── utils.rb │ ├── kernel.rb │ ├── rubylib.rb │ ├── feature.rb │ ├── metadata.rb │ ├── ledgered.rb │ ├── library.rb │ └── ledger.rb └── library.rb ├── script ├── install-current.sh ├── install-0.2.0.sh └── install-master.sh ├── test ├── helper.rb ├── fixture │ ├── lib │ │ └── foo.rb │ └── .index ├── case_library.rb ├── case_ledger.rb └── case_version.rb ├── demo ├── fixtures │ ├── foo │ │ ├── lib │ │ │ └── foo.rb │ │ └── .ruby │ └── tryme │ │ ├── 1.0 │ │ ├── lib │ │ │ └── tryme.rb │ │ └── .ruby │ │ └── 1.1 │ │ ├── lib │ │ └── tryme.rb │ │ └── .ruby ├── applique │ ├── check.rb │ └── project.rb ├── 01_loading.rdoc ├── helpers │ ├── new_project.rdoc │ └── projects.rdoc ├── 03_ledger.rdoc ├── 02_library.rdoc └── 04_version.rdoc ├── .yardopts ├── .gitignore ├── .travis.yml ├── .ruby ├── .test ├── Assembly ├── Gemfile ├── work ├── benchmarks │ ├── bm_split.rb │ └── bm_stack.rb ├── consider │ └── loader.rb ├── snippets │ └── loadpath_search.rb └── deprecated │ └── requirements.rb ├── Index.yml ├── MANIFEST ├── LICENSE.txt ├── .index ├── HISTORY.rdoc ├── README.md ├── .gemspec └── setup.rb /lib/library.yml: -------------------------------------------------------------------------------- 1 | ../.index -------------------------------------------------------------------------------- /lib/ubylibs.rb: -------------------------------------------------------------------------------- 1 | require 'library' 2 | -------------------------------------------------------------------------------- /script/install-current.sh: -------------------------------------------------------------------------------- 1 | install-0.2.0.sh -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'lemon' 2 | require 'ae' 3 | -------------------------------------------------------------------------------- /demo/fixtures/foo/lib/foo.rb: -------------------------------------------------------------------------------- 1 | $foo_message = "Foo 0.8.2" 2 | -------------------------------------------------------------------------------- /demo/fixtures/tryme/1.0/lib/tryme.rb: -------------------------------------------------------------------------------- 1 | $tryme_message = "Try Me v1.0" -------------------------------------------------------------------------------- /demo/fixtures/tryme/1.1/lib/tryme.rb: -------------------------------------------------------------------------------- 1 | $tryme_message = "Try Me v1.1" -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title Library 2 | --protected 3 | --private 4 | lib/ 5 | - 6 | [A-Z]*.* 7 | -------------------------------------------------------------------------------- /test/fixture/lib/foo.rb: -------------------------------------------------------------------------------- 1 | class Foo 2 | 3 | def bar 4 | "baz!" 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /demo/fixtures/foo/.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | name: foo 3 | version: 0.8.2 4 | date: 2011-12-12 5 | title: Foo 6 | -------------------------------------------------------------------------------- /lib/library/core_ext.rb: -------------------------------------------------------------------------------- 1 | require_relative 'core_ext/file' 2 | require_relative 'core_ext/hash' 3 | 4 | -------------------------------------------------------------------------------- /lib/library/errors.rb: -------------------------------------------------------------------------------- 1 | require_relative 'errors/load_error' 2 | require_relative 'errors/validation_error' 3 | -------------------------------------------------------------------------------- /demo/fixtures/tryme/1.0/.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | name: tryme 3 | version: 1.0 4 | title: TryMe 5 | resources: 6 | home: "http://tryme.foo" 7 | -------------------------------------------------------------------------------- /demo/fixtures/tryme/1.1/.ruby: -------------------------------------------------------------------------------- 1 | --- 2 | name: tryme 3 | version: 1.1 4 | title: TryMe 5 | resources: 6 | home: "http://tryme.foo" 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .reap/digest 2 | .yardoc 3 | log 4 | doc 5 | pkg 6 | tmp 7 | web 8 | work/sandbox 9 | work/trash 10 | setup.receipt 11 | setup.config 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | script: "bundle exec rubytest -v" 3 | rvm: 4 | - 1.8.7 5 | - 1.9.2 6 | - 1.9.3 7 | - rbx-2.0 8 | - jruby 9 | - ree 10 | 11 | -------------------------------------------------------------------------------- /.ruby: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | profile :development do 4 | 5 | config 'qed' do 6 | $LEDGER.isolate_project(File.dirname(__FILE__)) 7 | end 8 | 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/library/errors/validation_error.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # Library ValidationError is raised when library metadata is not conforming. 4 | # 5 | class ValidationError < ::RuntimeError 6 | end 7 | 8 | end 9 | -------------------------------------------------------------------------------- /test/case_library.rb: -------------------------------------------------------------------------------- 1 | covers 'library' 2 | 3 | testcase Library do 4 | 5 | class_method :new do 6 | 7 | test do 8 | Library.new(File.dirname(__FILE__) + '/fixture') 9 | end 10 | 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /lib/library/core_ext/file.rb: -------------------------------------------------------------------------------- 1 | class File 2 | 3 | # 4 | RE_PATH_SEPERATOR = Regexp.new('[' + Regexp.escape(File::Separator) + %q{\\\/} + ']') 5 | 6 | # 7 | def self.split_root(path) 8 | path.split(RE_PATH_SEPERATOR, 2) 9 | end 10 | 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/library/version.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | #require 'versus' 4 | 5 | # The Library::Version class is essentially a tuple (immutable array) 6 | # with special comparision operators. 7 | # 8 | class Version < Version::Number 9 | end 10 | 11 | end 12 | -------------------------------------------------------------------------------- /.test: -------------------------------------------------------------------------------- 1 | Test.run(:default) do |run| 2 | require './test/helper' 3 | run.files << 'test/case_*.rb' 4 | end 5 | 6 | Test.run(:cov) do |run| 7 | require './test/helper' 8 | run.files << 'test/case_*.rb' 9 | SimpleCov.start do |cov| 10 | cov.coverage_dir = 'log/coverage' 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /Assembly: -------------------------------------------------------------------------------- 1 | --- 2 | gem: 3 | active: true 4 | 5 | github: 6 | gh_pages: web 7 | 8 | yard: 9 | active: true 10 | 11 | dnote: 12 | title: Source Notes 13 | output: log/notes.html 14 | 15 | announce: 16 | service: email 17 | mailto: 18 | - ruby-talk@ruby-lang.org 19 | - rubyworks-mailinglist@googlegroups.com 20 | 21 | 22 | -------------------------------------------------------------------------------- /demo/applique/check.rb: -------------------------------------------------------------------------------- 1 | if ENV['RUBYOPT'].index('-rubylibs') || ENV['RUBYOPT'].index('-roll') 2 | abort "Remove -rubylibs or -roll from RUBYOPT before running these tests." 3 | end 4 | 5 | # Make sure we use local version of files. 6 | #$:.unshift('lib') 7 | 8 | def fixtures 9 | @_fixtures ||= File.dirname(__FILE__) + '/../fixtures' 10 | end 11 | 12 | -------------------------------------------------------------------------------- /demo/01_loading.rdoc: -------------------------------------------------------------------------------- 1 | = Load and Require 2 | 3 | The #library method can be used to constrain a library to a particular 4 | version. 5 | 6 | library('tryme', '1.1') 7 | 8 | If we try to constrain a library to an incompatible version a VersionConflit 9 | will be raised. 10 | 11 | expect Library::VersionConflict do 12 | library('tryme', '1.0') 13 | end 14 | 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: utf-8 3 | 4 | source "http://rubygems.org" 5 | #gemspec 6 | 7 | gem "autoload" 8 | gem "versus" 9 | 10 | group :development do 11 | 12 | group :build do 13 | gem "detroit" 14 | gem "setup", ">= 5.0" 15 | end 16 | 17 | group :test do 18 | gem "rubytest" 19 | gem "lemon" 20 | gem "ae" 21 | end 22 | 23 | end 24 | 25 | -------------------------------------------------------------------------------- /lib/library/core_ext/hash.rb: -------------------------------------------------------------------------------- 1 | class Hash 2 | 3 | # 4 | # Transform keys of hash returning a new hash. 5 | # 6 | def rekey #:yield: 7 | if block_given? 8 | inject({}){|h,(k,v)| h[yield(k)]=v; h} 9 | else 10 | inject({}){|h,(k,v)| h[k.to_sym]=v; h} 11 | end 12 | end 13 | 14 | # 15 | # In-place rekey. 16 | # 17 | def rekey! #:yield: 18 | replace(rekey{|k| yield(k) }) 19 | end 20 | 21 | end 22 | 23 | -------------------------------------------------------------------------------- /lib/library.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | require 'autoload' 4 | require 'versus' 5 | 6 | require 'library/utils' 7 | require 'library/library' 8 | require 'library/rubylib' 9 | require 'library/ledger' 10 | require 'library/ledgered' 11 | 12 | $LEDGER = Library::Ledger.new 13 | $LOAD_STACK = [] 14 | $LOAD_CACHE = {} 15 | 16 | class Library 17 | extend Ledgered 18 | # Should this be here? Or just in `ubylibs.rb`? 19 | bootstrap! 20 | end 21 | 22 | -------------------------------------------------------------------------------- /lib/library/autoload.rb: -------------------------------------------------------------------------------- 1 | # Ruby's autoload method does not use the normal require 2 | # mechanisms, therefore if we wish to support it we will 3 | # have to override and create our own system. OTOH Matz 4 | # has said autoload will go away in Ruby 2.0, so maybe 5 | # we can just let this go and not worry about it. 6 | 7 | module Kernel 8 | #alias __autoload__ autoload 9 | end 10 | 11 | #class ::Module 12 | # alias __autoload__ autoload 13 | #end 14 | 15 | -------------------------------------------------------------------------------- /work/benchmarks/bm_split.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | 3 | n = 50000 4 | s = "split:this;that" 5 | 6 | Benchmark.bmbm do |x| 7 | x.report("control") { 8 | n.times { 9 | s.split(':') 10 | s.split(';') 11 | } 12 | } 13 | 14 | x.report("loop") { 15 | n.times { 16 | s.split(':').map{ |e| e.split(';') }.flatten 17 | } 18 | } 19 | 20 | x.report("regexp") { 21 | n.times { 22 | s.split(/[:;]/) 23 | } 24 | } 25 | end 26 | -------------------------------------------------------------------------------- /work/benchmarks/bm_stack.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | 3 | n = 50000 4 | 5 | s0 = [] 6 | 7 | s1 = [] 8 | s2 = [] 9 | 10 | class S 11 | def initialize(t1, t2) 12 | @t1 = t1 13 | @t2 = t2 14 | end 15 | end 16 | 17 | Benchmark.bmbm do |x| 18 | x.report("two stacks") { 19 | n.times { 20 | s1 << "thing1" 21 | s2 << "thing2" 22 | } 23 | } 24 | 25 | x.report("one stack") { 26 | n.times { 27 | s0 << S.new("thing1", "thing2") 28 | } 29 | } 30 | end 31 | -------------------------------------------------------------------------------- /lib/library/index.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | protected 4 | 5 | # 6 | # 7 | # 8 | def self.const_missing(name) 9 | index[name.to_s.downcase] || super(name) 10 | end 11 | 12 | # 13 | # 14 | # 15 | def self.index 16 | @_index ||= ( 17 | file = File.expand_path('../library.yml', __dir__) 18 | YAML.load_file(file) 19 | ) 20 | end 21 | 22 | # 23 | # TODO: __dir__ can be removed as of Ruby 2.0. 24 | # 25 | def self.__dir__ 26 | File.dirname(__FILE__) 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/library/core_ext/gem.rb: -------------------------------------------------------------------------------- 1 | if not defined?(Gem) 2 | 3 | module Gem 4 | # Some libraries, such as RDoc search through 5 | # all libraries for plugins using this method. 6 | # If RubyGems is not being used, then Rolls 7 | # emulates it. 8 | # 9 | # Gem.find_files('rdoc/discover') 10 | # 11 | # TODO: Perhaps it should override if it exists 12 | # and call back to it on failuer? 13 | def self.find_files(path) 14 | ::Library.search(path).map{ |f| f.to_s } 15 | end 16 | end 17 | 18 | end 19 | 20 | -------------------------------------------------------------------------------- /test/case_ledger.rb: -------------------------------------------------------------------------------- 1 | covers 'library/ledger' 2 | 3 | testcase Library::Ledger do 4 | 5 | method :add do 6 | test do 7 | ledger = Library::Ledger.new 8 | ledger.add(File.dirname(__FILE__) + '/fixture') 9 | ledger.assert.size == 1 10 | end 11 | end 12 | 13 | method :[] do 14 | test do 15 | ledger = Library::Ledger.new 16 | ledger.add(File.dirname(__FILE__) + '/fixture') 17 | 18 | ledger[:foo].assert.is_a? Array 19 | ledger['foo'].assert.is_a? Array 20 | end 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /demo/helpers/new_project.rdoc: -------------------------------------------------------------------------------- 1 | = Additional Project 2 | 3 | == Neo 0.0.1 4 | 5 | Given a project directory "neo/0.0.1". 6 | 7 | With a file "VERSION" containing... 8 | 9 | name: neo 10 | vers: 0.0.1 11 | 12 | With a file "PROFILE" containing... 13 | 14 | title : Neo 15 | author : Thomas Sawyer 16 | homepage: http://neo.foo 17 | 18 | With a file "lib/neo.rb" containing... 19 | 20 | $neo_message = "Neo v0.0.1" 21 | 22 | With a file ".ruby/name" containing... 23 | 24 | neo 25 | 26 | With a file ".ruby/version" containing... 27 | 28 | 0.0.1 29 | 30 | -------------------------------------------------------------------------------- /script/install-0.2.0.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | local dir=$(pwd) 4 | mkdir /tmp/ruby-library-install 5 | cd /tmp/ruby-library-install 6 | 7 | wget https://github.com/rubyworks/autoload/archive/0.2.0.zip 8 | unzip 0.2.0.zip 9 | 10 | wget https://github.com/rubyworks/versus/archive/0.2.0.zip 11 | unzip 0.2.0.zip 12 | 13 | wget https://github.com/rubyworks/library/archive/0.2.0.zip 14 | unzip 0.2.0.zip 15 | 16 | cd autoload-0.2.0 17 | ruby setup.rb 18 | cd .. 19 | 20 | cd versus-0.2.0 21 | ruby setup.rb 22 | cd .. 23 | 24 | cd library-0.2.0 25 | ruby setup.rb 26 | cd .. 27 | 28 | cd $dir 29 | rm -r /tmp/ruby-library-install 30 | 31 | -------------------------------------------------------------------------------- /work/consider/loader.rb: -------------------------------------------------------------------------------- 1 | # TODO: Should we use loabable, instead of overriding Kernel methods directly? 2 | 3 | require 'loadable' 4 | 5 | module Roll 6 | 7 | class Loader 8 | include Loadable 9 | 10 | # 11 | def call(fname, options={}) 12 | file = Library.find(path, options) 13 | p file 14 | end 15 | 16 | # 17 | def each(options={}, &block) 18 | ledger.each do |name, lib| 19 | lib = lib.sort.first if Array===lib 20 | lib.loadpath.each do |path| 21 | #path = File.join(lib.location, path) 22 | traverse(path, &block) 23 | end 24 | end 25 | end 26 | 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /script/install-master.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | local dir=$(pwd) 4 | mkdir /tmp/ruby-library-install 5 | cd /tmp/ruby-library-install 6 | 7 | wget https://github.com/rubyworks/autoload/archive/master.zip 8 | unzip master.zip 9 | rm master.zip 10 | 11 | wget https://github.com/rubyworks/versus/archive/master.zip 12 | unzip master.zip 13 | rm master.zip 14 | 15 | wget https://github.com/rubyworks/library/archive/master.zip 16 | unzip master.zip 17 | rm master.zip 18 | 19 | cd autoload-master 20 | ruby setup.rb 21 | cd .. 22 | 23 | cd versus-master 24 | ruby setup.rb 25 | cd .. 26 | 27 | cd library-master 28 | ruby setup.rb 29 | cd .. 30 | 31 | cd $dir 32 | rm -r /tmp/ruby-library-install 33 | 34 | -------------------------------------------------------------------------------- /work/snippets/loadpath_search.rb: -------------------------------------------------------------------------------- 1 | 2 | ## last ditch attempt, search all $LOAD_PATH 3 | if suffix 4 | SUFFIXES.each do |ext| 5 | $LOAD_PATH.each do |location| 6 | file = ::File.join(location, path + ext) 7 | if ::File.file?(file) 8 | return Library::Script.new(location, '.', path, ext) 9 | matches << file 10 | end 11 | end 12 | end 13 | else 14 | $LOAD_PATH.each do |location| 15 | file = ::File.join(location, file) 16 | if ::File.file?(file) 17 | return Library::Script.new(location, '.', path, ext) unless select 18 | matches << file 19 | end 20 | end 21 | end 22 | 23 | 24 | -------------------------------------------------------------------------------- /Index.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2.0.0" 3 | 4 | name: 5 | library 6 | 7 | title: 8 | Library 9 | 10 | summary: 11 | Library Management System 12 | 13 | description: 14 | Library is library management system for Ruby. Library objectifies 15 | the concept of a Ruby library and encapsualtes its essential nature 16 | as a location on disk from which scripts can be required. 17 | 18 | authors: 19 | - Trans 20 | 21 | resources: 22 | home: http://rubyworks.github.com/library 23 | code: http://github.com/rubyworks/library 24 | mail: http://groups.google.com/groups/rubyworks-mailinglist 25 | 26 | repositories: 27 | upstream: git://github.com/rubyworks/library.git 28 | 29 | organizations: 30 | - Rubyworks (http://rubyworks.github.com/) 31 | 32 | copyrights: 33 | - 2006 Rubyworks (BSD-2-Clause) 34 | 35 | created: "2006-12-10" 36 | 37 | -------------------------------------------------------------------------------- /demo/applique/project.rb: -------------------------------------------------------------------------------- 1 | require 'library' 2 | require 'library/kernel' 3 | 4 | require 'pathname' 5 | 6 | project_directory = Pathname.new('projects') 7 | 8 | When 'Given a project directory "(((\S+)))"' do |name| 9 | @project_directory = project_directory + name 10 | libdir = @project_directory + 'lib' 11 | FileUtils.mkdir_p(libdir) unless libdir.exist? 12 | end 13 | 14 | When 'With a file "(((\S+)))" containing' do |name, text| 15 | file = @project_directory + name 16 | FileUtils.mkdir_p(file.parent) unless file.parent.exist? 17 | File.open(file.to_s, 'w'){ |f| f << text } 18 | end 19 | 20 | Before :demo do 21 | prime_ledger 22 | #p $LEDGER 23 | end 24 | 25 | # Support method to setup fresh Ledger. 26 | def prime_ledger 27 | projects = File.dirname(__FILE__) + '/../fixtures/*' 28 | 29 | # reset ledger (shouldn't this happen in Library.prime call?) 30 | $LEDGER = ::Library::Ledger.new 31 | 32 | ::Library.prime(*projects, :expound=>true) 33 | end 34 | 35 | -------------------------------------------------------------------------------- /demo/helpers/projects.rdoc: -------------------------------------------------------------------------------- 1 | = Basic Set of Projects 2 | 3 | == TryMe 1.0 4 | 5 | Given a project directory "tryme/1.0". 6 | 7 | With a file "VERSION" containing... 8 | 9 | name: tryme 10 | vers: 1.0 11 | 12 | With a file "PROFILE" containing... 13 | 14 | title : TryMe 15 | author : Thomas Sawyer 16 | homepage: http://tryme.foo 17 | 18 | With a file "lib/tryme.rb" containing... 19 | 20 | $tryme_message = "Try Me v1.0" 21 | 22 | With a file ".ruby/name" containing... 23 | 24 | tryme 25 | 26 | With a file ".ruby/version" containing... 27 | 28 | 1.0 29 | 30 | == TryMe 1.1 31 | 32 | Given a project directory "tryme/1.1". 33 | 34 | With a file "VERSION" containing... 35 | 36 | name: tryme 37 | vers: 1.1 38 | 39 | With a file "PROFILE" containing... 40 | 41 | title : TryMe 42 | author : Thomas Sawyer 43 | homepage: http://tryme.foo 44 | 45 | With a file "lib/tryme.rb" containing... 46 | 47 | $tryme_message = "Try Me v1.1" 48 | 49 | With a file ".ruby/name" containing... 50 | 51 | tryme 52 | 53 | With a file ".ruby/version" containing... 54 | 55 | 1.1 56 | 57 | -------------------------------------------------------------------------------- /demo/03_ledger.rdoc: -------------------------------------------------------------------------------- 1 | = The Ledger 2 | 3 | At the heart of Library lies a Ledger object. It is essentially a Hash of 4 | library names indexing Library objects stored in the global $LEDGER variable. 5 | 6 | $LEDGER.keys.sort.assert == ['foo', 'ruby', 'tryme'] 7 | 8 | The Library class redirects a number of calls to the $LEDGER, so we 9 | could invoke the same methods on it instead. 10 | 11 | Library.names.assert == $LEDGER.keys 12 | 13 | Which we choose to use is really a matter of taste. The Roll::Library 14 | methods were designed for readability, whereas $LEDGER is used internally. 15 | For the rest of this demo we will use $LEDGER since this demonstration 16 | is specifically about it. 17 | 18 | The values of $LEDGER will always be either a Library object or an 19 | array of Library objects, of the same name but differnt versions. 20 | 21 | When a particular library is activated for the first time the corresponding 22 | array value will be replaced by the library. 23 | 24 | $LEDGER['tryme'].assert.is_a?(Array) 25 | 26 | library('tryme') 27 | 28 | $LEDGER['tryme'].assert.is_a?(Library) 29 | 30 | 31 | -------------------------------------------------------------------------------- /lib/library/legacy_feature.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # 4 | # 5 | # TODO: I bet we can get rid of LegacyFeature if we modify Feature to handle `nil` library better. 6 | # 7 | class LegacyFeature < Feature 8 | 9 | # 10 | def initialize(pathname) 11 | @library = nil 12 | @fullname = pathname 13 | @loadpath = File.dirname(pathname) # not that this makes any real sense, but... 14 | @filename = File.basename(pathname) 15 | @extension = nil 16 | 17 | @required = {} 18 | end 19 | 20 | =begin 21 | # 22 | def load(options={}) 23 | Library.load_stack << self 24 | begin 25 | @required = true if options[:require] 26 | require_without_library(fullname) 27 | ensure 28 | Library.load_stack.pop 29 | end 30 | end 31 | 32 | # 33 | def require(options={}) 34 | Library.load_stack << self 35 | begin 36 | load_without_library(fullname, options[:wrap]) 37 | ensure 38 | Library.load_stack.pop 39 | end 40 | end 41 | 42 | # 43 | def required? 44 | @required 45 | end 46 | =end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/library/errors/load_error.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # Library::LoadError is a subclass of Ruby's standard LoadError class, 4 | # modified slightly to provide better error messages. 5 | # 6 | class LoadError < ::LoadError 7 | 8 | # 9 | # Setup new LoadError instance. 10 | # 11 | def initialize(failed_path, library_name=nil) 12 | super() 13 | 14 | @failed_path = failed_path 15 | @library_name = library_name 16 | 17 | if library_name 18 | @message = "#{@library_name}:#{@failed_path}" 19 | else 20 | @message = failed_path 21 | end 22 | 23 | clean_backtrace 24 | end 25 | 26 | # 27 | # Error message string. 28 | # 29 | def to_s 30 | "LoadError: cannot load such file -- #{@message}" 31 | end 32 | 33 | # 34 | # Take an +error+ and remove any mention of 'library' from it's backtrace. 35 | # Will leaving the backtrace untouched if $DEBUG is set to true. 36 | # 37 | def clean_backtrace 38 | return if ENV['debug'] || $DEBUG 39 | bt = backtrace 40 | bt = bt.reject{ |e| $RUBY_IGNORE_CALLERS.any?{ |re| re =~ e } } if bt 41 | set_backtrace(bt) 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | #!mast .ruby .yardopts bin demo lib man qed spec test [A-Z]*.* 2 | .ruby 3 | .yardopts 4 | demo/01_loading.rdoc 5 | demo/02_library.rdoc 6 | demo/03_ledger.rdoc 7 | demo/04_version.rdoc 8 | demo/applique/check.rb 9 | demo/applique/project.rb 10 | demo/fixtures/foo/.ruby 11 | demo/fixtures/foo/lib/foo.rb 12 | demo/fixtures/tryme/1.0/.ruby 13 | demo/fixtures/tryme/1.0/lib/tryme.rb 14 | demo/fixtures/tryme/1.1/.ruby 15 | demo/fixtures/tryme/1.1/lib/tryme.rb 16 | demo/helpers/new_project.rdoc 17 | demo/helpers/projects.rdoc 18 | lib/olls.rb 19 | lib/rolls/autoload.rb 20 | lib/rolls/core_ext.rb 21 | lib/rolls/index.rb 22 | lib/rolls/kernel.rb 23 | lib/rolls/ledger.rb 24 | lib/rolls/library/feature.rb 25 | lib/rolls/library/ledgered.rb 26 | lib/rolls/library/load_error.rb 27 | lib/rolls/library/metadata.rb 28 | lib/rolls/library/validation_error.rb 29 | lib/rolls/library.rb 30 | lib/rolls/rbconfig.rb 31 | lib/rolls/rolls.rb 32 | lib/rolls/rubygems.rb 33 | lib/rolls/rubylib.rb 34 | lib/rolls/version.rb 35 | lib/rolls.rb 36 | lib/rolls.yml 37 | test/case_ledger.rb 38 | test/case_library.rb 39 | test/case_version.rb 40 | test/fixture/.index 41 | test/fixture/lib/foo.rb 42 | test/helper.rb 43 | HISTORY.rdoc 44 | LICENSE.txt 45 | Index.yml 46 | README.md 47 | -------------------------------------------------------------------------------- /test/fixture/.index: -------------------------------------------------------------------------------- 1 | --- 2 | source: 3 | - var 4 | authors: 5 | - name: trans 6 | email: transfire@gmail.com 7 | copyrights: 8 | - holder: Rubyworks 9 | year: '2006' 10 | license: BSD-2-Clause 11 | replacements: [] 12 | alternatives: [] 13 | requirements: 14 | - name: detroit 15 | groups: 16 | - build 17 | development: true 18 | - name: setup 19 | version: 5.0+ 20 | groups: 21 | - build 22 | development: true 23 | - name: rubytest 24 | groups: 25 | - test 26 | development: true 27 | - name: lemon 28 | groups: 29 | - test 30 | development: true 31 | - name: ae 32 | groups: 33 | - test 34 | development: true 35 | dependencies: [] 36 | conflicts: [] 37 | repositories: 38 | - uri: git://github.com/rubyworks/library.git 39 | scm: git 40 | name: upstream 41 | resources: 42 | home: http://rubyworks.github.com/library 43 | code: http://github.com/rubyworks/library 44 | mail: http://groups.google.com/groups/rubyworks-mailinglist 45 | extra: {} 46 | load_path: 47 | - lib 48 | revision: 0 49 | created: '2006-12-10' 50 | summary: Testing Library. 51 | title: Foo 52 | version: 0.1.0 53 | name: foo 54 | description: ! "The Foo class is a test class for Library gem." 55 | organization: rubyworks 56 | date: '2012-01-03' 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD-2-Clause License 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are 4 | permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of 7 | conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 10 | of conditions and the following disclaimer in the documentation and/or other materials 11 | provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 14 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 15 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 16 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 19 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 20 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 21 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | -------------------------------------------------------------------------------- /lib/library/core_ext/rbconfig.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | module ::RbConfig 4 | 5 | # 6 | # Return the path to the data directory associated with the given 7 | # library name. 8 | # 9 | # Normally this is just: 10 | # 11 | # "#{Config::CONFIG['datadir']}/#{name}" 12 | # 13 | # But it may be modified by packages like RubyGems and Rolls to handle 14 | # versioned data directories. 15 | # 16 | def self.datadir(name, versionless=false) 17 | if lib = Library.instance(name) 18 | lib.datadir(versionless) 19 | elsif defined?(super) 20 | super(name) 21 | else 22 | File.join(CONFIG['datadir'], name) 23 | end 24 | end 25 | 26 | # 27 | # Return the path to the configuration directory. 28 | # 29 | def self.confdir(name) 30 | if lib = Library.instance(name) 31 | lib.confdir 32 | else 33 | File.join(CONFIG['confdir'], name) 34 | end 35 | end 36 | 37 | # 38 | # Patterns used to identiy a Windows platform. 39 | # 40 | WIN_PATTERNS = [ 41 | /bccwin/i, 42 | /cygwin/i, 43 | /djgpp/i, 44 | /mingw/i, 45 | /mswin/i, 46 | /wince/i, 47 | ] 48 | 49 | #WINDOWS_PLATFORM = !!WIN_PATTERNS.find{ |r| RUBY_PLATFORM =~ r } 50 | 51 | # 52 | # Is this a windows platform? This method compares the entires 53 | # in +WIN_PATTERNS+ against +RUBY_PLATFORM+. 54 | # 55 | def self.windows_platform? 56 | case RUBY_PLATFORM 57 | when *WIN_PATTERNS 58 | true 59 | else 60 | false 61 | end 62 | end 63 | 64 | end 65 | 66 | -------------------------------------------------------------------------------- /.index: -------------------------------------------------------------------------------- 1 | --- 2 | type: ruby 3 | revision: 2013 4 | sources: 5 | - Index.yml 6 | - Gemfile 7 | authors: 8 | - name: Trans 9 | email: transfire@gmail.com 10 | organizations: 11 | - name: Rubyworks (http://rubyworks.github.com/) 12 | requirements: 13 | - groups: 14 | - default 15 | version: ! '>= 0' 16 | name: autoload 17 | - groups: 18 | - default 19 | version: ! '>= 0' 20 | name: versus 21 | - groups: 22 | - development 23 | - build 24 | version: ! '>= 0' 25 | name: detroit 26 | - groups: 27 | - development 28 | - build 29 | version: ! '>= 5.0' 30 | name: setup 31 | - groups: 32 | - development 33 | - test 34 | version: ! '>= 0' 35 | name: rubytest 36 | - groups: 37 | - development 38 | - test 39 | version: ! '>= 0' 40 | name: lemon 41 | - groups: 42 | - development 43 | - test 44 | version: ! '>= 0' 45 | name: ae 46 | conflicts: [] 47 | alternatives: [] 48 | resources: 49 | - type: home 50 | uri: http://rubyworks.github.com/library 51 | label: Homepage 52 | - type: code 53 | uri: http://github.com/rubyworks/library 54 | label: Source Code 55 | - type: mail 56 | uri: http://groups.google.com/groups/rubyworks-mailinglist 57 | label: Mailing List 58 | repositories: 59 | - name: upstream 60 | scm: git 61 | uri: git://github.com/rubyworks/library.git 62 | categories: [] 63 | paths: 64 | load: 65 | - lib 66 | copyrights: 67 | - holder: Rubyworks 68 | year: '2006' 69 | license: BSD-2-Clause 70 | version: 2.0.0 71 | name: library 72 | title: Library 73 | summary: Library Management System 74 | description: Library is library management system for Ruby. Library objectifies the 75 | concept of a Ruby library and encapsualtes its essential nature as a location on 76 | disk from which scripts can be required. 77 | created: '2006-12-10' 78 | date: '2013-01-05' 79 | -------------------------------------------------------------------------------- /demo/02_library.rdoc: -------------------------------------------------------------------------------- 1 | = Library Instances 2 | 3 | The Library class can be initialized given the location of the project. 4 | 5 | tryme10 = Library.new(fixtures + '/tryme/1.0') 6 | tryme11 = Library.new(fixtures + '/tryme/1.1') 7 | 8 | With a library instance in hand we can query it for information about itself. 9 | 10 | tryme10.name.assert == "tryme" 11 | tryme11.name.assert == "tryme" 12 | 13 | tryme10.version.to_s.assert == "1.0" 14 | tryme11.version.to_s.assert == "1.1" 15 | 16 | Secondary information, taken from a project's .ruby file, can be queried via 17 | the #metadata method. 18 | 19 | tryme10.metadata['resources']['home'].assert == "http://tryme.foo" 20 | tryme11.metadata['resources']['home'].assert == "http://tryme.foo" 21 | 22 | Of course, the most important function of a library is to load and require 23 | a script. With an Library instance in hand this can be achieved directly. 24 | 25 | tryme10.load('tryme.rb') 26 | $tryme_message.assert == "Try Me v1.0" 27 | 28 | But if we try to load from another version, we will get a VersionConflict 29 | error. 30 | 31 | expect Library::VersionConflict do 32 | tryme11.load('tryme.rb') 33 | end 34 | 35 | However, we can bypass this constraint (if we know what you are doing!) with 36 | the :force option. 37 | 38 | tryme11.load('tryme.rb', :force=>true) 39 | $tryme_message.assert == "Try Me v1.1" 40 | 41 | Notice that when requiring files directly via a Library instance, if a file 42 | is required from two different versions of a library, a VersionConflict 43 | error will be rasied. 44 | 45 | expect Library::VersionConflict do 46 | tryme11.require('tryme') 47 | end 48 | 49 | But there is no error if we use the active version. 50 | 51 | tryme10.require('tryme') 52 | 53 | TODO: In the future, I think we will put in a version check, and a :force 54 | option to override. 55 | 56 | -------------------------------------------------------------------------------- /test/case_version.rb: -------------------------------------------------------------------------------- 1 | covers 'library/version' 2 | 3 | testcase Library::Version do 4 | 5 | concern "Ensure functionality of Roll's Version class." 6 | 7 | method :to_s do 8 | test do 9 | v = Library::Version.new('1.2.3') 10 | v.to_s.assert == '1.2.3' 11 | end 12 | end 13 | 14 | method :to_str do 15 | test do 16 | v = Library::Version.new('1.2.3') 17 | v.to_str.assert == '1.2.3' 18 | end 19 | end 20 | 21 | method :inspect do 22 | test do 23 | v = Library::Version.new('1.2.3') 24 | v.inspect.assert == '1.2.3' 25 | end 26 | end 27 | 28 | method :[] do 29 | test do 30 | v = Library::Version.new('1.2.3') 31 | v[0].assert == 1 32 | v[1].assert == 2 33 | v[2].assert == 3 34 | end 35 | end 36 | 37 | method :<=> do 38 | test do 39 | v1 = Library::Version.new('1.2.3') 40 | v2 = Library::Version.new('1.2.4') 41 | (v2 <=> v1).assert == 1 42 | end 43 | end 44 | 45 | # TODO 46 | # def =~( other ) 47 | # #other = other.to_t 48 | # upver = other.dup 49 | # upver[0] += 1 50 | # @self >= other and @self < upver 51 | # end 52 | 53 | method :=~ do 54 | test "pessimistic constraint" do 55 | v1 = Library::Version.new('1.2.4') 56 | v2 = Library::Version.new('1.2') 57 | assert(v1 =~ v2) 58 | end 59 | end 60 | 61 | method :major do 62 | test do 63 | v = Library::Version.new('1.2.3') 64 | v.major.assert == 1 65 | end 66 | end 67 | 68 | method :minor do 69 | test do 70 | v = Library::Version.new('1.2.3') 71 | v.minor.assert == 2 72 | end 73 | end 74 | 75 | method :patch do 76 | test do 77 | v = Library::Version.new('1.2.3') 78 | v.patch.assert == 3 79 | end 80 | end 81 | 82 | class_method :parse_constraint do 83 | test do 84 | constraint = Library::Version.parse_constraint("~> 1.0.0") 85 | constraint.assert == ["=~", Library::Version['1.0.0']] 86 | end 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /work/deprecated/requirements.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # 4 | class Requirements 5 | 6 | include Enumerable 7 | 8 | # 9 | def initialize(location) 10 | @location = location 11 | @dependencies = nil 12 | end 13 | 14 | # Location of project. 15 | def location 16 | @location 17 | end 18 | 19 | # Returns an array of `[name, constraint]`. 20 | def dependencies 21 | @dependencies || load_require_file 22 | end 23 | 24 | # TODO: split runtime from all others 25 | def runtime 26 | dependencies 27 | end 28 | 29 | # 30 | def exist? 31 | File.exist?(File.join(location, 'REQUIRE')) 32 | end 33 | 34 | # 35 | def load_require_file 36 | return [] 37 | 38 | #require 'yaml' 39 | dependencies = [] 40 | file = File.join(location, 'REQUIRE') 41 | if File.exist?(file) 42 | begin 43 | data = YAML.load(File.new(file)) 44 | list = [] 45 | data.each do |type, reqs| 46 | list.concat(reqs) 47 | end 48 | list.each do |dep| 49 | name, *vers = dep.split(/\s+/) 50 | vers = vers.join('') 51 | vers = nil if vers.empty? 52 | dependencies << [name, vers] 53 | end 54 | rescue 55 | $stderr.puts "roll: failed to load requirements -- #{file}" 56 | dependencies = [] 57 | end 58 | end 59 | @dependencies = dependencies 60 | end 61 | 62 | # Returns a list of Library and/or [name, vers] entries. 63 | # A Libray entry means the library was loaded, whereas the 64 | # name/vers array means it failed. (TODO: Best way to do this?) 65 | # 66 | # TODO: don't do stdout here 67 | def verify(verbose=false) 68 | libs, fail = [], [] 69 | dependencies.each do |name, vers| 70 | lib = Library[name, vers] 71 | if lib 72 | libs << lib 73 | $stdout.puts " [LOAD] #{name} #{vers}" if verbose 74 | unless libs.include?(lib) or fail.include?(luib) 75 | lib.requirements.verify(verbose) 76 | end 77 | else 78 | fail << [name, vers] 79 | $stdout.puts " [FAIL] #{name} #{vers}" if verbose 80 | end 81 | end 82 | return libs, fail 83 | end 84 | 85 | # 86 | def each #:yield: 87 | dependencies.each{|x| yield(x) } 88 | end 89 | 90 | # 91 | def size 92 | dependencies.size 93 | end 94 | 95 | # 96 | def empty? 97 | dependencies.empty? 98 | end 99 | end 100 | 101 | end 102 | 103 | -------------------------------------------------------------------------------- /HISTORY.rdoc: -------------------------------------------------------------------------------- 1 | = RELEASE HISTORY 2 | 3 | == 2.0.0 / 2012-01-08 4 | 5 | First, this realease renames the package name from `roll` to `rolls`. 6 | Seems the plural rolls off the tounge better ;) 7 | 8 | Changes: 9 | 10 | * Rename gem from `roll` to `rolls`. 11 | 12 | 13 | == 1.2.0 / 2010-06-15 14 | 15 | This release gets roll command working and improves 16 | the reliability of the system as a whole, including making 17 | metadata lookup more consistant (hint: you want a PACKAGE file). 18 | It is even partially compatible with Gem stores now (exceptions 19 | being loadpaths other than lib/ and the use of autoload). 20 | 21 | Changes: 22 | 23 | * Reworked metadata system (in line with evolving POM). 24 | * Improved search heuristics (usually much faster now). 25 | 26 | 27 | == 1.1.0 / 2010-03-01 28 | 29 | This release fix a few bugs and makes a few adjustments, 30 | but mostly cleans up code behind the scenes. 31 | 32 | Changes: 33 | 34 | * Fix incorrect multi-match and absolute path lookup 35 | * Support for Rubinius RUBY_IGNORE_CALLERS 36 | 37 | 38 | == 1.0.0 / 2010-02-11 39 | 40 | This release overhauls the underlying system, which is now very 41 | fast. It supports customizable library environments, and banashes 42 | all traces of package management to the domain of other tools. 43 | 44 | Changes: 45 | 46 | * Overhauled the entire underlying system. 47 | * Start-up time is blazing fast, loading's is pretty good too. 48 | * Metadata uses POM standard, although not dependent (yet?). 49 | * Environments provide selectable sets of available libraries. 50 | 51 | 52 | == 0.9.4 / 2008-06-05 53 | 54 | The .roll file is no longer used. Instead Rolls is now 55 | using a VERSION file combined with meta/ entries for 56 | loadpath and dependencies (ie. requires). 57 | 58 | Changes: 59 | 60 | * VERSION and meta/ entries are used instead of '.roll'. 61 | 62 | 63 | == 0.9.3 / 2007-02-10 64 | 65 | Changes: 66 | 67 | * Change roll file format and name. It is now .roll. 68 | * Relative require with #use should now work. 69 | 70 | 71 | == 0.9.2 / 2007-12-17 72 | 73 | Changes: 74 | 75 | * Changed roll file format from ROLLRC to {name}.roll. 76 | * The name change enabled an order of magnitude increase in startup time! 77 | 78 | 79 | == 0.9.1 / 2007-11-27 80 | 81 | Changes: 82 | 83 | * Standard metadate file is now ROLLRC. 84 | * Improved parsing of ROLLRC file, #release is now a Time object. 85 | 86 | 87 | == 0.9.0 / 2007-11-12 88 | 89 | Changes: 90 | 91 | * Removed Roll namespace. Library and VersionNumber are now in toplevel. 92 | * Fixed spelling of 'version' in Library#<=>. 93 | * Kernel#require and load now route to Library meta-methods. 94 | * @roll and related methods have been renamed to @package. 95 | * Reduced scan glob to single pattern. Scanning is over 3x faster! 96 | 97 | -------------------------------------------------------------------------------- /lib/library/utils.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | module Utils 4 | extend self 5 | 6 | # 7 | # TODO: Not sure RUBYLIB environment should be included in user_path. 8 | # 9 | 10 | # 11 | # Lookup a path in locations that were added to $LOAD_PATH manually. 12 | # These include those added via `-I` command line option, the `RUBYLIB` 13 | # environment variable and those add to $LOAD_PATH via code. 14 | # 15 | # This is a really throwback to the old load system. But it is necessary as 16 | # long as the old system is used, to ensure expected behavior. 17 | # 18 | # @return [String] 19 | def find_userpath(path, options) 20 | find_path(user_path, path, options) 21 | end 22 | 23 | # 24 | # Find a path in the given load paths, taking into account load options. 25 | # 26 | # @return [String] 27 | # 28 | def find_path(loadpath, pathname, options) 29 | return nil if loadpath.empty? 30 | 31 | suffix = options[:suffix] || options[:suffix].nil? 32 | #suffix = true if options[:require] # TODO: Is this always true? 33 | suffix = false if SUFFIXES.include?(::File.extname(pathname)) # TODO: Why not just add '' to SUFFIXES? 34 | 35 | suffixes = suffix ? SUFFIXES : SUFFIXES_NOT 36 | 37 | loadpath.each do |lpath| 38 | suffixes.each do |ext| 39 | f = ::File.join(lpath, pathname + ext) 40 | return f if ::File.file?(f) 41 | end 42 | end 43 | 44 | return nil 45 | end 46 | 47 | # 48 | # Lookup a path in locations that were added to $LOAD_PATH manually. 49 | # These include those added via `-I` command line option, the `RUBYLIB` 50 | # environment variable and those add to $LOAD_PATH via code. 51 | # 52 | # @return [Array] 53 | # 54 | def user_path 55 | load_path = $LOAD_PATH - ruby_library_locations 56 | load_path = load_path.reject{ |p| gem_paths.any?{ |g| p.start_with?(g) } } 57 | end 58 | 59 | # 60 | # Ruby library locations as given in RbConfig. 61 | # 62 | # @return [Array] 63 | # 64 | def ruby_library_locations 65 | @_ruby_library_locations ||= ( 66 | RbConfig::CONFIG.values_at( 67 | 'rubylibdir', 68 | 'archdir', 69 | 'sitedir', 70 | 'sitelibdir', 71 | 'sitearchdir', 72 | 'vendordir', 73 | 'vendorlibdir', 74 | 'vendorarchdir' 75 | ) 76 | ) 77 | end 78 | 79 | # 80 | # List of gem paths taken from the environment variable `GEM_PATH`, or failing 81 | # that `GEM_HOME`. 82 | # 83 | # @todo Perhaps these should be taken directly from Gem module instead? 84 | # 85 | # @return [Array] 86 | # 87 | def gem_paths 88 | @_gem_paths ||= (ENV['GEM_PATH'] || ENV['GEM_HOME']).split(/[:;]/) 89 | end 90 | 91 | end 92 | 93 | end 94 | -------------------------------------------------------------------------------- /lib/library/kernel.rb: -------------------------------------------------------------------------------- 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 | unless method_defined?(:require_without_library) 9 | 10 | class << self 11 | alias require_without_library require 12 | alias load_without_library load 13 | end 14 | 15 | alias require_without_library require 16 | alias load_without_library load 17 | 18 | # 19 | # Acquire feature - This is Roll's modern require/load method. 20 | # It differs from the usual `#require` or `#load` primarily by 21 | # the fact that it will search the current loading library, 22 | # i.e. the one belonging to the feature on the top of the 23 | # #LOAD_STACK, before looking elsewhere. The reason we can't 24 | # adjust `#require` to do this is becuase it could load a local 25 | # feature when a non-local feature was intended. For example, if 26 | # a library contained 'fileutils.rb' then this would be loaded 27 | # rather the Ruby's standard library. When using `#acquire`, 28 | # one would have to use the `ruby/` prefix to ensure the Ruby 29 | # library gets loaded. 30 | # 31 | # @param pathname [String] 32 | # The pathname of the feature to acquire. 33 | # 34 | # @param options [Hash] 35 | # Load options are `:wrap`, `:load`, `:legacy` and `:search`. 36 | # 37 | # @return [true, false] 38 | # Was the feature newly required or successfully loaded, depending 39 | # on the `:load` option settings. 40 | # 41 | def acquire(pathname, options={}) #, &block) 42 | Library.acquire(pathname, options) #, &block) 43 | end 44 | 45 | module_function :acquire 46 | 47 | # 48 | # Require feature - This is the same as acquire except that the 49 | # `:legacy` option is fixed as `true`. 50 | # 51 | # @param pathname [String] 52 | # The pathname of the feature to require. 53 | # 54 | # @param options [Hash] 55 | # Load options can be `:wrap`, `:load` and `:search`. 56 | # 57 | # @return [true,false] if feature was newly required 58 | # 59 | def require(pathname, options={}) #, &block) 60 | Library.require(pathname, options) #, &block) 61 | end 62 | 63 | module_function :require 64 | 65 | # 66 | # Load feature - This is the same as acquire except that the 67 | # `:legacy` and `:load` options are fixed as `true`. 68 | # 69 | # @param pathname [String] 70 | # The pathname of the feature to load. 71 | # 72 | # @param options [Hash] 73 | # Load options can be :wrap and :search. 74 | # 75 | # @return [true, false] if feature was successfully loaded 76 | # 77 | def load(pathname, options={}) #, &block) 78 | Library.load(pathname, options) #, &block) 79 | end 80 | 81 | module_function :load 82 | 83 | end 84 | 85 | end 86 | -------------------------------------------------------------------------------- /demo/04_version.rdoc: -------------------------------------------------------------------------------- 1 | = Version Class 2 | 3 | Librarys has a versioj class which it uses to compare 4 | versions via vairous constraints. 5 | 6 | Load the library. 7 | 8 | require 'library/version' 9 | 10 | First, a version object will return the string representation 11 | via #to_s. 12 | 13 | v = Library::Version.new('1.2.3') 14 | '1.2.3'.assert == v.to_s 15 | 16 | It will also do this for #to_str. 17 | 18 | v = Library::Version.new('1.2.3') 19 | '1.2.3'.assert == v.to_str 20 | 21 | The actual segments of a version are stored in an array. 22 | We can access those via #[]. 23 | 24 | v = Library::Version.new('1.2.3') 25 | 1.assert == v[0] 26 | 2.assert == v[1] 27 | 3.assert == v[2] 28 | 29 | In addition certain segmetns have special names. 30 | The first is accessible via #major. 31 | 32 | v = Library::Version.new('1.2.3') 33 | v.major.assert == 1 34 | 35 | The second is accessible via #minor. 36 | 37 | v = Library::Version.new('1.2.3') 38 | v.minor.assert == 2 39 | 40 | The third is accessible via #patch. 41 | 42 | v = Library::Version.new('1.2.3') 43 | v.patch.assert == 3 44 | 45 | And lastly anything beyond the patch number is accessible 46 | via #build. 47 | 48 | v = Library::Version.new('1.2.3.pre.1') 49 | v.build.assert == 'pre.1' 50 | 51 | Two version can be compared with the #<=> method, which 52 | importantly also makes lists of versions sortable. 53 | 54 | v1 = Library::Version.new('1.2.3') 55 | v2 = Library::Version.new('1.2.4') 56 | (v2 <=> v1).assert == 1 57 | 58 | While #=~ provides *pessimistic* constraint comparison. 59 | 60 | v1 = Library::Version.new('1.2.4') 61 | v2 = Library::Version.new('1.2') 62 | assert(v1 =~ v2) 63 | 64 | The Version class also provides some useful singleton methods 65 | such as #parse_constraint. This method deciphers a comparision string. 66 | 67 | a = Library::Version.parse_constraint("~> 1.0.0") 68 | e = ["=~", Library::Version['1.0.0']] 69 | a.assert == e 70 | 71 | Equality can be written with `==` or `=`. 72 | 73 | a = Library::Version.parse_constraint("= 0.9.0") 74 | e = ["==", Library::Version['0.9.0']] 75 | a.assert == e 76 | 77 | Without an operator equality is implied. 78 | 79 | a = Library::Version.parse_constraint("1.1") 80 | e = [ "==", Library::Version['1.1'] ] 81 | a.assert == e 82 | 83 | This is usefuly for parsing requirement configurations. Using it, the Version 84 | class can build contraint comparison procedures. 85 | 86 | compare = Library::Version.constraint_lambda("=1.0") 87 | compare.call('1.0').assert == true 88 | compare.call('0.9').assert == false 89 | 90 | Greater than 91 | 92 | compare = Library::Version.constraint_lambda(">1.0") 93 | compare.call('1.1').assert == true 94 | compare.call('0.9').assert == false 95 | 96 | Less than 97 | 98 | compare = Library::Version.constraint_lambda("<1.0") 99 | compare.call('1.1').assert == false 100 | compare.call('0.9').assert == true 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Homepage](http://rubyworks.github.com/library) / 2 | [Documentation](http://wiki.github.com/rubyworks/library) / 3 | [Report Issue](http://github.com/rubyworks/library/issues) / 4 | [Source Code](http://github.com/rubyworks/library) 5 | ( [![Build Status](https://travis-ci.org/rubyworks/autoload.png)](https://travis-ci.org/rubyworks/library) ) 6 | 7 | 8 | # [The Ruby Library
    as a Library](#description) 9 | 10 | *Library* is library management system for Ruby. More specifically, it is a 11 | reformulation and partial-reimplementation of Ruby's load system. At it's 12 | core, the `Library` class is the objectification of a location in the file 13 | system from which Ruby scripts can be required. It is accompanied by a 14 | `Library::Ledger` which tracks a set such locations, and makes them available 15 | via Ruby's standard `require` and `load` methods. Unlike Ruby's built-in load 16 | system, Library supports versioning and it does all this in a way that is both 17 | easily *customizable* and *fast*. 18 | 19 | Combined with some supporting functionality, this bestows a variety of useful 20 | possibilities to Ruby developers: 21 | 22 | * Work with libraries in an object-oriented manner. 23 | * Develop interdependent projects in real time without installs or vendoring. 24 | * Create isolated library environments based on project requirements. 25 | * Nullify the need for per-project gemsets and multiple copies of the same gem. 26 | * Access libraries anywhere; there is no special "home" path they *must* reside. 27 | * Can also serve gem installed libraries as easily as any others. 28 | 29 | 30 | ## Documentation 31 | 32 | Because there is fair amount of information to cover this section will 33 | refer you to the project wiki pages for instruction. Most users can follow 34 | the [Quick Start Guide](https://github.com/rubyworks/library/wiki/Quick-Start-Guide). 35 | For more detailed instruction on how setup Library and get the most out select 36 | from the following links: 37 | 38 | * [Installation](https://github.com/rubyworks/library/wiki/Installation) 39 | * [System Setup](https://github.com/rubyworks/library/wiki/System-Setup) 40 | * [Project Conformity](https://github.com/rubyworks/library/wiki/Project-Conformity) 41 | * [Run Modes](https://github.com/rubyworks/library/wiki/Run-Modes) 42 | * [Dependency Isolation](https://github.com/rubyworks/library/wiki/Dependency-Isolation) 43 | * [Configuring Locations](https://github.com/library/wiki/Configuring-Locations) 44 | * [API Usage](https://github.com/rubyworks/library/wiki/API-Usage) 45 | 46 | 47 | ## Status 48 | 49 | Library started out as a project called "ROLL", which stood for *Ruby Objectified Library Ledger*. 50 | Create in 2006, it was RubyForge project #1004. She's actually been around a while! 51 | Over the years the code has gone through several rewrites, but has always remained in service 52 | as a development tool. So, on the whole, the underlying functionality is in good working order. 53 | However, the system is still undergoing some refinement --as one can imagine, it is not the 54 | easiest type of library to write or maintain. So some things are still subject to change. 55 | 56 | 57 | ## Copyrights 58 | 59 | Library is copyrighted open source software. 60 | 61 | Copyright (c) 2006 Rubyworks. All rights reserved. 62 | 63 | It can be modified and redistributable in accordance with the **BSD-2-Clause** license. 64 | 65 | See the LICENSE.txt file details. 66 | 67 | -------------------------------------------------------------------------------- /lib/library/rubylib.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'library' 3 | 4 | # RubyLibrary is a specialized subclass of Library specifically designed 5 | # to serve Ruby's standard library locations. It is used to speed up load 6 | # times for for library files that are standard Ruby scripts and should never 7 | # be overriden by any 3rd party libraries. Good examples are 'ostruct' and 8 | # 'optparse'. 9 | # 10 | # This class is in the proccess of being refined to exclude certian 3rd 11 | # party redistributions, such RDoc and Soap4r. 12 | # 13 | class RubyLibrary < Library 14 | 15 | include ::RbConfig 16 | 17 | # 18 | def self.singleton 19 | @r ||= new 20 | end 21 | 22 | # 23 | # Setup Ruby library. 24 | # 25 | def initialize(*) #(location, metadata={}) 26 | paths = CONFIG.values_at( 27 | 'rubylibdir', 28 | 'archdir', 29 | 'sitedir', 30 | 'sitelibdir', 31 | 'sitearchdir', 32 | 'vendordir', 33 | 'vendorlibdir', 34 | 'vendorarchdir' 35 | ) 36 | 37 | location = find_base_path(paths) 38 | loadpath = paths.map{ |d| d.sub(location + '/','') } 39 | 40 | @location = location 41 | @loadpath = loadpath 42 | @name = 'ruby' 43 | @metadata = {} # TODO: can we fillout Ruby's metadata some ? 44 | end 45 | 46 | # 47 | # Then name of RubyLibrary is `ruby`. 48 | # 49 | def name 50 | 'ruby' 51 | end 52 | 53 | # 54 | # Ruby version is RUBY_VERSION. 55 | # 56 | def version 57 | RUBY_VERSION 58 | end 59 | 60 | # TODO: Remove rugbygems from $LOAD_PATH and use that? 61 | 62 | # TODO: Sometimes people add paths directly to $LOAD_PATH, 63 | # should these be accessible via `ruby/`? 64 | 65 | # 66 | # Load path is essentially $LOAD_PATH, less gem paths. 67 | # 68 | def loadpath 69 | #$LOAD_PATH - ['.'] 70 | @loadpath 71 | end 72 | 73 | alias load_path loadpath 74 | 75 | # 76 | # Release date. 77 | # 78 | # @todo This currently just returns current date/time. 79 | # Is there a way to get Ruby's own release date? 80 | # 81 | def date 82 | Time.now 83 | end 84 | 85 | # 86 | alias released date 87 | 88 | # 89 | # Ruby requires nothing. 90 | # 91 | def requirements 92 | [] 93 | end 94 | 95 | # 96 | # Ruby needs to ignore a few 3rd party libraries. They will 97 | # be picked up by the final fallback to Ruby's original require 98 | # if all else fails. 99 | # 100 | def find(file, suffix=true) 101 | return nil if /^rdoc/ =~ file 102 | super(file, suffix) 103 | end 104 | 105 | # 106 | # Location of executables, which for Ruby is `RbConfig::CONFIG['bindir']`. 107 | # 108 | def bindir 109 | ::RbConfig::CONFIG['bindir'] 110 | end 111 | 112 | # 113 | # Is there a `bin/` location? 114 | # 115 | def bindir? 116 | File.exist?(bindir) 117 | end 118 | 119 | # 120 | # Location of library system configuration files. 121 | # For Ruby this is `RbConfig::CONFIG['sysconfdir']`. 122 | # 123 | def confdir 124 | ::RbConfig::CONFIG['sysconfdir'] 125 | end 126 | 127 | # 128 | # Is there a "`etc`" location? 129 | # 130 | def confdir? 131 | File.exist?(confdir) 132 | end 133 | 134 | # 135 | # Location of library shared data directory. For Ruby this is 136 | # `RbConfig::CONFIG['datadir']`. 137 | # 138 | def datadir 139 | ::RbConfig::CONFIG['datadir'] 140 | end 141 | 142 | # 143 | # Is there a `data/` location? 144 | # 145 | def datadir? 146 | File.exist?(datadir) 147 | end 148 | 149 | # 150 | # Require library +file+ given as a Script instance. 151 | # 152 | # @param [String] feature 153 | # Instance of Feature. 154 | # 155 | # @return [Boolean] Success of requiring the feature. 156 | # 157 | def require_absolute(feature) 158 | return false if $".include?(feature.localname) # ruby 1.8 does not use absolutes 159 | success = super(feature) 160 | $" << feature.localname # ruby 1.8 does not use absolutes TODO: move up? 161 | $".uniq! 162 | success 163 | end 164 | 165 | # 166 | # Load library +file+ given as a Script instance. 167 | # 168 | # @param [String] feature 169 | # Instance of Feature. 170 | # 171 | # @return [Boolean] Success of loading the feature. 172 | # 173 | def load_absolute(feature, wrap=nil) 174 | success = super(feature, wrap) 175 | $" << feature.localname # ruby 1.8 does not use absolutes TODO: move up? 176 | $".uniq! 177 | success 178 | end 179 | 180 | # 181 | # The loadpath sorted by largest path first. 182 | # 183 | def loadpath_sorted 184 | loadpath.sort{ |a,b| b.size <=> a.size } 185 | end 186 | 187 | # 188 | # Construct a Script match. 189 | # 190 | def libfile(lpath, file, ext=nil) 191 | Library::Feature.new(self, lpath, file, ext) 192 | end 193 | 194 | private 195 | 196 | # Given an array of path strings, find the longest common prefix path. 197 | def find_base_path(paths) 198 | return paths.first if paths.length <= 1 199 | arr = paths.sort 200 | f = arr.first.split('/') 201 | l = arr.last.split('/') 202 | i = 0 203 | i += 1 while f[i] == l[i] && i <= f.length 204 | f.slice(0, i).join('/') 205 | end 206 | 207 | end 208 | 209 | -------------------------------------------------------------------------------- /.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'yaml' 4 | 5 | module DotRuby 6 | 7 | # 8 | class GemSpec 9 | 10 | # For which revision of .ruby is this gemspec intended? 11 | REVISION = 0 unless defined?(REVISION) 12 | 13 | # 14 | PATTERNS = { 15 | :bin_files => 'bin/*', 16 | :lib_files => 'lib/{**/}*.rb', 17 | :ext_files => 'ext/{**/}extconf.rb', 18 | :doc_files => '*.{txt,rdoc,md,markdown,tt,textile}', 19 | :test_files => '{test/{**/}*_test.rb,spec/{**/}*_spec.rb}' 20 | } unless defined?(PATTERNS) 21 | 22 | # 23 | def self.instance 24 | new.to_gemspec 25 | end 26 | 27 | attr :metadata 28 | 29 | attr :manifest 30 | 31 | # 32 | def initialize 33 | @metadata = YAML.load_file('.ruby') 34 | @manifest = Dir.glob('manifest{,.txt}', File::FNM_CASEFOLD).first 35 | 36 | if @metadata['revision'].to_i != REVISION 37 | warn "You have the wrong revision. Trying anyway..." 38 | end 39 | end 40 | 41 | # 42 | def scm 43 | @scm ||= \ 44 | case 45 | when File.directory?('.git') 46 | :git 47 | end 48 | end 49 | 50 | # 51 | def files 52 | @files ||= \ 53 | #glob_files[patterns[:files]] 54 | case 55 | when manifest 56 | File.readlines(manifest). 57 | map{ |line| line.strip }. 58 | reject{ |line| line.empty? || line[0,1] == '#' } 59 | when scm == :git 60 | `git ls-files -z`.split("\0") 61 | else 62 | Dir.glob('{**/}{.*,*}') # TODO: be more specific using standard locations ? 63 | end.select{ |path| File.file?(path) } 64 | end 65 | 66 | # 67 | def glob_files(pattern) 68 | Dir.glob(pattern).select { |path| 69 | File.file?(path) && files.include?(path) 70 | } 71 | end 72 | 73 | # 74 | def patterns 75 | PATTERNS 76 | end 77 | 78 | # 79 | def executables 80 | @executables ||= \ 81 | glob_files(patterns[:bin_files]).map do |path| 82 | File.basename(path) 83 | end 84 | end 85 | 86 | def extensions 87 | @extensions ||= \ 88 | glob_files(patterns[:ext_files]).map do |path| 89 | File.basename(path) 90 | end 91 | end 92 | 93 | # 94 | def name 95 | metadata['name'] || metadata['title'].downcase.gsub(/\W+/,'_') 96 | end 97 | 98 | # 99 | def to_gemspec 100 | Gem::Specification.new do |gemspec| 101 | gemspec.name = name 102 | gemspec.version = metadata['version'] 103 | gemspec.summary = metadata['summary'] 104 | gemspec.description = metadata['description'] 105 | 106 | metadata['authors'].each do |author| 107 | gemspec.authors << author['name'] 108 | 109 | if author.has_key?('email') 110 | if gemspec.email 111 | gemspec.email << author['email'] 112 | else 113 | gemspec.email = [author['email']] 114 | end 115 | end 116 | end 117 | 118 | gemspec.licenses = metadata['copyrights'].map{ |c| c['license'] }.compact 119 | 120 | metadata['requirements'].each do |req| 121 | name = req['name'] 122 | version = req['version'] 123 | groups = req['groups'] || [] 124 | 125 | case version 126 | when /^(.*?)\+$/ 127 | version = ">= #{$1}" 128 | when /^(.*?)\-$/ 129 | version = "< #{$1}" 130 | when /^(.*?)\~$/ 131 | version = "~> #{$1}" 132 | end 133 | 134 | if groups.empty? or groups.include?('runtime') 135 | # populate runtime dependencies 136 | if gemspec.respond_to?(:add_runtime_dependency) 137 | gemspec.add_runtime_dependency(name,*version) 138 | else 139 | gemspec.add_dependency(name,*version) 140 | end 141 | else 142 | # populate development dependencies 143 | if gemspec.respond_to?(:add_development_dependency) 144 | gemspec.add_development_dependency(name,*version) 145 | else 146 | gemspec.add_dependency(name,*version) 147 | end 148 | end 149 | end 150 | 151 | # convert external dependencies into a requirements 152 | if metadata['external_dependencies'] 153 | ##gemspec.requirements = [] unless metadata['external_dependencies'].empty? 154 | metadata['external_dependencies'].each do |req| 155 | gemspec.requirements << req.to_s 156 | end 157 | end 158 | 159 | # determine homepage from resources 160 | homepage = metadata['resources'].find{ |key, url| key =~ /^home/ } 161 | gemspec.homepage = homepage.last if homepage 162 | 163 | gemspec.require_paths = metadata['load_path'] || ['lib'] 164 | gemspec.post_install_message = metadata['install_message'] 165 | 166 | # RubyGems specific metadata 167 | gemspec.files = files 168 | gemspec.extensions = extensions 169 | gemspec.executables = executables 170 | 171 | if Gem::VERSION < '1.7.' 172 | gemspec.default_executable = gemspec.executables.first 173 | end 174 | 175 | gemspec.test_files = glob_files(patterns[:test_files]) 176 | 177 | unless gemspec.files.include?('.document') 178 | gemspec.extra_rdoc_files = glob_files(patterns[:doc_files]) 179 | end 180 | end 181 | end 182 | 183 | end #class GemSpec 184 | 185 | end 186 | 187 | DotRuby::GemSpec.instance 188 | -------------------------------------------------------------------------------- /lib/library/feature.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # The Feature class represents a single file within a library. 4 | # 5 | # This class had been called `Script` until it occured to me that 6 | # Ruby choose the name "feature" by it's use of them in the global 7 | # variable `$LOADED_FEATURES`. 8 | # 9 | class Feature 10 | 11 | # 12 | # Create a new Feature instance. 13 | # 14 | # @param library [Library] 15 | # The Library object to which the feature belongs. 16 | # 17 | # @param loadpath [String] 18 | # The loadpath within the library in which the feature resides. 19 | # 20 | # @param filename [String] 21 | # The file path of the feature relative to the loadpath. 22 | # 23 | # @param extension [Boolean] 24 | # File extension to append to the feature filename. 25 | # 26 | def initialize(library, loadpath, filename, extension=nil) 27 | @library = library 28 | @loadpath = loadpath 29 | @filename = filename 30 | @extension = extension 31 | 32 | @required = {} 33 | end 34 | 35 | # 36 | # The Library object to which the file belongs. 37 | # 38 | attr_reader :library 39 | 40 | # 41 | # The loadpath within the library in which the feature resides. 42 | # 43 | # @return [Array] Load path relative to library location. 44 | # 45 | attr_reader :loadpath 46 | 47 | # 48 | # The file path of the feature relative to the loadpath. 49 | # 50 | attr_reader :filename 51 | 52 | # 53 | # Extension of feature file, e.g. `.rb`. 54 | # 55 | attr_reader :extension 56 | 57 | # 58 | # Name of the library to which the feature belongs. 59 | # 60 | # @return [String] name of the feature's library 61 | # 62 | def library_name 63 | Library === library ? library.name : nil 64 | end 65 | 66 | # 67 | # 68 | # 69 | def library_activate 70 | if Library === library 71 | library.activate 72 | #Library.activate(library) 73 | end 74 | end 75 | 76 | # 77 | # Library location. 78 | # 79 | # @return [String] location of library 80 | # 81 | def location 82 | Library===library ? library.location : library 83 | end 84 | 85 | # 86 | # Full path name of of feature. 87 | # 88 | # @return [String] expanded file path of feature 89 | # 90 | def fullname 91 | @fullname ||= ::File.join(location, loadpath, filename + (extension || '')) 92 | end 93 | 94 | # 95 | # The path of the feature relative to the loadpath. 96 | # 97 | # @return [String] file path less location and loadpath 98 | # 99 | def localname 100 | @localname ||= ::File.join(filename + (extension || '')) 101 | end 102 | 103 | # 104 | # Load feature. 105 | # 106 | # @param [Hash] options 107 | # 108 | # @option options [Boolean] :require 109 | # Load the feature only once (per scope). 110 | # 111 | # @return [true,false] true if loaded/required, false otherwise. 112 | # 113 | def load(options={}) 114 | # ruby 1.8 does not use absolutes 115 | #if library_name == 'ruby' #or library_name == 'site_ruby' 116 | # if options[:require] && !options[:wrap] 117 | # return false if $".include?(localname) 118 | # $" << localname 119 | # end 120 | #end 121 | 122 | $LOAD_STACK << self #library 123 | begin 124 | library_activate unless options[:force] 125 | success = evaluate(options[:wrap], options[:require]) 126 | #rescue ::LoadError => load_error 127 | # raise LoadError.new(localname, library_name) 128 | ensure 129 | $LOAD_STACK.pop 130 | end 131 | success 132 | end 133 | 134 | # 135 | # Require feature. This is the same as load except the the feature will 136 | # only be loaded once per scope. The default scope is `TOPLEVEL_BINDING`. 137 | # 138 | # @return [true,false] true if loaded, false if it already has been loaded. 139 | # 140 | def require(options={}) 141 | options[:require] = true 142 | load(options) 143 | end 144 | 145 | #def require(options={}) 146 | # if library_name == 'ruby' #or library_name == 'site_ruby' 147 | # return false if $".include?(localname) # ruby 1.8 does not use absolutes 148 | # $" << localname # ruby 1.8 does not use absolutes 149 | # end 150 | # 151 | # # TODO: return false if $".include(fullname) ? But Ruby should be handling this. 152 | # 153 | # Library.load_stack << self #library 154 | # begin 155 | # library_activate unless options[:force] 156 | # @required = true 157 | # success = require_without_library(fullname) 158 | # #rescue ::LoadError => load_error # TODO: deativeate this if $DEBUG ? 159 | # # raise LoadError.new(localname, library_name) 160 | # ensure 161 | # Library.load_stack.pop 162 | # end 163 | # success 164 | #end 165 | 166 | # 167 | # Has the feature been required? 168 | # 169 | def required?(scope=nil) 170 | @required[scope || TOPLEVEL_BINDING] 171 | end 172 | 173 | # 174 | # Compare this features full path name to another using `#==`. 175 | # 176 | # @param [Feature,String] another feature or file path. 177 | # 178 | # @return [true,false] do the features represent the the same file 179 | # 180 | def ==(other) 181 | fullname == other.to_s 182 | end 183 | 184 | # 185 | # Same as `#==`. 186 | # 187 | # @param [Feature, String] another feature or file path. 188 | # 189 | # @return [true, false] if features are the same file 190 | # 191 | def eql?(other) 192 | fullname == other.to_s 193 | end 194 | 195 | # 196 | # Same a fullname. 197 | # 198 | # @return [String] expanded file path 199 | # 200 | def to_s 201 | fullname 202 | end 203 | 204 | # 205 | # Same a fullname. 206 | # 207 | # @return [String] expanded file path 208 | # 209 | def to_str 210 | fullname 211 | end 212 | 213 | # 214 | # Use `#fullname` to calculate a hash value for the feature file. 215 | # 216 | # @return [Integer] hash value 217 | # 218 | def hash 219 | fullname.hash 220 | end 221 | 222 | # 223 | # Read file. 224 | # 225 | def read 226 | ::File.read(fullname) 227 | end 228 | 229 | private 230 | 231 | # 232 | # Evaluate feature within given scope. 233 | # 234 | # @param [Boolean] required 235 | # Only evaluate once per scope. 236 | # 237 | # @return [Boolean] Will return true if evaluated, false otherwise. 238 | # 239 | def evaluate(scope, required=false) 240 | return false if required && required?(scope) 241 | 242 | case scope 243 | when FalseClass, NilClass, TrueClass 244 | if required 245 | success = require_without_library(fullname) 246 | @required[TOPLEVEL_BINDING] = true if required 247 | else 248 | success = load_without_library(fullname, scope) 249 | end 250 | when Module, Class 251 | scope.module_eval(read, fullname) 252 | @required[scope] = true if required 253 | success = true 254 | when Binding 255 | scope.eval(read, fullname) 256 | @required[scope] = true if required 257 | success = true 258 | else 259 | # TODO: Evaluate feature into object scope ? 260 | raise LoadError 261 | end 262 | 263 | success 264 | end 265 | 266 | end 267 | 268 | end 269 | -------------------------------------------------------------------------------- /lib/library/metadata.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | #$DEBUG = true 4 | require 'yaml' # TODO: this should not be needed here 5 | 6 | # The Metadata class encapsulates a library's basic information, in particular 7 | # name, version and load path. 8 | # 9 | class Metadata 10 | 11 | # 12 | # Setup new metadata object. 13 | # 14 | # @param [String] location 15 | # Location of project on disc. 16 | # 17 | # @param [Hash] metadata 18 | # Set metadata manually, instead of loading from file. 19 | # 20 | def initialize(location, metadata=nil) 21 | @location = location 22 | 23 | #@load = metadata.delete(:load) 24 | #@load = true if @load.nil? # default is true 25 | 26 | @data = {} 27 | 28 | if metadata && !metadata.empty? 29 | update(metadata) 30 | else 31 | load_metadata 32 | end 33 | 34 | raise "#{location} has no name or version" unless @data[:name] && @data[:version] 35 | end 36 | 37 | # 38 | # Update metadata with data hash. 39 | # 40 | # @param [Hash] data 41 | # Data to merge into metadata table. 42 | # 43 | def update(data) 44 | data = data.rekey 45 | 46 | data.each do |key, value| 47 | __send__("#{key}=", value) if respond_to?("#{key}=") 48 | end 49 | end 50 | 51 | # 52 | # Location of library. 53 | # 54 | attr :location 55 | 56 | # 57 | # Paths, the only one used currently is `load`. 58 | # 59 | def paths 60 | @data[:paths] 61 | end 62 | 63 | # 64 | # Set paths map. 65 | # 66 | # @param [Hash] paths 67 | # Paths map. 68 | # 69 | def paths=(paths) 70 | @data[:paths] = ( 71 | h = {} 72 | paths.each do |key, val| 73 | val = (String === val ? split_path(val) : val) 74 | h[key.to_sym] = Array(val) 75 | end 76 | h 77 | ) 78 | end 79 | 80 | # 81 | # Local load paths. 82 | # 83 | def loadpath 84 | @data[:paths] ||= {} 85 | @data[:paths][:load] || ['lib'] 86 | end 87 | 88 | alias_method :load_path, :loadpath 89 | 90 | # 91 | # Set the load paths. 92 | # 93 | def loadpath=(path) 94 | case path 95 | when nil 96 | path = ['lib'] 97 | when String 98 | path = split_path(path) 99 | end 100 | @data[:paths] ||= {} 101 | @data[:paths][:load] = path 102 | end 103 | 104 | alias_method :load_path=, :loadpath= 105 | 106 | # 107 | # Name of library. 108 | # 109 | def name 110 | @data[:name] 111 | end 112 | 113 | # 114 | # Set name. 115 | # 116 | def name=(string) 117 | @data[:name] = string.to_s if string 118 | end 119 | 120 | # 121 | # Version number. 122 | # 123 | # Technically, a library should not appear in a ledger list if it lacks 124 | # a version. However, just in case this occurs (say by a hand edited 125 | # environment) we fallback to a version of '0.0.0'. 126 | # 127 | def version 128 | @data[:version] ||= Version.new('0.0.0') 129 | end 130 | 131 | # 132 | # Set version, converts string into Version number class. 133 | # 134 | def version=(string) 135 | @data[:version] = Version.new(string) if string 136 | end 137 | 138 | # 139 | # Release date. 140 | # 141 | def date 142 | @data[:date] 143 | end 144 | 145 | alias_method :released, :date 146 | 147 | # 148 | # Set the date. 149 | # 150 | # TODO: Should we convert date to Time object? 151 | # 152 | def date=(date) 153 | @data[:date] = date 154 | end 155 | 156 | alias_method :released=, :date= 157 | 158 | # 159 | # Runtime and development requirements combined. 160 | # 161 | def requirements 162 | @data[:requirements] 163 | end 164 | 165 | # 166 | # Runtime and development requirements combined. 167 | # 168 | def requirements=(requirements) 169 | @data[:requirements] = requirements 170 | end 171 | 172 | # 173 | # Runtime requirements. 174 | # 175 | def runtime_requirements 176 | @runtime_requirements ||= requirements.reject{ |r| r['development'] } 177 | end 178 | 179 | # 180 | # Development requirements. 181 | # 182 | def development_requirements 183 | @development_requirements ||= requirements.select{ |r| r['development'] } 184 | end 185 | 186 | # 187 | # Access to non-primary metadata. 188 | # 189 | def [](name) 190 | @data[name.to_sym] 191 | end 192 | 193 | # TODO: Should we support +omit+ setting, or should we add a way to 194 | # exclude loctions via environment setting? 195 | 196 | # 197 | # Omit from any ledger? 198 | # 199 | def omit? 200 | @omit 201 | end 202 | 203 | # 204 | # Set omit. 205 | # 206 | def omit=(boolean) 207 | @omit = boolean 208 | end 209 | 210 | # 211 | # Does this location have .index file? 212 | # 213 | def dotindex? 214 | @_dotindex ||= File.exist?(File.join(location, '.index')) 215 | end 216 | 217 | # 218 | # Deterime if the location is a gem location. It does this by looking 219 | # for the corresponding `gems/specification/*.gemspec` file. 220 | # 221 | def gemspec? 222 | #return true if Dir[File.join(location, '*.gemspec')].first 223 | pkgname = File.basename(location) 224 | gemsdir = File.dirname(location) 225 | specdir = File.join(File.dirname(gemsdir), 'specifications') 226 | Dir[File.join(specdir, "#{pkgname}.gemspec")].first 227 | end 228 | 229 | # 230 | # Access to complete gemspec. This is for use with extended metadata. 231 | # 232 | def gemspec 233 | @_gemspec ||= ( 234 | require 'rubygems' 235 | ::Gem::Specification.load(gemspec_file) 236 | ) 237 | end 238 | 239 | # 240 | # Verify that a library's requirements are all available in the ledger. 241 | # Returns a list of `[name, version]` of Libraries that were not found. 242 | # 243 | # @return [Array] List of missing requirements. 244 | # 245 | def missing_requirements(development=false) #verbose=false) 246 | libs, fail = [], [] 247 | reqs = development ? requirements : runtime_requirements 248 | reqs.each do |req| 249 | name = req['name'] 250 | vers = req['version'] 251 | lib = Library[name, vers] 252 | if lib 253 | libs << lib 254 | #$stdout.puts " [LOAD] #{name} #{vers}" if verbose 255 | unless libs.include?(lib) or fail.include?([lib,vers]) 256 | lib.verify_requirements(development) #verbose) 257 | end 258 | else 259 | fail << [name, vers] 260 | #$stdout.puts " [FAIL] #{name} #{vers}" if verbose 261 | end 262 | end 263 | return fail 264 | end 265 | 266 | # 267 | # Like {#missing_requirements} but returns `true`/`false`. 268 | # 269 | def missing_requirements?(development=false) 270 | list = missing_requirements(development=false) 271 | list.empty? ? false : true 272 | end 273 | 274 | # 275 | # Returns hash of primary metadata. 276 | # 277 | # @return [Hash] primary metdata 278 | # 279 | def to_h 280 | { 'location' => location, 281 | 'name' => name, 282 | 'version' => version.to_s, 283 | 'date' => date.to_s, 284 | 'load_path' => load_path, 285 | 'requirements' => requirements, 286 | 'omit' => omit 287 | } 288 | end 289 | 290 | private 291 | 292 | # Load metadata. 293 | def load_metadata 294 | if dotindex? 295 | load_dotindex 296 | elsif gemspec? 297 | load_gemspec 298 | end 299 | end 300 | 301 | # 302 | # Load metadata for .index file. 303 | # 304 | def load_dotindex 305 | file = File.join(location, '.index') 306 | data = YAML.load_file(file) 307 | update(data) 308 | end 309 | 310 | # Load metadata from a gemspec. This is a fallback option. It is highly 311 | # recommended that a project have a `.index` file instead. 312 | # 313 | # This method requires that the `metaspec` gem be installed. # TODO: metaspec gem name ? 314 | # 315 | # TODO: Deprecate YAML form of gemspec, RubyGems no longer supports it. 316 | # 317 | def load_gemspec 318 | text = File.read(gemspec_file) 319 | if text =~ /\A---/ 320 | require 'yaml' 321 | spec = YAML.load(text) 322 | else 323 | spec = eval(text) #, gemspec_file) 324 | end 325 | 326 | data = {} 327 | data[:name] = spec.name 328 | data[:version] = spec.version.to_s 329 | data[:date] = spec.date 330 | 331 | data[:paths] = { 332 | 'load' => spec.require_paths 333 | } 334 | 335 | data[:requirements] = [] 336 | 337 | spec.runtime_dependencies.each do |dep| 338 | req = { 339 | 'name' => dep.name, 340 | 'version' => dep.requirement.to_s 341 | } 342 | data[:requirements] << req 343 | end 344 | 345 | spec.development_dependencies.each do |dep| 346 | req = { 347 | 'name' => dep.name, 348 | 'version' => dep.requirement.to_s, 349 | 'development' => true 350 | } 351 | data[:requirements] << req 352 | end 353 | 354 | update(data) 355 | end 356 | 357 | # 358 | # Returns the path to the .gemspec file. 359 | # 360 | def gemspec_file 361 | gemspec_file_system || gemspec_file_local 362 | end 363 | 364 | # 365 | # Returns the path to a gemspec file located in the project location, 366 | # if it exists. Otherwise returns +nil+. 367 | # 368 | def gemspec_file_local 369 | @_gemspec_file_local ||= Dir[File.join(location, '*.gemspec')].first 370 | end 371 | 372 | # 373 | # Returns the path to a gemspec file located in the gems/specifications 374 | # directory, if it exists. Otherwise returns +nil+. 375 | # 376 | def gemspec_file_system 377 | @_gemspec_file_system ||= ( 378 | pkgname = File.basename(location) 379 | gemsdir = File.dirname(location) 380 | specdir = File.join(File.dirname(gemsdir), 'specifications') 381 | Dir[File.join(specdir, "#{pkgname}.gemspec")].first 382 | ) 383 | end 384 | 385 | # 386 | #def require_indexer 387 | # require 'rubygems' 388 | # require 'indexer' 389 | # require 'indexer/rubygems' 390 | #end 391 | 392 | # 393 | def split_path(path) 394 | path.strip.split(/[,;:\ \n\t]/).map{|s| s.strip} 395 | end 396 | 397 | end 398 | 399 | end 400 | 401 | 402 | 403 | # Fake 404 | #module Gem 405 | # class Specification < Hash 406 | # def initialize 407 | # yield(self) 408 | # end 409 | # def method_missing(s,v=nil,*a,&b) 410 | # case s.to_s 411 | # when /=$/ 412 | # self[s.to_s.chomp('=').to_sym] = v 413 | # else 414 | # self[s] 415 | # end 416 | # end 417 | # end 418 | #end 419 | 420 | 421 | -------------------------------------------------------------------------------- /lib/library/ledgered.rb: -------------------------------------------------------------------------------- 1 | class Library 2 | 3 | # This module simply extends the Library class, giving it certain 4 | # convenience methods for interacting with the current Ledger. 5 | # 6 | # TODO: Change name of module, or move to Library. 7 | # 8 | module Ledgered 9 | 10 | require 'tmpdir' 11 | 12 | # 13 | # Access to library ledger. 14 | # 15 | # @return [Array] The `$LEDGER` array. 16 | # 17 | def ledger 18 | $LEDGER 19 | end 20 | 21 | # 22 | # Library names from ledger. 23 | # 24 | # @return [Array] The keys from `$LEDGER` array. 25 | # 26 | def names 27 | $LEDGER.keys 28 | end 29 | 30 | alias_method :list, :names 31 | 32 | # 33 | # A shortcut for #instance. 34 | # 35 | # @return [Library,NilClass] The activated Library instance, or `nil` if not found. 36 | # 37 | def [](name, constraint=nil) 38 | $LEDGER.activate(name, constraint) if $LEDGER.key?(name) 39 | end 40 | 41 | # 42 | # Get an instance of a library by name, or name and version. 43 | # Libraries are singleton, so once loaded the same object is 44 | # always returned. 45 | # 46 | # @todo This method might be deprecated. 47 | # 48 | # @return [Library,NilClass] The activated Library instance, or `nil` if not found. 49 | # 50 | def instance(name, constraint=nil) 51 | $LEDGER.activate(name, constraint) if $LEDGER.key?(name) 52 | end 53 | 54 | # 55 | # Activate a library. Same as #instance but will raise and error if the 56 | # library is not found. This can also take a block to yield on the library. 57 | # 58 | # @param [String] name 59 | # Name of library. 60 | # 61 | # @param [String] constraint 62 | # Valid version constraint. 63 | # 64 | # @raise [LoadError] 65 | # If library not found. 66 | # 67 | # @return [Library] 68 | # The activated Library object. 69 | # 70 | def activate(name, constraint=nil, &block) #:yield: 71 | $LEDGER.activate(name, constraint, &block) 72 | end 73 | 74 | # 75 | # Like `#new`, but adds library to library ledger. 76 | # 77 | # @todo Better name for this method? 78 | # 79 | # @return [Library] The new library. 80 | # 81 | def add(location) 82 | $LEDGER.add(location) 83 | end 84 | 85 | # 86 | # Find matching library features. This is the "mac daddy" method used by 87 | # the #require and #load methods to find the specified +path+ among 88 | # the various libraries and their load paths. 89 | # 90 | def find(path, options={}) 91 | $LEDGER.find_feature(path, options) 92 | end 93 | 94 | # 95 | # Brute force variation of `#find` looks through all libraries for a 96 | # matching features. This serves as the fallback method if `#find` comes 97 | # up empty. 98 | # 99 | # @param [String] path 100 | # path name for which to search 101 | # 102 | # @param [Hash] options 103 | # Search options. 104 | # 105 | # @option options [Boolean] :latest 106 | # Search only the active or most current version of any library. 107 | # 108 | # @option options [Boolean] :suffix 109 | # Automatically try standard extensions if pathname has none. 110 | # 111 | # @option options [Boolean] :legacy 112 | # Do not match within library's +name+ directory, eg. `lib/foo/*`. 113 | # 114 | # @return [Feature,Array] Matching feature(s). 115 | # 116 | def find_any(path, options={}) 117 | $LEDGER.find_any(path, options) 118 | end 119 | 120 | # 121 | # Brute force search looks through all libraries for matching features. 122 | # This is the same as #find_any, but returns a list of matches rather 123 | # then the first matching feature found. 124 | # 125 | # @param [String] path 126 | # path name for which to search 127 | # 128 | # @param [Hash] options 129 | # Search options. 130 | # 131 | # @option options [Boolean] :latest 132 | # Search only the active or most current version of any library. 133 | # 134 | # @option options [Boolean] :suffix 135 | # Automatically try standard extensions if pathname has none. 136 | # 137 | # @option options [Boolean] :legacy 138 | # Do not match within library's +name+ directory, eg. `lib/foo/*`. 139 | # 140 | # @return [Feature,Array] Matching feature(s). 141 | # 142 | def search(path, options={}) 143 | $LEDGER.search(path, options) 144 | end 145 | 146 | # 147 | # Search for all matching library files that match the given pattern. 148 | # This could be of useful for plugin loader. 149 | # 150 | # @param [Hash] options 151 | # Glob matching options. 152 | # 153 | # @option options [Boolean] :latest 154 | # Search only activated libraries or the most recent version 155 | # of a given library. 156 | # 157 | # @return [Array] Matching file paths. 158 | # 159 | # @todo Should this return list of Feature objects instead of file paths? 160 | # 161 | def glob(match, options={}) 162 | $LEDGER.glob(match, options) 163 | end 164 | 165 | # 166 | # @deprecated 167 | # 168 | def find_files(match, options={}) 169 | glob(match, options) 170 | end 171 | 172 | # 173 | # Access to global load stack. 174 | # When loading files, the current library doing the loading is pushed 175 | # on this stack, and then popped-off when it is finished. 176 | # 177 | # @return [Array] The `$LOAD_STACK` array. 178 | # 179 | def load_stack 180 | $LOAD_STACK 181 | end 182 | 183 | # 184 | # Require a feature from the library. 185 | # 186 | # @param [String] pathname 187 | # The pathname of feature relative to library's loadpath. 188 | # 189 | # @param [Hash] options 190 | # 191 | # @return [true,false] If feature was newly required or successfully loaded. 192 | # 193 | def require(pathname, options={}) 194 | $LEDGER.require(pathname, options) 195 | end 196 | 197 | # 198 | # Load file path. This is just like #require except that previously 199 | # loaded files will be reloaded and standard extensions will not be 200 | # automatically appended. 201 | # 202 | # @param pathname [String] 203 | # pathname of feature relative to library's loadpath 204 | # 205 | # @return [true,false] if feature was successfully loaded 206 | # 207 | def load(pathname, options={}) #, &block) 208 | $LEDGER.load(pathname, options) 209 | end 210 | 211 | # 212 | # Like require but also with local lookup. It will first check to see 213 | # if the currently loading library has the path relative to its load paths. 214 | # 215 | # acquire('core_ext/margin') 216 | # 217 | # To "load" the library, rather than "require" it, set the +:load+ 218 | # option to true. 219 | # 220 | # acquire('core_ext/string/margin', :load=>true) 221 | # 222 | # @param pathname [String] 223 | # Pathname of feature relative to library's loadpath. 224 | # 225 | # @return [true, false] If feature was newly required. 226 | # 227 | def acquire(pathname, options={}) #, &block) 228 | $LEDGER.acquire(pathname, options) 229 | end 230 | 231 | # ----------------- 232 | 233 | # 234 | # Lookup libraries that have a depenedency on the given library name 235 | # and version. 236 | # 237 | # @todo Does not yet handle version constraint. 238 | # @todo Sucky name. 239 | # 240 | # @return [Array] 241 | # 242 | def depends_upon(match_name) #, constraint) 243 | list = [] 244 | $LEDGER.each do |name, libs| 245 | case libs 246 | when Library 247 | list << libs if libs.requirements.any?{ |r| match_name == r['name'] } 248 | else 249 | libs.each do |lib| 250 | list << lib if lib.requirements.any?{ |r| match_name == r['name'] } 251 | end 252 | end 253 | end 254 | list 255 | end 256 | 257 | # 258 | # Go thru each library and collect bin paths. 259 | # 260 | # @todo Should this be defined on Ledger? 261 | # 262 | def PATH() 263 | path = [] 264 | list.each do |name| 265 | lib = Library[name] # TODO: This activates each library, probably not what we want, get max version instead? 266 | path << lib.bindir if lib.bindir? 267 | end 268 | path.join(windows_platform? ? ';' : ':') 269 | end 270 | 271 | # 272 | # Lock the ledger, by saving it to the temporary lock file. 273 | # 274 | # @todo Should we update the ledger first? 275 | # 276 | def lock 277 | output = lock_file 278 | 279 | dir = File.dirname(output) 280 | unless File.directory?(dir) 281 | FileUtils.mkdir_p(dir) 282 | end 283 | 284 | File.open(output, 'w+') do |f| 285 | f << $LEDGER.to_yaml 286 | end 287 | end 288 | 289 | # 290 | # Remove lock file and reset ledger. 291 | # 292 | def unlock 293 | FileUtils.rm(lock_file) if File.exist?(lock_file) 294 | reset! 295 | end 296 | 297 | # 298 | # Synchronize the ledger to the current system state and save. 299 | # Also, returns the bin paths of all libraries. 300 | # 301 | # @return [Array] List of bin paths. 302 | # 303 | def sync 304 | unlock if locked? 305 | lock 306 | PATH() 307 | end 308 | 309 | # 310 | # Library lock file. 311 | # 312 | # @return [String] Path to ledger lock file. 313 | # 314 | def lock_file 315 | File.join(tmpdir, "#{ruby_version}.ledger") 316 | end 317 | 318 | # 319 | # Check is `RUBY_LIBRARY_LIVE` environment variable is set on. 320 | # 321 | # @return [Booelan] Using live mode? 322 | # 323 | def live? 324 | case ENV['RUBY_LIBRARY_LIVE'].to_s.downcase 325 | when 'on', 'true', 'yes', 'y' 326 | true 327 | else 328 | false 329 | end 330 | end 331 | 332 | =begin 333 | # 334 | # Check is `RUBY_LIBRARY_DEVELOPMENT` environment variable is set on. 335 | # 336 | # @return [Booelan] Using development mode? 337 | # 338 | def development? 339 | case ENV['RUBY_LIBRARY_DEVELOPMENT'].to_s.downcase 340 | when 'on', 'true', 'yes', 'y' 341 | true 342 | else 343 | false 344 | end 345 | end 346 | =end 347 | 348 | # 349 | # Is there a saved locked ledger? 350 | # 351 | def locked? 352 | File.exist?(lock_file) 353 | end 354 | 355 | # 356 | # Reset the Ledger. 357 | # 358 | def reset! 359 | #$LEDGER = Ledger.new 360 | #$LOAD_STACK = [] 361 | $LOAD_CACHE = {} 362 | 363 | if File.exist?(lock_file) && ! live? 364 | ledger = YAML.load_file(lock_file) 365 | case ledger 366 | when Ledger 367 | $LEDGER = ledger 368 | return $LEDGER 369 | when Hash 370 | $LEDGER.replace(ledger) 371 | return $LEDGER 372 | else 373 | warn "Bad cached ledger at #{lock_file}" 374 | end 375 | end 376 | 377 | $LEDGER.prime(*lookup_paths, :expound=>true) 378 | 379 | #if development? 380 | # find project root 381 | # if root 382 | # $LEDGER.isolate_project(root) 383 | # end 384 | #end 385 | end 386 | 387 | private 388 | 389 | # 390 | # Bootstap the system, which is to say hit `#reset!` and 391 | # load the Kernel overrides. 392 | # 393 | def bootstrap! 394 | reset! 395 | require_relative 'kernel' 396 | end 397 | 398 | # 399 | # A temporary directory in which the locked ledger can be stored. 400 | # 401 | def tmpdir 402 | File.join(Dir.tmpdir, 'ruby') 403 | end 404 | 405 | # 406 | # Get an identifier for the current Ruby. This is taken from the `RUBY` 407 | # environment variable if it is set, otherwise the `RUBY_VERSION` constant 408 | # is returned. 409 | # 410 | # @return [String] Ruby version indentifier. 411 | # 412 | def ruby_version 413 | if ruby = ENV['RUBY'] 414 | File.basename(ruby) 415 | else 416 | RUBY_VERSION 417 | end 418 | end 419 | 420 | # 421 | # Library list file. 422 | # 423 | #def path_file 424 | # File.expand_path("~/.ruby/#{ruby_version}.path") 425 | # #File.expand_path('~/.ruby-path') 426 | #end 427 | 428 | # 429 | # List of paths where the lookup of libraries should proceed. 430 | # This come from the `RUBY_LIBRARY` environment variable, if set. 431 | # Otherwise it fallback to `GEM_PATH` or `GEM_HOME`. 432 | # 433 | def lookup_paths 434 | if list = ENV['RUBY_LIBRARY'] 435 | list.split(/[:;]/) 436 | #elsif File.exist?(path_file) 437 | # File.readlines(path_file).map{ |x| x.strip }.reject{ |x| x.empty? || x =~ /^\s*\#/ } 438 | elsif ENV['GEM_PATH'] 439 | ENV['GEM_PATH'].split(/[:;]/).map{ |dir| File.join(dir, 'gems', '*') } 440 | elsif ENV['GEM_HOME'] 441 | ENV['GEM_HOME'].split(/[:;]/).map{ |dir| File.join(dir, 'gems', '*') } 442 | else 443 | warn "No Ruby libraries." 444 | [] 445 | end 446 | end 447 | 448 | # 449 | # Is the current platform a Windows-based OS? 450 | # 451 | # @todo This is one of those methods that probably can always 452 | # use a little improvement. 453 | # 454 | def windows_platform? 455 | case RUBY_PLATFORM 456 | when /cygwin|mswin|mingw|bccwin|wince|emx/ 457 | true 458 | else 459 | false 460 | end 461 | end 462 | 463 | end 464 | 465 | extend Ledgered 466 | end 467 | -------------------------------------------------------------------------------- /lib/library/library.rb: -------------------------------------------------------------------------------- 1 | # Library class encapsulates a location on disc that contains a Ruby 2 | # project, with loadable features, of course. 3 | # 4 | class Library 5 | require 'library/core_ext' 6 | require 'library/index' 7 | require 'library/errors' 8 | require 'library/version' 9 | require 'library/metadata' 10 | require 'library/feature' 11 | require 'library/legacy_feature' 12 | 13 | # 14 | # Dynamic link extension. 15 | # 16 | #DLEXT = '.' + ::RbConfig::CONFIG['DLEXT'] 17 | 18 | # TODO: Some extensions are platform specific --only add the ones needed 19 | # for the current platform to SUFFIXES. 20 | 21 | # 22 | # Possible suffixes for feature files, that #require will try automatically. 23 | # 24 | SUFFIXES = ['.rb', '.rbw', '.so', '.bundle', '.dll', '.sl', '.jar'] #, ''] 25 | 26 | # 27 | # The opposite of SUFFIXES. Using this helps DRY-up the find_feature code. 28 | # 29 | SUFFIXES_NOT = [''] 30 | 31 | # 32 | # Extensions glob, joins extensions with comma and wrap in curly brackets. 33 | # 34 | SUFFIX_PATTERN = "{#{SUFFIXES.join(',')}}" 35 | 36 | # 37 | # New Library object. 38 | # 39 | # If data is given it must have `:name` and `:version`. It can 40 | # also have `:loadpath`, `:date`, and `:omit`. 41 | # 42 | # @param location [String] 43 | # Expanded file path to library's root directory. 44 | # 45 | # @param metadata [Hash] 46 | # Overriding matadata (to circumvent loading it from `.index` file). 47 | # 48 | def initialize(location, metadata={}) 49 | raise TypeError, "not a directory - #{location}" unless File.directory?(location) 50 | 51 | @location = location.to_s 52 | @metadata = Metadata.new(location, metadata) 53 | 54 | raise ValidationError, "Non-conforming library (missing name) -- `#{location}'" unless name 55 | raise ValidationError, "Non-conforming library (missing version) -- `#{location}'" unless version 56 | end 57 | 58 | # TODO: All instance method calling on $LEDGER should probably be moved the Ledger class 59 | # and called there. Looks like that would be #activate, #verify and #active? methods. 60 | 61 | # 62 | # Activate a library. 63 | # 64 | # @return [true,false] Has the library has been activated? 65 | # 66 | def activate 67 | current = $LEDGER[name] 68 | 69 | if Library === current 70 | raise VersionConflict.new(self, current) if current != self 71 | else 72 | ## NOTE: we are only doing this for the sake of autoload 73 | ## which does not honor a customized require method. 74 | #if Library.autoload_hack? 75 | # absolute_loadpath.each do |path| 76 | # $LOAD_PATH.unshift(path) 77 | # end 78 | #end 79 | $LEDGER[name] = self 80 | end 81 | 82 | # TODO: activate runtime requirements? 83 | #verify 84 | end 85 | 86 | # 87 | # Take requirements and activate them. This will reveal any 88 | # version conflicts or missing dependencies. 89 | # 90 | # @param [Boolean] development 91 | # Include development dependencies? 92 | # 93 | def verify(development=false) 94 | reqs = development ? requirements : runtime_requirements 95 | reqs.each do |req| 96 | name, constraint = req['name'], req['version'] 97 | Library.activate(name, constraint) 98 | end 99 | end 100 | 101 | # 102 | # Is this library active in global ledger? 103 | # 104 | def active? 105 | $LEDGER[name] == self 106 | end 107 | 108 | 109 | # 110 | # Location of library files on disc. 111 | # 112 | def location 113 | @location 114 | end 115 | 116 | # 117 | # Access to library metadata. Metadata is gathered from 118 | # the `.index` file or a `.gemspec` file. 119 | # 120 | # @return [Metadata] metadata object 121 | # 122 | def metadata 123 | @metadata 124 | end 125 | 126 | # 127 | # Library's "unixname". 128 | # 129 | # @return [String] name of library 130 | # 131 | def name 132 | @name ||= metadata.name 133 | end 134 | 135 | # 136 | # Library's version number. 137 | # 138 | # @return [VersionNumber] version number 139 | # 140 | def version 141 | @version ||= metadata.version 142 | end 143 | 144 | # 145 | # Library's internal load path(s). This will default to `['lib']` 146 | # if not otherwise given. 147 | # 148 | # @return [Array] list of load paths 149 | # 150 | def load_path 151 | metadata.load_path 152 | end 153 | 154 | alias_method :loadpath, :load_path 155 | 156 | # 157 | # Release date. 158 | # 159 | # @return [Time] library's release date 160 | # 161 | def date 162 | metadata.date 163 | end 164 | 165 | # 166 | # Alias for +#date+. 167 | # 168 | alias_method :released, :date 169 | 170 | # 171 | # Library's requirements. Note that in gemspec terminology these are 172 | # called *dependencies*. 173 | # 174 | # @return [Array] list of requirements 175 | # 176 | def requirements 177 | metadata.requirements || [] 178 | end 179 | 180 | # 181 | # Runtime requirements. 182 | # 183 | # @return [Array] list of runtime requirements 184 | # 185 | def runtime_requirements 186 | requirements.select{ |req| !req['development'] } 187 | end 188 | 189 | # 190 | # Omit library form ledger? 191 | # 192 | # @return [Boolean] if true, omit library from ledger 193 | # 194 | def omit 195 | @metadata.omit 196 | end 197 | 198 | # 199 | # Same as `#omit`. 200 | # 201 | alias_method :omit?, :omit 202 | 203 | # TODO: Should #load_path be absolute, and #relative_loadpath or some such be used instead? 204 | 205 | # 206 | # Returns a list of load paths expand to full path names. 207 | # 208 | # @return [Array] list of expanded load paths 209 | # 210 | def absolute_loadpath 211 | loadpath.map{ |lp| ::File.join(location, lp) } 212 | end 213 | 214 | # 215 | # Does a library contain a relative +file+ within it's loadpath. 216 | # If so return the libary file object for it, otherwise +false+. 217 | # 218 | # Note that this method was designed to maximize speed. 219 | # 220 | # @param [#to_s] file 221 | # The relative pathname of the file to find. 222 | # 223 | # @param [Hash] options 224 | # The Hash of optional settings to adjust search behavior. 225 | # 226 | # @option options [Boolean] :suffix 227 | # Automatically try standard extensions if pathname has none. 228 | # 229 | # @option options [Boolean] :legacy 230 | # (deprecated) Do not match within library's +name+ directory, eg. `lib/foo/*`. 231 | # 232 | # @return [Feature,nil] The feature, if found. 233 | # 234 | def find_feature(pathname, options={}) 235 | #legacy = options[:legacy] 236 | main = options[:main] 237 | suffix = options[:suffix] || options[:suffix].nil? 238 | #suffix = true if options[:require] # TODO: Is this always true? 239 | suffix = false if SUFFIXES.include?(::File.extname(pathname)) # TODO: Why not just add '' to SUFFIXES? 240 | 241 | suffixes = suffix ? SUFFIXES : SUFFIXES_NOT 242 | 243 | loadpath.each do |lpath| 244 | suffixes.each do |ext| 245 | f = ::File.join(location, lpath, pathname + ext) 246 | return feature(lpath, pathname, ext) if ::File.file?(f) 247 | end 248 | end #unless legacy 249 | 250 | legacy_loadpath.each do |lpath| 251 | suffixes.each do |ext| 252 | f = ::File.join(location, lpath, pathname + ext) 253 | return feature(lpath, pathname, ext) if ::File.file?(f) 254 | end 255 | end unless main 256 | 257 | nil 258 | end 259 | 260 | =begin 261 | def find_feature(pathname, options={}) 262 | main = options[:main] 263 | #legacy = options[:legacy] 264 | suffix = options[:suffix] || options[:suffix].nil? 265 | #suffix = true if options[:require] 266 | suffix = false if SUFFIXES.include?(::File.extname(pathname)) 267 | if suffix 268 | loadpath.each do |lpath| 269 | SUFFIXES.each do |ext| 270 | f = ::File.join(location, lpath, pathname + ext) 271 | return feature(lpath, pathname, ext) if ::File.file?(f) 272 | end 273 | end #unless legacy 274 | legacy_loadpath.each do |lpath| 275 | SUFFIXES.each do |ext| 276 | f = ::File.join(location, lpath, pathname + ext) 277 | return feature(lpath, pathname, ext) if ::File.file?(f) 278 | end 279 | end unless main 280 | else 281 | loadpath.each do |lpath| 282 | f = ::File.join(location, lpath, pathname) 283 | return feature(lpath, pathname) if ::File.file?(f) 284 | end #unless legacy 285 | legacy_loadpath.each do |lpath| 286 | f = ::File.join(location, lpath, pathname) 287 | return feature(lpath, pathname) if ::File.file?(f) 288 | end unless main 289 | end 290 | nil 291 | end 292 | =end 293 | 294 | # 295 | # Alias for #find_feature. 296 | # 297 | alias_method :include?, :find_feature 298 | 299 | # 300 | # 301 | # 302 | def legacy? 303 | !legacy_loadpath.empty? 304 | end 305 | 306 | # 307 | # What is `legacy_loadpath`? Well, library doesn't require you to put your 308 | # library's scripts in a named lib path, e.g. `lib/foo/`. Instead one can 309 | # just put them in `lib/` b/c Library keeps things indexed by honest to 310 | # goodness library names. The `legacy_path` then is used to handle these 311 | # old style paths along with the new. 312 | # 313 | def legacy_loadpath 314 | @legacy_loadpath ||= ( 315 | path = [] 316 | loadpath.each do |lp| 317 | llp = File.join(lp, name) 318 | dir = File.join(location, llp) 319 | path << llp if File.directory?(dir) 320 | end 321 | path 322 | ) 323 | end 324 | 325 | # 326 | # Create a new Feature object from +lpath+, +pathname+ and +ext+. 327 | # 328 | def feature(lpath, pathname, ext=nil) 329 | Feature.new(self, lpath, pathname, ext) 330 | end 331 | 332 | # 333 | # Requre feature from library. 334 | # 335 | def require(pathname, options={}) 336 | if feature = find_feature(pathname, options) 337 | feature.require(options) 338 | else 339 | raise LoadError.new(path, name) # TODO: silently? 340 | end 341 | end 342 | 343 | # 344 | # Load feature form library. 345 | # 346 | def load(pathname, options={}) 347 | if feature = find_feature(pathname, options) 348 | feature.load(options) 349 | else 350 | raise LoadError.new(pathname, self.name) 351 | end 352 | end 353 | 354 | # 355 | # Inspect library instance. 356 | # 357 | def inspect 358 | if version 359 | %[#] 360 | else 361 | %[#] 362 | end 363 | end 364 | 365 | # 366 | # Same as #inspect. 367 | # 368 | def to_s 369 | inspect 370 | end 371 | 372 | # 373 | # Compare by version. 374 | # 375 | def <=>(other) 376 | version <=> other.version 377 | end 378 | 379 | # 380 | # Return default feature. This is the feature that has same name as 381 | # the library itself. 382 | # 383 | def default_feature 384 | @default ||= find_feature(name, :main=>true) 385 | end 386 | 387 | # @deprecated 388 | alias_method :default, :default_feature 389 | 390 | #-- 391 | # # List of subdirectories that are searched when loading. 392 | # #-- 393 | # # This defualts to ['lib/{name}', 'lib']. The first entry is 394 | # # usually proper location; the latter is added for default 395 | # # compatability with the traditional require system. 396 | # #++ 397 | # def libdir 398 | # loadpath.map{ |path| ::File.join(location, path) } 399 | # end 400 | # 401 | # # Does the library have any lib directories? 402 | # def libdir? 403 | # lib.any?{ |d| ::File.directory?(d) } 404 | # end 405 | #++ 406 | 407 | # 408 | # Location of executable. This is alwasy bin/. This is a fixed 409 | # convention, unlike lib/ which needs to be more flexable. 410 | # 411 | def bindir 412 | ::File.join(location, 'bin') 413 | end 414 | 415 | # 416 | # Is there a `bin/` location? 417 | # 418 | def bindir? 419 | ::File.exist?(bindir) 420 | end 421 | 422 | # 423 | # Location of library system configuration files. 424 | # This is alwasy the `etc/` directory. 425 | # 426 | def confdir 427 | ::File.join(location, 'etc') 428 | end 429 | 430 | # 431 | # Is there a `etc/` location? 432 | # 433 | def confdir? 434 | ::File.exist?(confdir) 435 | end 436 | 437 | # Location of library shared data directory. 438 | # This is always the `data/` directory. 439 | def datadir 440 | ::File.join(location, 'data') 441 | end 442 | 443 | # Is there a `data/` location? 444 | def datadir? 445 | ::File.exist?(datadir) 446 | end 447 | 448 | # 449 | #def to_rb 450 | # to_h.inspect 451 | #end 452 | 453 | # 454 | # Convert to hash. 455 | # 456 | # @return [Hash] The library metadata in a hash. 457 | # 458 | def to_h 459 | { 460 | :location => location, 461 | :name => name, 462 | :version => version.to_s, 463 | :loadpath => loadpath, 464 | :date => date.to_s, 465 | :requirements => requirements 466 | } 467 | end 468 | 469 | module ::Kernel 470 | # 471 | # In which library is the current file participating? 472 | # 473 | # @return [Library] The library currently loading features. 474 | # 475 | def __LIBRARY__ 476 | $LOAD_STACK.last.library 477 | end 478 | 479 | # 480 | # Activate a library, same as `Library.instance` but will raise and error 481 | # if the library is not found. This can also take a block to yield on the 482 | # library. 483 | # 484 | # @param name [String] 485 | # The library's name. 486 | # 487 | # @param constraint [String] 488 | # A valid version constraint. 489 | # 490 | # @return [Library] The Library instance. 491 | # 492 | def library(name, constraint=nil, &block) #:yield: 493 | Library.activate(name, constraint, &block) 494 | end 495 | 496 | module_function :library 497 | end 498 | 499 | end 500 | -------------------------------------------------------------------------------- /lib/library/ledger.rb: -------------------------------------------------------------------------------- 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 ||= false) 19 | end 20 | 21 | # 22 | def initialize 23 | @table = Hash.new(){ |h,k| h[k] = [] } 24 | end 25 | 26 | # 27 | # Add library to the ledger given a library location or a Library object. 28 | # 29 | # @todo: Name of this method sucks, change to what? 30 | # 31 | # @param [String,Library] 32 | # The directory path to a ruby library or an instance of Library. 33 | # 34 | # @return [Library] Added library object. 35 | # 36 | def add(library) 37 | library = Library.new(library) unless Library === library 38 | 39 | #raise TypeError unless Library === library 40 | #begin 41 | entry = @table[library.name] 42 | 43 | if Array === entry 44 | entry << library unless entry.include?(library) 45 | else 46 | # Library is already active so compare and make sure they are the 47 | # same, otherwise warn. (Should this ever raise an error?) 48 | if entry != library # TODO: Is this the right equals comparison? 49 | warn "Added library has already been activated." 50 | end 51 | end 52 | #rescue Exception => error 53 | # warn error.message if ENV['debug'] 54 | #end 55 | 56 | library 57 | end 58 | 59 | # 60 | # Alias for #add method, but returns the ledger instead of the library. 61 | # 62 | def <<(library) 63 | add(library) 64 | self 65 | end 66 | 67 | # 68 | # Get library or library version set by name. 69 | # 70 | # @param [String] name 71 | # Name of library. 72 | # 73 | # @return [Library,Array] Library or lihbrary set referenced by name. 74 | # 75 | def [](name) 76 | @table[name.to_s] 77 | end 78 | 79 | # 80 | # Set ledger entry. 81 | # 82 | # @param [String] Name of library. 83 | # 84 | # @raise [TypeError] If library is not a Library object. 85 | # 86 | def []=(name, library) 87 | raise TypeError unless Library === library 88 | 89 | @table[name.to_s] = library 90 | end 91 | 92 | # 93 | # Replace current ledger with table from another. 94 | # 95 | def replace(ledger) 96 | case ledger 97 | when Ledger 98 | @table.replace(ledger.table) 99 | when Hash 100 | initialize # reinitialize 101 | ledger.each do |name, value| 102 | raise TypeError unless Array === value || Library === value 103 | @table[name.to_s] = value 104 | end 105 | else 106 | raise TypeError 107 | end 108 | end 109 | 110 | # 111 | # Iterate over each ledger entry. 112 | # 113 | def each(&block) 114 | @table.each(&block) 115 | end 116 | 117 | # 118 | # Get a copy of the underlying table. 119 | # 120 | def to_h 121 | @table.dup 122 | end 123 | 124 | # 125 | # Size of the ledger is the number of libraries available. 126 | # 127 | # @return [Fixnum] Size of the ledger. 128 | # 129 | def size 130 | @table.size 131 | end 132 | 133 | # TODO: Seems like Ledger#key?, #keys and #values should have better names. 134 | 135 | # 136 | # Checks ledger for presents of library by name. 137 | # 138 | # @return [Boolean] 139 | # 140 | def key?(name) 141 | @table.key?(name.to_s) 142 | end 143 | 144 | # 145 | # Get a list of names of all libraries in the ledger. 146 | # 147 | # @return [Array] list of library names 148 | # 149 | def keys 150 | @table.keys 151 | end 152 | 153 | # 154 | # Get a list of libraries and library version sets in the ledger. 155 | # 156 | # @return [Array] 157 | # List of libraries and library version sets. 158 | # 159 | def values 160 | @table.values 161 | end 162 | 163 | # 164 | # Inspection string. 165 | # 166 | # @return [String] Inspection string. 167 | # 168 | def inspect 169 | @table.inspect 170 | end 171 | 172 | # 173 | # Limit versions of a library to the given constraint. 174 | # Unlike `#activate` this does not reduce the possible versions 175 | # to a single library, but only reduces the number of possibilites. 176 | # 177 | # @param [String] name 178 | # Name of library. 179 | # 180 | # @param [String] constraint 181 | # Valid version constraint. 182 | # 183 | # @return [Array] List of conforming versions. 184 | # 185 | def constrain(name, constraint) 186 | name = name.to_s 187 | 188 | libraries = self[name] 189 | 190 | return nil unless Array === libraries 191 | 192 | vers = libraries.select do |library| 193 | library.version.satisfy?(constraint) 194 | end 195 | 196 | @table[name] = vers #self[name] = vers 197 | end 198 | 199 | # 200 | # Activate a library, retrieving a Library instance by name, or name 201 | # and version, and ensuring only that instance will be returned for 202 | # all subsequent requests. Libraries are singleton, so once activated 203 | # the same object is always returned. 204 | # 205 | # This method will raise a LoadError if the name is not found. 206 | # 207 | # Note that activating all runtime requirements of a library being 208 | # activated was considered, but decided against. There's no reason 209 | # to activate a library until it is actually needed. However this is 210 | # not so when testing, or verifying available requirements, so other 211 | # methods are provided such as `#activate_requirements`. 212 | # 213 | # @param [String] name 214 | # Name of library. 215 | # 216 | # @param [String] constraint 217 | # Valid version constraint. 218 | # 219 | # @return [Library] 220 | # The activated Library object. 221 | # 222 | # @todo Should we also check $"? Eg. `return false if $".include?(path)`. 223 | # 224 | def activate(name, constraint=nil) #:yield: 225 | raise LoadError, "no such library -- #{name}" unless key?(name) 226 | 227 | library = self[name] 228 | 229 | if Library === library 230 | if constraint 231 | unless library.version.satisfy?(constraint) 232 | raise Library::VersionConflict, library 233 | end 234 | end 235 | else # library is an array of versions 236 | if constraint 237 | #verscon = Version::Constraint.parse(constraint) 238 | #library = library.select{ |lib| verscon.compare(lib.version) }.max 239 | library = constrain(name, constraint).max 240 | else 241 | library = library.max 242 | end 243 | unless library 244 | raise VersionError, "no library version -- #{name} #{constraint}" 245 | end 246 | self[name] = library #constrain(library) 247 | end 248 | 249 | yield(library) if block_given? 250 | 251 | library 252 | end 253 | 254 | # 255 | # Load file path. This is just like #require except that previously 256 | # loaded files will be reloaded and standard extensions will not be 257 | # automatically appended. 258 | # 259 | # @param pathname [String] 260 | # pathname of feature relative to library's loadpath 261 | # 262 | # @return [true,false] if feature was successfully loaded 263 | # 264 | def load(pathname, options={}) 265 | unless Hash === options 266 | options = {} 267 | options[:wrap] = options 268 | end 269 | options = options.rekey 270 | 271 | #if feature = $LOAD_CACHE[pathname] 272 | # return feature.load if options[:load] 273 | # return false if feature.required? 274 | # return feature.require(options) 275 | #end 276 | 277 | if feature = find_feature(pathname, options) 278 | feature.load(options) 279 | else # fallback to Ruby's load system 280 | feature = LegacyFeature.new(pathname) 281 | $LOAD_CACHE[pathname] = feature 282 | success = feature.load(options) 283 | end 284 | end 285 | 286 | # 287 | # Require a feature from the library. 288 | # 289 | # @param [String] pathname 290 | # The pathname of feature relative to library's loadpath. 291 | # 292 | # @param [Hash] options 293 | # 294 | # @return [true,false] If feature was newly required or successfully loaded. 295 | # 296 | 297 | def require(pathname, options={}) 298 | options[:require] = true 299 | options[:suffix] = true 300 | 301 | load(pathname, options) 302 | 303 | #if file = $LOAD_CACHE[path] 304 | # return file.load 305 | #end 306 | 307 | #if file = Library.find(path, options) 308 | # #file.library_activate 309 | # $LOAD_CACHE[path] = file 310 | # return file.load(options) #acquire(options) 311 | #end 312 | 313 | ##if options[:load] 314 | # load_without_library(path, options[:wrap]) 315 | ##else 316 | ## require_without_library(path) 317 | ##end 318 | end 319 | 320 | # 321 | # Require from current library. 322 | # 323 | # @param pathname [String] 324 | # Pathname of feature relative to current library's loadpath. 325 | # 326 | # @return [true, false] If feature is newly required. 327 | # 328 | # @todo better name for `#require_local`? 329 | # 330 | def require_local(path, options={}) 331 | if feature = find_local_feature(path, options) 332 | $stderr.puts "#{path} (local)" if monitor? # debugging 333 | return feature.require(options) 334 | end 335 | end 336 | 337 | # 338 | # Require from current library. 339 | # 340 | # @param pathname [String] 341 | # Pathname of feature relative to current library's loadpath. 342 | # 343 | # @return [true, false] If feature is newly required. 344 | # 345 | alias_method :acquire, :require_local 346 | 347 | # 348 | # Find matching library features. This is the "mac daddy" method used by 349 | # the #require and #load methods to find the specified +path+ among 350 | # the various libraries and their load paths. 351 | # 352 | def find_feature(path, options={}) 353 | path = path.to_s 354 | 355 | #suffix = options[:suffix] 356 | #search = options[:search] 357 | legacy = options[:legacy] 358 | 359 | ftr = $LOAD_CACHE[path] 360 | 361 | return ftr if ftr 362 | 363 | $stderr.print path if monitor? # debugging 364 | 365 | # absolute, home or current path 366 | # 367 | # NOTE: Ideally we would try to find a matching path among avaliable libraries 368 | # so that the library can be activated, however this would add extra overhead 369 | # overhead and will by mostly a YAGNI, so we forgo this functionality, at least for now. 370 | case path[0,1] 371 | when '/', '~', '.' 372 | $stderr.puts " (absolute)" if monitor? # debugging 373 | # TODO: expand path and ensure it exists? 374 | ftr = LegacyFeature.new(path) 375 | $LOAD_CACHE[path] = ftr 376 | return ftr 377 | end 378 | 379 | # Look in user paths, there include -I and RUBYLIB environment locations, 380 | # as well as manually added paths to $LOAD_PATH. Very hackish stuff! 381 | if userpath = Utils.find_userpath(path, options) 382 | return LegacyFeature.new(userpath) 383 | end 384 | 385 | from, subpath = ::File.split_root(path) 386 | 387 | if from == 'ruby' # ruby hack 388 | $stderr.puts " (ruby)" if monitor? # debugging 389 | #lib = RubyLibrary.singleton 390 | if subpath 391 | ftr = find_library_feature('ruby', subpath, options) 392 | else 393 | # what the hell is just `load 'ruby'` ;) 394 | end 395 | else 396 | if lib = Library[from] # TODO: this activates, should it only do so if it has the feature? self[from].max instead? 397 | $stderr.puts " (from)" if monitor? # debugging 398 | if subpath # library name with subpath (path == from) 399 | ftr = lib.find_feature(path, options) || lib.find_feature(subpath, options) 400 | # TODO: activate library here? if ftr? 401 | else # just library name 402 | ftr = lib.default_feature 403 | # TODO: activate library here? if ftr? 404 | end 405 | end 406 | end 407 | 408 | return $LOAD_CACHE[path] = ftr if ftr 409 | 410 | # legacy brute force search 411 | # (This is very bad b/c it is the source of name clashes between libraries.) 412 | if legacy 413 | #options[:legacy] = true 414 | if ftr = find_any(path, options) 415 | $stderr.puts " (6 legacy search)" if monitor? # debugging 416 | return($LOAD_CACHE[path] = ftr) 417 | end 418 | end 419 | 420 | $stderr.puts " (7 fallback)" if monitor? # debugging 421 | 422 | nil 423 | end 424 | 425 | # 426 | # 427 | # 428 | def find_library_feature(lib, path, options={}) 429 | case lib 430 | when Library 431 | when :ruby, 'ruby' 432 | lib = RubyLibrary.singleton # sort of a hack to let rubygems edge in 433 | else # b/c if RubyLibary is in the regular ledger 434 | lib = self[lib] #library(lib) # then it prevents gems working for anything 435 | end # with the same name in ruby site locations. 436 | ftr = lib.find_feature(path, options) 437 | raise LoadError, "no such file to load -- #{path}" unless ftr 438 | $stderr.puts " (direct)" if monitor? # debugging 439 | # TODO: activate library here if ftr? 440 | return ftr 441 | end 442 | 443 | # 444 | # Find a feature from the currently loading library. 445 | # 446 | def find_local_feature(path, options={}) 447 | if lib = __LIBRARY__ 448 | if ftr = lib.find(path, options) 449 | return nil if $LOAD_STACK.include?(ftr) # prevent recursive loading 450 | return ftr 451 | end 452 | else 453 | # can this even happen? 454 | nil 455 | end 456 | end 457 | 458 | # 459 | # Brute force variation of `#find` looks through all libraries for a 460 | # matching features. This serves as the fallback method if `#find` comes 461 | # up empty. 462 | # 463 | # @param [String] path 464 | # path name for which to search 465 | # 466 | # @param [Hash] options 467 | # Search options. 468 | # 469 | # @option options [Boolean] :latest 470 | # Search only the active or most current version of any library. 471 | # 472 | # @option options [Boolean] :suffix 473 | # Automatically try standard extensions if pathname has none. 474 | # 475 | # @option options [Boolean] :legacy 476 | # Do not match within library's +name+ directory, eg. `lib/foo/*`. 477 | # 478 | # @return [Feature] Matching feature. 479 | # 480 | def find_any(path, options={}) 481 | options = options.merge(:main=>true) 482 | 483 | latest = options[:latest] 484 | 485 | # TODO: Perhaps the selected and unselected should be kept in separate lists? 486 | unselected, selected = *partition{ |name, libs| Array === libs } 487 | 488 | # broad search of pre-selected libraries 489 | selected.each do |(name, lib)| 490 | if ftr = lib.find(path, options) 491 | next if Library.load_stack.last == ftr 492 | return ftr 493 | end 494 | end 495 | 496 | # finally a broad search on unselected libraries 497 | unselected.each do |(name, libs)| 498 | libs = libs.sort 499 | libs = [libs.first] if latest 500 | libs.each do |lib| 501 | ftr = lib.find(path, options) 502 | return ftr if ftr 503 | end 504 | end 505 | 506 | nil 507 | end 508 | 509 | # 510 | # Brute force search looks through all libraries for matching features. 511 | # This is the same as #find_any, but returns a list of matches rather 512 | # then the first matching feature found. 513 | # 514 | # @param [String] path 515 | # path name for which to search 516 | # 517 | # @param [Hash] options 518 | # Search options. 519 | # 520 | # @option options [Boolean] :latest 521 | # Search only the active or most current version of any library. 522 | # 523 | # @option options [Boolean] :suffix 524 | # Automatically try standard extensions if pathname has none. 525 | # 526 | # @option options [Boolean] :legacy 527 | # Do not match within library's +name+ directory, eg. `lib/foo/*`. 528 | # 529 | # @return [Feature,Array] Matching feature(s). 530 | # 531 | def search(path, options={}) 532 | options = options.merge(:main=>true) 533 | 534 | latest = options[:latest] 535 | 536 | matches = [] 537 | 538 | # TODO: Perhaps the selected and unselected should be kept in separate lists? 539 | unselected, selected = *partition{ |name, libs| Array === libs } 540 | 541 | # broad search of pre-selected libraries 542 | selected.each do |(name, lib)| 543 | if ftr = lib.find(path, options) 544 | next if Library.load_stack.last == ftr 545 | matches << ftr 546 | end 547 | end 548 | 549 | # finally a broad search on unselected libraries 550 | unselected.each do |(name, libs)| 551 | libs = [libs.sort.first] if latest 552 | libs.each do |lib| 553 | ftr = lib.find(path, options) 554 | matches << ftr if ftr 555 | end 556 | end 557 | 558 | matches.uniq 559 | end 560 | 561 | # 562 | # Search for all matching library files that match the given pattern. 563 | # This could be of useful for plugin loader. 564 | # 565 | # @param [Hash] options 566 | # Glob matching options. 567 | # 568 | # @option options [Boolean] :latest 569 | # Search only activated libraries or the most recent version 570 | # of a given library. 571 | # 572 | # @return [Array] Matching file paths. 573 | # 574 | # @todo Should this return list of Feature objects instead of file paths? 575 | # 576 | def glob(match, options={}) 577 | latest = options[:latest] 578 | 579 | matches = [] 580 | 581 | each do |name, libs| 582 | case libs 583 | when Array 584 | libs = libs.sort 585 | libs = [libs.first] if latest 586 | else 587 | libs = [libs] 588 | end 589 | 590 | libs.each do |lib| 591 | lib.loadpath.each do |path| 592 | find = File.join(lib.location, path, match) 593 | list = Dir.glob(find) 594 | list = list.map{ |d| d.chomp('/') } 595 | matches.concat(list) 596 | end 597 | end 598 | end 599 | 600 | matches 601 | end 602 | 603 | # 604 | # Load up the ledger with a given set of paths and add an instance of 605 | # the special `RubyLibrary` class after that. 606 | # 607 | # @param [Array] paths 608 | # 609 | # @option paths [Boolean] :expound 610 | # Expound on path entires. See {#expound_paths}. 611 | # 612 | # @return [Ledger] The primed ledger. 613 | # 614 | def prime(*paths) 615 | options = Hash === paths.last ? paths.pop : {} 616 | 617 | paths = expound_paths(*paths) if options[:expound] 618 | 619 | #require 'library/rubylib' # TODO: What's the reason rubylib.rb is loaded here? 620 | 621 | paths.each do |path| 622 | begin 623 | add(path) if library_path?(path) 624 | rescue => err 625 | $stderr.puts err.message if ENV['debug'] 626 | end 627 | end 628 | 629 | # We can not do this b/c it prevents gems from working 630 | # when a file has the same name as something in the 631 | # ruby lib or site locations. For example, if we intsll 632 | # the test-unit gem and require `test/unit`. Of course, 633 | # it Ruby ever adopted the "Rolls Way" then this could 634 | # be restored. 635 | #add_library(RubyLibrary.new) 636 | 637 | self 638 | end 639 | 640 | # 641 | # Reduce the ledger to only those libraries the given library requires. 642 | # 643 | # @param [String] name 644 | # The name of the primary library. 645 | # 646 | # @param [String] constraint 647 | # The version constraint string. 648 | # 649 | # @return [Ledger] The ledger. 650 | # 651 | def isolate(name, constraint=nil) 652 | library = activate(name, constraint) 653 | 654 | # TODO: shouldn't this be done in #activate ? 655 | acivate_requirements(library) 656 | 657 | unused = [] 658 | each do |name, libs| 659 | ununsed << name if Array === libs 660 | end 661 | unused.each{ |name| @table.delete(name) } 662 | 663 | self 664 | end 665 | 666 | # 667 | # Add a library given it's path and reduce the ledger to just those libraries 668 | # the given library requires. 669 | # 670 | # @param [String] name 671 | # The name of the primary library. 672 | # 673 | # @param [String] constraint 674 | # The version constraint string. 675 | # 676 | # @return [Ledger] The ledger. 677 | # 678 | def isolate_project(root) 679 | library = add(root) 680 | # make this library the active one 681 | $LEDGER[library.name] = library 682 | # constrain all requirements 683 | constrain_requirements(library) 684 | # now activate all requirements 685 | activate_requirements(library) 686 | # remove any unused requirements 687 | unused = [] 688 | each do |name, libs| 689 | ununsed << name if Array === libs 690 | end 691 | unused.each{ |name| @table.delete(name) } 692 | # done 693 | library 694 | end 695 | 696 | protected 697 | 698 | # 699 | # Protected access to underlying table. 700 | # 701 | def table 702 | @table 703 | end 704 | 705 | private 706 | 707 | # 708 | # For flexible priming, this method is used to recursively 709 | # lookup library locations. 710 | # 711 | # If a given path is a file, it will considered a lookup *roll*, 712 | # such that each line entry in the file is considered another 713 | # path to be expounded upon. 714 | # 715 | # If a given path is a directory, it will be returned if it 716 | # is a valid Ruby library location. 717 | # 718 | # If it is a directory each of its subdirectories will be checked 719 | # to see if it is a valid Ruby library location, and returned if so. 720 | # 721 | # However, if the directory contains a subdirectory called `gems`, 722 | # that directory will be serache instead. This is done to support 723 | # RubyGems locations via the `$HOME_PATH`. 724 | # 725 | # If, on the other hand, a given path is a file glob pattern, 726 | # the pattern will be expanded and those paths will expounded 727 | # upon in turn. 728 | # 729 | # @param [Array] entries 730 | # File system paths to use to build list of library locations. 731 | # 732 | # @return [Array] List of verified library locations. 733 | # 734 | def expound_paths(*entries) 735 | paths = [] 736 | 737 | entries.each do |entry| 738 | entry = entry.strip 739 | 740 | next if entry.empty? 741 | next if entry.start_with?('#') 742 | 743 | if File.directory?(entry) 744 | if library_path?(entry) 745 | paths << entry 746 | else 747 | if File.directory?(File.join(entry, 'gems')) 748 | subpaths = Dir.glob(File.join(entry, 'gems/*/')) 749 | else 750 | subpaths = Dir.glob(File.join(entry, '*/')) 751 | end 752 | subpaths.each do |subpath| 753 | paths << subpath if library_path?(subpath) 754 | end 755 | end 756 | elsif File.file?(entry) 757 | paths.concat(expound_paths(*File.readlines(entry))) 758 | else 759 | glob_paths = Dir.glob(entry) 760 | if glob_paths.first != entry 761 | paths.concat(expound_paths(*glob_paths)) 762 | end 763 | end 764 | end 765 | 766 | paths 767 | end 768 | 769 | # 770 | # Is a directory a Ruby library? 771 | # 772 | # @todo Support gem home location. 773 | # 774 | def library_path?(path) 775 | #dotindex(path) || (ENV['RUBYLIBS_GEMSPEC'] && gemspec(path)) 776 | if ENV['RUBY_LIBRARY_NO_GEMS'] 777 | dotindex(path) 778 | else 779 | dotindex(path) || gemspec(path) 780 | end 781 | end 782 | 783 | # TODO: First recursively constrain the ledger, then activate. That way 784 | # any missing libraries will cause an error. (hmmm... actually that's 785 | # an imperfect way to resolve version dependencies). Ultimately we probably 786 | # need a separate module to handle this. 787 | 788 | # 789 | # Activate library requirements. 790 | # 791 | # @todo: checklist is used to prevent possible infinite recursion, but 792 | # it might be better to put a flag in Library instead. 793 | # 794 | def acivate_requirements(library, development=false, checklist={}) 795 | reqs = development ? library.requirements : library.runtime_requirements 796 | 797 | checklist[library] = true 798 | 799 | reqs.each do |req| 800 | name = req['name'] 801 | vers = req['version'] 802 | 803 | library = activate(name, vers) 804 | 805 | activate_requirements(library, development, checklist) unless checklist[library] 806 | end 807 | 808 | self 809 | end 810 | 811 | # 812 | # Constrain library requirements. 813 | # 814 | def constrain_requirements(library, development=false, checklist={}) 815 | reqs = development ? library.requirements : library.runtime_requirements 816 | 817 | checklist[library] = true 818 | 819 | reqs.each do |req| 820 | name = req['name'] 821 | vers = req['version'] 822 | 823 | libr = constrain(name, vers) 824 | 825 | constrain_requirements(library, development, checklist) unless checklist[libr] 826 | end 827 | 828 | return self 829 | end 830 | 831 | # 832 | # If the directory has a `.index` file return it, otherwise +nil+. 833 | # 834 | def dotindex(path) 835 | file = File.join(path, '.index') 836 | File.file?(file) ? file : false 837 | end 838 | 839 | alias :dotindex? :dotindex 840 | 841 | # TODO: Would it be faster to determine gem?(path) if we just compared it to $GEM_PATH? 842 | # If so, it might be faster, we could find the gemspec file from there. Right now this 843 | # code is too redundant, and thus slower than need be. 844 | 845 | # 846 | # Does a path have a `.gemspec` file? This is fallback measure 847 | # if a `.index` file is not found. 848 | # 849 | def gemspec(path) 850 | installed_gemspec(path) || local_gemspec(path) 851 | end 852 | 853 | alias :gemspec? :gemspec 854 | 855 | # 856 | # Does a path have a `.gemspec` file? 857 | # 858 | def local_gemspec(path) 859 | glob = File.join(path, '{,*}.gemspec') 860 | Dir[glob].first 861 | end 862 | 863 | # 864 | # Determine if a path belongs to an installed gem. If so, it returns the 865 | # path to the gemspec, otherwise +false+. 866 | # 867 | # @return [String, NilClass] Path to gemspec, or nil. 868 | # 869 | def installed_gemspec(path) 870 | #return true if Dir[File.join(path, '*.gemspec')].first 871 | pkgname = ::File.basename(path) 872 | gemsdir = ::File.dirname(path) 873 | specdir = ::File.join(File.dirname(gemsdir), 'specifications') 874 | gemspec = ::File.join(specdir, "#{pkgname}.gemspec") 875 | ::File.exist?(gemspec) ? gemspec : nil 876 | end 877 | 878 | end 879 | 880 | end 881 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Setup.rb, 2010-06-01 10:27:20 4 | # 5 | # This is a stand-alone bundle of the setup.rb application. 6 | # You can place it in your projects script/ directory, or 7 | # rename it to 'setup.rb' and place it in your project's 8 | # root directory (just like old times). 9 | # 10 | module Setup 11 | VERSION = '5.0.1' 12 | end 13 | class << File #:nodoc: all 14 | unless respond_to?(:read) # Ruby 1.6 and less 15 | def read(fname) 16 | open(fname){ |f| return f.read } 17 | end 18 | end 19 | def dir?(path) 20 | directory?((path[-1,1] == '/') ? path : path + '/') 21 | end 22 | end 23 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 24 | module Errno #:nodoc: 25 | class ENOTEMPTY #:nodoc: 26 | end 27 | end 28 | end 29 | module Setup 30 | META_EXTENSION_DIR = '.setup' 31 | FILETYPES = %w( bin lib ext data etc man doc ) 32 | INSTALL_RECORD = 'setup.receipt' 33 | CONFIG_FILE = 'setup.config' 34 | end 35 | module Setup 36 | class Project 37 | ROOT_MARKER = '{setup.rb,script/setup,meta/,MANIFEST,lib/}' 38 | def rootdir 39 | @rootdir ||= ( 40 | root = Dir[File.join(Dir.pwd, ROOT_MARKER)].first 41 | if !root 42 | raise Error, "not a project directory" 43 | else 44 | Dir.pwd 45 | end 46 | ) 47 | end 48 | def name 49 | @name = ( 50 | if file = Dir["{script/setup,meta,.meta}/name"].first 51 | File.read(file).strip 52 | else 53 | nil 54 | end 55 | ) 56 | end 57 | def loadpath 58 | @loadpath ||= ( 59 | if file = Dir.glob('{script/setup,meta,.meta}/loadpath').first 60 | raw = File.read(file).strip.chomp(']') 61 | raw.split(/[\n,]/).map do |e| 62 | e.strip.sub(/^[\[-]\s*/,'') 63 | end 64 | else 65 | nil 66 | end 67 | ) 68 | end 69 | def extconfs 70 | @extconfs ||= Dir['ext/**/extconf.rb'] 71 | end 72 | def extensions 73 | @extensions ||= extconfs.collect{ |f| File.dirname(f) } 74 | end 75 | def compiles? 76 | !extensions.empty? 77 | end 78 | end 79 | end 80 | module Setup 81 | class Session 82 | attr :options 83 | def initialize(options={}) 84 | @options = options 85 | self.io ||= StringIO.new # log instead ? 86 | end 87 | def io 88 | @options[:io] 89 | end 90 | def io=(anyio) 91 | @options[:io] = anyio 92 | end 93 | def trace?; @options[:trace]; end 94 | def trace=(val) 95 | @options[:trace] = val 96 | end 97 | def trial?; @options[:trial]; end 98 | def trial=(val) 99 | @options[:trial] = val 100 | end 101 | def quiet?; @options[:quiet]; end 102 | def quiet=(val) 103 | @options[:quiet] = val 104 | end 105 | def force?; @options[:force]; end 106 | def force=(val) 107 | @options[:force] = val 108 | end 109 | def compile? 110 | configuration.compile? && project.compiles? 111 | end 112 | def all 113 | if compile? 114 | make 115 | end 116 | if configuration.test? 117 | ok = test 118 | exit 1 unless ok 119 | end 120 | install 121 | if configuration.ri? 122 | document 123 | end 124 | end 125 | def config 126 | log_header('Custom Configuration') 127 | configuration.save_config 128 | io.puts "Edit #{CONFIG_FILE} to customize configuration." unless quiet? 129 | puts configuration if trace? && !quiet? 130 | compiler.configure if compile? #compiler.compiles? 131 | end 132 | def make 133 | log_header('Compile') 134 | compiler.compile 135 | end 136 | alias_method :setup, :make 137 | def install 138 | log_header('Install') 139 | installer.install 140 | end 141 | def test 142 | return true unless tester.testable? 143 | log_header('Test') 144 | tester.test 145 | end 146 | def document 147 | log_header('Document') 148 | documentor.document 149 | end 150 | def clean 151 | log_header('Clean') 152 | compiler.clean 153 | end 154 | def distclean 155 | log_header('Distclean') 156 | compiler.distclean 157 | end 158 | def uninstall 159 | if !File.exist?(INSTALL_RECORD) 160 | io.puts "Nothing is installed." 161 | return 162 | end 163 | log_header('Uninstall') 164 | uninstaller.uninstall 165 | io.puts('Ok.') 166 | end 167 | def show 168 | puts configuration 169 | end 170 | def project 171 | @project ||= Project.new 172 | end 173 | def configuration 174 | @configuration ||= Configuration.new 175 | end 176 | def compiler 177 | @compiler ||= Compiler.new(project, configuration, options) 178 | end 179 | def installer 180 | @installer ||= Installer.new(project, configuration, options) 181 | end 182 | def tester 183 | @tester ||= Tester.new(project, configuration, options) 184 | end 185 | def documentor 186 | @documentor ||= Documentor.new(project, configuration, options) 187 | end 188 | def uninstaller 189 | @uninstaller ||= Uninstaller.new(project, configuration, options) 190 | end 191 | def log_header(phase) 192 | return if quiet? 193 | if trial? 194 | str = "#{phase.upcase} (trail run)" 195 | else 196 | str = "#{phase.upcase}" 197 | end 198 | line = "- " * 35 199 | line[0..str.size+3] = str 200 | io.puts("\n- - #{line}\n\n") 201 | end 202 | end 203 | end 204 | module Setup 205 | class Base 206 | attr :project 207 | attr :config 208 | attr_accessor :trial 209 | attr_accessor :trace 210 | attr_accessor :quiet 211 | attr_accessor :force 212 | attr_accessor :io 213 | def initialize(project, configuration, options={}) 214 | @project = project 215 | @config = configuration 216 | initialize_hooks 217 | options.each do |k,v| 218 | __send__("#{k}=", v) if respond_to?("#{k}=") 219 | end 220 | end 221 | def initialize_hooks 222 | file = META_EXTENSION_DIR + "/#{self.class.name.downcase}.rb" 223 | if File.exist?(file) 224 | script = File.read(file) 225 | (class << self; self; end).class_eval(script) 226 | end 227 | end 228 | def trial? ; @trial ; end 229 | def trace? ; @trace ; end 230 | def quiet? ; @quiet ; end 231 | def force? ; @force ; end 232 | def rootdir 233 | project.rootdir 234 | end 235 | def bash(*args) 236 | $stderr.puts args.join(' ') if trace? 237 | system(*args) or raise RuntimeError, "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 238 | end 239 | alias_method :command, :bash 240 | def ruby(*args) 241 | bash(config.rubyprog, *args) 242 | end 243 | def trace_off #:yield: 244 | begin 245 | save, @trace = trace?, false 246 | yield 247 | ensure 248 | @trace = save 249 | end 250 | end 251 | def rm_f(path) 252 | io.puts "rm -f #{path}" if trace? or trial? 253 | return if trial? 254 | force_remove_file(path) 255 | end 256 | def force_remove_file(path) 257 | begin 258 | remove_file(path) 259 | rescue 260 | end 261 | end 262 | def remove_file(path) 263 | File.chmod 0777, path 264 | File.unlink(path) 265 | end 266 | def rmdir(path) 267 | io.puts "rmdir #{path}" if trace? or trial? 268 | return if trial? 269 | Dir.rmdir(path) 270 | end 271 | end 272 | class Error < StandardError 273 | end 274 | end 275 | module Setup 276 | class Compiler < Base 277 | def compiles? 278 | !extdirs.empty? 279 | end 280 | def configure 281 | extdirs.each do |dir| 282 | Dir.chdir(dir) do 283 | if File.exist?('extconf.rb') && !FileUtils.uptodate?('Makefile', ['extconf.rb']) 284 | ruby("extconf.rb") 285 | end 286 | end 287 | end 288 | end 289 | def compile 290 | extdirs.each do |dir| 291 | Dir.chdir(dir) do 292 | make 293 | end 294 | end 295 | end 296 | def clean 297 | extdirs.each do |dir| 298 | Dir.chdir(dir) do 299 | make('clean') 300 | end 301 | end 302 | end 303 | def distclean 304 | extdirs.each do |dir| 305 | Dir.chdir(dir) do 306 | make('distclean') 307 | end 308 | end 309 | end 310 | def extdirs 311 | Dir['ext/**/*/{MANIFEST,extconf.rb}'].map do |f| 312 | File.dirname(f) 313 | end.uniq 314 | end 315 | def make(task=nil) 316 | return unless File.exist?('Makefile') 317 | bash(*[config.makeprog, task].compact) 318 | end 319 | end 320 | end 321 | require 'rbconfig' 322 | require 'fileutils' 323 | require 'erb' 324 | require 'yaml' 325 | require 'shellwords' 326 | module Setup 327 | class Configuration 328 | RBCONFIG = ::RbConfig::CONFIG 329 | META_CONFIG_FILE = META_EXTENSION_DIR + '/configuration.rb' 330 | def self.options 331 | @@options ||= [] 332 | end 333 | def self.option(name, *args) #type, description) 334 | options << [name.to_s, *args] #type, description] 335 | attr_accessor(name) 336 | end 337 | option :prefix , :path, 'path prefix of target environment' 338 | option :bindir , :path, 'directory for commands' 339 | option :libdir , :path, 'directory for libraries' 340 | option :datadir , :path, 'directory for shared data' 341 | option :mandir , :path, 'directory for man pages' 342 | option :docdir , :path, 'directory for documentation' 343 | option :rbdir , :path, 'directory for ruby scripts' 344 | option :sodir , :path, 'directory for ruby extentions' 345 | option :sysconfdir , :path, 'directory for system configuration files' 346 | option :localstatedir , :path, 'directory for local state data' 347 | option :libruby , :path, 'directory for ruby libraries' 348 | option :librubyver , :path, 'directory for standard ruby libraries' 349 | option :librubyverarch , :path, 'directory for standard ruby extensions' 350 | option :siteruby , :path, 'directory for version-independent aux ruby libraries' 351 | option :siterubyver , :path, 'directory for aux ruby libraries' 352 | option :siterubyverarch , :path, 'directory for aux ruby binaries' 353 | option :rubypath , :prog, 'path to set to #! line' 354 | option :rubyprog , :prog, 'ruby program used for installation' 355 | option :makeprog , :prog, 'make program to compile ruby extentions' 356 | option :extconfopt , :opts, 'options to pass-thru to extconf.rb' 357 | option :shebang , :pick, 'shebang line (#!) editing mode (all,ruby,never)' 358 | option :no_test, :t , :bool, 'run pre-installation tests' 359 | option :no_ri, :d , :bool, 'generate ri documentation' 360 | option :no_doc , :bool, 'install doc/ directory' 361 | option :no_ext , :bool, 'compile/install ruby extentions' 362 | option :install_prefix , :path, 'install to alternate root location' 363 | option :root , :path, 'install to alternate root location' 364 | option :installdirs , :pick, 'install location mode (site,std,home)' #, local) 365 | option :type , :pick, 'install location mode (site,std,home)' 366 | ::RbConfig::CONFIG.each do |key,val| 367 | next if key == "configure_args" 368 | name = key.to_s.downcase 369 | define_method(name){ val } 370 | end 371 | config_args = Shellwords.shellwords(::RbConfig::CONFIG["configure_args"]) 372 | config_args.each do |ent| 373 | if ent.index("=") 374 | key, val = *ent.split("=") 375 | else 376 | key, val = ent, true 377 | end 378 | name = key.downcase 379 | name = name.sub(/^--/,'') 380 | name = name.gsub(/-/,'_') 381 | define_method(name){ val } 382 | end 383 | def options 384 | self.class.options 385 | end 386 | def initialize(values={}) 387 | initialize_metaconfig 388 | initialize_defaults 389 | initialize_environment 390 | initialize_configfile unless values[:reset] 391 | values.each{ |k,v| __send__("#{k}=", v) } 392 | yield(self) if block_given? 393 | end 394 | def initialize_metaconfig 395 | if File.exist?(META_CONFIG_FILE) 396 | script = File.read(META_CONFIG_FILE) 397 | (class << self; self; end).class_eval(script) 398 | end 399 | end 400 | def initialize_defaults 401 | self.type = 'site' 402 | self.no_ri = true 403 | self.no_test = true 404 | self.no_doc = true 405 | self.no_ext = false 406 | end 407 | def initialize_environment 408 | options.each do |name, *args| 409 | if value = ENV["RUBYSETUP_#{name.to_s.upcase}"] 410 | __send__("#{name}=", value) 411 | end 412 | end 413 | end 414 | def initialize_configfile 415 | if exist? 416 | erb = ERB.new(File.read(CONFIG_FILE)) 417 | txt = erb.result(binding) 418 | dat = YAML.load(txt) 419 | dat.each do |k, v| 420 | next if 'type' == k 421 | next if 'installdirs' == k 422 | k = k.gsub('-','_') 423 | __send__("#{k}=", v) 424 | end 425 | if dat['type'] 426 | self.type = dat['type'] 427 | end 428 | if dat['installdirs'] 429 | self.installdirs = dat['installdirs'] 430 | end 431 | end 432 | end 433 | attr_accessor :reset 434 | def base_bindir 435 | @base_bindir ||= subprefix('bindir') 436 | end 437 | def base_libdir 438 | @base_libdir ||= subprefix('libdir') 439 | end 440 | def base_datadir 441 | @base_datadir ||= subprefix('datadir') 442 | end 443 | def base_mandir 444 | @base_mandir ||= subprefix('mandir') 445 | end 446 | def base_docdir 447 | @base_docdir || File.dirname(subprefix('docdir')) 448 | end 449 | def base_rubylibdir 450 | @rubylibdir ||= subprefix('rubylibdir') 451 | end 452 | def base_rubyarchdir 453 | @base_rubyarchdir ||= subprefix('archdir') 454 | end 455 | def base_sysconfdir 456 | @base_sysconfdir ||= subprefix('sysconfdir') 457 | end 458 | def base_localstatedir 459 | @base_localstatedir ||= subprefix('localstatedir') 460 | end 461 | def type 462 | @type ||= 'site' 463 | end 464 | def type=(val) 465 | @type = val 466 | case val.to_s 467 | when 'std', 'ruby' 468 | @rbdir = librubyver #'$librubyver' 469 | @sodir = librubyverarch #'$librubyverarch' 470 | when 'site' 471 | @rbdir = siterubyver #'$siterubyver' 472 | @sodir = siterubyverarch #'$siterubyverarch' 473 | when 'home' 474 | self.prefix = File.join(home, '.local') # TODO: Use XDG 475 | @rbdir = nil #'$libdir/ruby' 476 | @sodir = nil #'$libdir/ruby' 477 | else 478 | raise Error, "bad config: use type=(std|site|home) [#{val}]" 479 | end 480 | end 481 | alias_method :installdirs, :type 482 | alias_method :installdirs=, :type= 483 | alias_method :install_prefix, :root 484 | alias_method :install_prefix=, :root= 485 | def prefix 486 | @prefix ||= RBCONFIG['prefix'] 487 | end 488 | def prefix=(path) 489 | @prefix = pathname(path) 490 | end 491 | def libruby 492 | @libruby ||= RBCONFIG['prefix'] + "/lib/ruby" 493 | end 494 | def libruby=(path) 495 | path = pathname(path) 496 | @librubyver = librubyver.sub(libruby, path) 497 | @librubyverarch = librubyverarch.sub(libruby, path) 498 | @libruby = path 499 | end 500 | def librubyver 501 | @librubyver ||= RBCONFIG['rubylibdir'] 502 | end 503 | def librubyver=(path) 504 | @librubyver = pathname(path) 505 | end 506 | def librubyverarch 507 | @librubyverarch ||= RBCONFIG['archdir'] 508 | end 509 | def librubyverarch=(path) 510 | @librubyverarch = pathname(path) 511 | end 512 | def siteruby 513 | @siteruby ||= RBCONFIG['sitedir'] 514 | end 515 | def siteruby=(path) 516 | path = pathname(path) 517 | @siterubyver = siterubyver.sub(siteruby, path) 518 | @siterubyverarch = siterubyverarch.sub(siteruby, path) 519 | @siteruby = path 520 | end 521 | def siterubyver 522 | @siterubyver ||= RBCONFIG['sitelibdir'] 523 | end 524 | def siterubyver=(path) 525 | @siterubyver = pathname(path) 526 | end 527 | def siterubyverarch 528 | @siterubyverarch ||= RBCONFIG['sitearchdir'] 529 | end 530 | def siterubyverarch=(path) 531 | @siterubyverarch = pathname(path) 532 | end 533 | def bindir 534 | @bindir || File.join(prefix, base_bindir) 535 | end 536 | def bindir=(path) 537 | @bindir = pathname(path) 538 | end 539 | def libdir 540 | @libdir || File.join(prefix, base_libdir) 541 | end 542 | def libdir=(path) 543 | @libdir = pathname(path) 544 | end 545 | def datadir 546 | @datadir || File.join(prefix, base_datadir) 547 | end 548 | def datadir=(path) 549 | @datadir = pathname(path) 550 | end 551 | def mandir 552 | @mandir || File.join(prefix, base_mandir) 553 | end 554 | def mandir=(path) 555 | @mandir = pathname(path) 556 | end 557 | def docdir 558 | @docdir || File.join(prefix, base_docdir) 559 | end 560 | def docdir=(path) 561 | @docdir = pathname(path) 562 | end 563 | def rbdir 564 | @rbdir || File.join(prefix, base_rubylibdir) 565 | end 566 | def sodir 567 | @sodir || File.join(prefix, base_rubyarchdir) 568 | end 569 | def sysconfdir 570 | @sysconfdir ||= base_sysconfdir 571 | end 572 | def sysconfdir=(path) 573 | @sysconfdir = pathname(path) 574 | end 575 | def localstatedir 576 | @localstatedir ||= base_localstatedir 577 | end 578 | def localstatedir=(path) 579 | @localstatedir = pathname(path) 580 | end 581 | def rubypath 582 | @rubypath ||= File.join(RBCONFIG['bindir'], RBCONFIG['ruby_install_name'] + RBCONFIG['EXEEXT']) 583 | end 584 | def rubypath=(path) 585 | @rubypath = pathname(path) 586 | end 587 | def rubyprog 588 | @rubyprog || rubypath 589 | end 590 | def rubyprog=(command) 591 | @rubyprog = command 592 | end 593 | def makeprog 594 | @makeprog ||= ( 595 | if arg = RBCONFIG['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 596 | arg.sub(/'/, '').split(/=/, 2)[1] 597 | else 598 | 'make' 599 | end 600 | ) 601 | end 602 | def makeprog=(command) 603 | @makeprog = command 604 | end 605 | def extconfopt 606 | @extconfopt ||= '' 607 | end 608 | def extconfopt=(string) 609 | @extconfopt = string 610 | end 611 | def shebang 612 | @shebang ||= 'ruby' 613 | end 614 | def shebang=(val) 615 | if %w(all ruby never).include?(val) 616 | @shebang = val 617 | else 618 | raise Error, "bad config: use SHEBANG=(all|ruby|never) [#{val}]" 619 | end 620 | end 621 | def no_ext 622 | @no_ext 623 | end 624 | def no_ext=(val) 625 | @no_ext = boolean(val) 626 | end 627 | def no_test 628 | @no_test 629 | end 630 | def no_test=(val) 631 | @no_test = boolean(val) 632 | end 633 | def no_doc 634 | @no_doc 635 | end 636 | def no_doc=(val) 637 | @no_doc = boolean(val) 638 | end 639 | def no_ri 640 | @no_ri 641 | end 642 | def no_ri=(val) 643 | @no_ri = boolean(val) 644 | end 645 | def compile? 646 | !no_ext 647 | end 648 | def test? 649 | !no_test 650 | end 651 | def ri? 652 | !no_ri 653 | end 654 | def doc? 655 | !no_doc 656 | end 657 | def to_h 658 | h = {} 659 | options.each do |name, *args| 660 | h[name.to_s] = __send__(name) 661 | end 662 | h 663 | end 664 | def to_s 665 | to_yaml.sub(/\A---\s*\n/,'') 666 | end 667 | def to_yaml(*args) 668 | to_h.to_yaml(*args) 669 | end 670 | def save_config 671 | out = to_yaml 672 | if not File.exist?(File.dirname(CONFIG_FILE)) 673 | FileUtils.mkdir_p(File.dirname(CONFIG_FILE)) 674 | end 675 | if File.exist?(CONFIG_FILE) 676 | txt = File.read(CONFIG_FILE) 677 | return nil if txt == out 678 | end 679 | File.open(CONFIG_FILE, 'w'){ |f| f << out } 680 | true 681 | end 682 | def exist? 683 | File.exist?(CONFIG_FILE) 684 | end 685 | private 686 | def pathname(path) 687 | path.gsub(%r<\\$([^/]+)>){ self[$1] } 688 | end 689 | def boolean(val, name=nil) 690 | case val 691 | when true, false, nil 692 | val 693 | else 694 | case val.to_s.downcase 695 | when 'y', 'yes', 't', 'true' 696 | true 697 | when 'n', 'no', 'f', 'false' 698 | false 699 | else 700 | raise Error, "bad config: use --#{name}=(yes|no) [\#{val}]" 701 | end 702 | end 703 | end 704 | def subprefix(path, with='') 705 | val = RBCONFIG[path] 706 | raise "Unknown path -- #{path}" if val.nil? 707 | prefix = Regexp.quote(RBCONFIG['prefix']) 708 | val.sub(/\A#{prefix}/, with) 709 | end 710 | def home 711 | ENV['HOME'] || raise(Error, 'HOME is not set.') 712 | end 713 | end #class ConfigTable 714 | end #module Setup 715 | =begin 716 | def inintialize_metaconfig 717 | path = Dir.glob(METACONFIG_FILE).first 718 | if path && File.file?(path) 719 | MetaConfigEnvironment.new(self).instance_eval(File.read(path), path) 720 | end 721 | end 722 | class MetaConfigEnvironment 723 | def initialize(config) #, installer) 724 | @config = config 725 | end 726 | def config_names 727 | @config.descriptions.collect{ |n, t, d| n.to_s } 728 | end 729 | def config?(name) 730 | @config.descriptions.find do |sym, type, desc| 731 | sym.to_s == name.to_s 732 | end 733 | end 734 | def bool_config?(name) 735 | @config.descriptions.find do |sym, type, desc| 736 | sym.to_s == name.to_s && type == :bool 737 | end 738 | end 739 | def path_config?(name) 740 | @config.descriptions.find do |sym, type, desc| 741 | sym.to_s == name.to_s && type == :path 742 | end 743 | end 744 | def value_config?(name) 745 | @config.descriptions.find do |sym, type, desc| 746 | sym.to_s == name.to_s && type != :prog 747 | end 748 | end 749 | def add_config(name, default, desc) 750 | @config.descriptions << [name.to_sym, nil, desc] 751 | end 752 | def add_bool_config(name, default, desc) 753 | @config.descriptions << [name.to_sym, :bool, desc] 754 | end 755 | def add_path_config(name, default, desc) 756 | @config.descriptions << [name.to_sym, :path, desc] 757 | end 758 | def set_config_default(name, default) 759 | @config[name] = default 760 | end 761 | def remove_config(name) 762 | item = @config.descriptions.find do |sym, type, desc| 763 | sym.to_s == name.to_s 764 | end 765 | index = @config.descriptions.index(item) 766 | @config.descriptions.delete(index) 767 | end 768 | end 769 | =end 770 | module Setup 771 | class Documentor < Base 772 | def document 773 | return if config.no_doc 774 | exec_ri 775 | end 776 | def exec_ri 777 | case config.type #installdirs 778 | when 'std', 'ruby' 779 | output = "--ri-site" 780 | when 'site' 781 | output = "--ri-site" 782 | when 'home' 783 | output = "--ri" 784 | else 785 | abort "bad config: should not be possible -- type=#{config.type}" 786 | end 787 | if File.exist?('.document') 788 | files = File.read('.document').split("\n") 789 | files.reject!{ |l| l =~ /^\s*[#]/ || l !~ /\S/ } 790 | files.collect!{ |f| f.strip } 791 | else 792 | files = [] 793 | files << 'lib' if File.directory?('lib') 794 | files << 'ext' if File.directory?('ext') 795 | end 796 | opt = [] 797 | opt << "-U" 798 | opt << "-q" #if quiet? 799 | opt << output 800 | opt << files 801 | opt = opt.flatten 802 | cmd = "rdoc " + opt.join(' ') 803 | if trial? 804 | puts cmd 805 | else 806 | begin 807 | success = system(cmd) 808 | raise unless success 809 | io.puts "Ok ri." #unless quiet? 810 | rescue Exception 811 | $stderr.puts "ri generation failed" 812 | $stderr.puts "command was: '#{cmd}'" 813 | end 814 | end 815 | end 816 | def exec_rdoc 817 | main = Dir.glob("README{,.*}", File::FNM_CASEFOLD).first 818 | if File.exist?('.document') 819 | files = File.read('.document').split("\n") 820 | files.reject!{ |l| l =~ /^\s*[#]/ || l !~ /\S/ } 821 | files.collect!{ |f| f.strip } 822 | else 823 | files = [] 824 | files << main if main 825 | files << 'lib' if File.directory?('lib') 826 | files << 'ext' if File.directory?('ext') 827 | end 828 | checkfiles = (files + files.map{ |f| Dir[File.join(f,'*','**')] }).flatten.uniq 829 | if FileUtils.uptodate?('doc/rdoc', checkfiles) 830 | puts "RDocs look current." 831 | return 832 | end 833 | output = 'doc/rdoc' 834 | title = (PACKAGE.capitalize + " API").strip if PACKAGE 835 | template = config.doctemplate || 'html' 836 | opt = [] 837 | opt << "-U" 838 | opt << "-q" #if quiet? 839 | opt << "--op=#{output}" 840 | opt << "--title=#{title}" 841 | opt << "--main=#{main}" if main 842 | opt << files 843 | opt = opt.flatten 844 | cmd = "rdoc " + opt.join(' ') 845 | if trial? 846 | puts cmd 847 | else 848 | begin 849 | system(cmd) 850 | puts "Ok rdoc." unless quiet? 851 | rescue Exception 852 | puts "Fail rdoc." 853 | puts "Command was: '#{cmd}'" 854 | puts "Proceeding with install anyway." 855 | end 856 | end 857 | end 858 | end 859 | end 860 | module Setup 861 | class Installer < Base 862 | def install_prefix 863 | config.install_prefix 864 | end 865 | def install 866 | Dir.chdir(rootdir) do 867 | install_bin 868 | install_ext 869 | install_lib 870 | install_data 871 | install_man 872 | install_doc 873 | install_etc 874 | prune_install_record 875 | end 876 | end 877 | def install_bin 878 | return unless directory?('bin') 879 | report_transfer('bin', config.bindir) 880 | files = files('bin') 881 | install_files('bin', files, config.bindir, 0755) 882 | end 883 | def install_ext 884 | return unless directory?('ext') 885 | report_transfer('ext', config.sodir) 886 | files = files('ext') 887 | files = select_dllext(files) 888 | files.each do |file| 889 | name = File.join(File.dirname(File.dirname(file)), File.basename(file)) 890 | dest = destination(config.sodir, name) 891 | install_file('ext', file, dest, 0555, install_prefix) 892 | end 893 | end 894 | def install_lib 895 | return unless directory?('lib') 896 | report_transfer('lib', config.rbdir) 897 | files = files('lib') 898 | install_files('lib', files, config.rbdir, 0644) 899 | end 900 | def install_data 901 | return unless directory?('data') 902 | report_transfer('data', config.datadir) 903 | files = files('data') 904 | install_files('data', files, config.datadir, 0644) 905 | end 906 | def install_etc 907 | return unless directory?('etc') 908 | report_transfer('etc', config.sysconfdir) 909 | files = files('etc') 910 | install_files('etc', files, config.sysconfdir, 0644) 911 | end 912 | def install_man 913 | return unless directory?('man') 914 | report_transfer('man', config.mandir) 915 | files = files('man') 916 | install_files('man', files, config.mandir, 0644) 917 | end 918 | def install_doc 919 | return unless config.doc? 920 | return unless directory?('doc') 921 | return unless project.name 922 | dir = File.join(config.docdir, "ruby-#{project.name}") 923 | report_transfer('doc', dir) 924 | files = files('doc') 925 | install_files('doc', files, dir, 0644) 926 | end 927 | private 928 | def report_transfer(source, directory) 929 | unless quiet? 930 | if install_prefix 931 | out = File.join(install_prefix, directory) 932 | else 933 | out = directory 934 | end 935 | io.puts "* #{source} -> #{out}" 936 | end 937 | end 938 | def directory?(path) 939 | File.directory?(path) 940 | end 941 | def files(dir) 942 | files = Dir["#{dir}/**/*"] 943 | files = files.select{ |f| File.file?(f) } 944 | files = files.map{ |f| f.sub("#{dir}/", '') } 945 | files 946 | end 947 | def select_dllext(files) 948 | ents = files.select do |file| 949 | File.extname(file) == ".#{dllext}" 950 | end 951 | if ents.empty? && !files.empty? 952 | raise Error, "ruby extention not compiled: 'setup.rb make' first" 953 | end 954 | ents 955 | end 956 | def dllext 957 | config.dlext 958 | end 959 | def install_files(dir, list, dest, mode) 960 | list.each do |fname| 961 | rdest = destination(dest, fname) 962 | install_file(dir, fname, rdest, mode, install_prefix) 963 | end 964 | end 965 | def install_file(dir, from, dest, mode, prefix=nil) 966 | mkdir_p(File.dirname(dest)) 967 | if trace? or trial? 968 | io.puts "install #{dir}/#{from} #{dest}" 969 | end 970 | return if trial? 971 | str = binread(File.join(dir, from)) 972 | if diff?(str, dest) 973 | trace_off { 974 | rm_f(dest) if File.exist?(dest) 975 | } 976 | File.open(dest, 'wb'){ |f| f.write(str) } 977 | File.chmod(mode, dest) 978 | end 979 | record_installation(dest) # record file as installed 980 | end 981 | def mkdir_p(dirname) #, prefix=nil) 982 | return if File.directory?(dirname) 983 | io.puts "mkdir -p #{dirname}" if trace? or trial? 984 | return if trial? 985 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 986 | if /\A[a-z]:\z/i =~ dirs[0] 987 | disk = dirs.shift 988 | dirs[0] = disk + dirs[0] 989 | end 990 | dirs.each_index do |idx| 991 | path = dirs[0..idx].join('') 992 | unless File.dir?(path) 993 | Dir.mkdir(path) 994 | end 995 | record_installation(path) # record directories made 996 | end 997 | end 998 | def record_installation(path) 999 | File.open(install_record, 'a') do |f| 1000 | f.puts(path) 1001 | end 1002 | end 1003 | def prune_install_record 1004 | entries = File.read(install_record).split("\n") 1005 | entries.uniq! 1006 | File.open(install_record, 'w') do |f| 1007 | f << entries.join("\n") 1008 | f << "\n" 1009 | end 1010 | end 1011 | def install_record 1012 | @install_record ||= ( 1013 | file = INSTALL_RECORD 1014 | dir = File.dirname(file) 1015 | unless File.directory?(dir) 1016 | FileUtils.mkdir_p(dir) 1017 | end 1018 | file 1019 | ) 1020 | end 1021 | def destination(dir, file) 1022 | dest = install_prefix ? File.join(install_prefix, File.expand_path(dir)) : dir 1023 | dest = File.join(dest, file) #if File.dir?(dest) 1024 | dest = File.expand_path(dest) 1025 | dest 1026 | end 1027 | def diff?(new_content, path) 1028 | return true unless File.exist?(path) 1029 | new_content != binread(path) 1030 | end 1031 | def binread(fname) 1032 | File.open(fname, 'rb') do |f| 1033 | return f.read 1034 | end 1035 | end 1036 | def install_shebang(files, dir) 1037 | files.each do |file| 1038 | path = File.join(dir, File.basename(file)) 1039 | update_shebang_line(path) 1040 | end 1041 | end 1042 | def update_shebang_line(path) 1043 | return if trial? 1044 | return if config.shebang == 'never' 1045 | old = Shebang.load(path) 1046 | if old 1047 | if old.args.size > 1 1048 | $stderr.puts "warning: #{path}" 1049 | $stderr.puts "Shebang line has too many args." 1050 | $stderr.puts "It is not portable and your program may not work." 1051 | end 1052 | new = new_shebang(old) 1053 | return if new.to_s == old.to_s 1054 | else 1055 | return unless config.shebang == 'all' 1056 | new = Shebang.new(config.rubypath) 1057 | end 1058 | $stderr.puts "updating shebang: #{File.basename(path)}" if trace? 1059 | open_atomic_writer(path) do |output| 1060 | File.open(path, 'rb') do |f| 1061 | f.gets if old # discard 1062 | output.puts new.to_s 1063 | output.print f.read 1064 | end 1065 | end 1066 | end 1067 | def new_shebang(old) 1068 | if /\Aruby/ =~ File.basename(old.cmd) 1069 | Shebang.new(config.rubypath, old.args) 1070 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1071 | Shebang.new(config.rubypath, old.args[1..-1]) 1072 | else 1073 | return old unless config.shebang == 'all' 1074 | Shebang.new(config.rubypath) 1075 | end 1076 | end 1077 | def open_atomic_writer(path, &block) 1078 | tmpfile = File.basename(path) + '.tmp' 1079 | begin 1080 | File.open(tmpfile, 'wb', &block) 1081 | File.rename tmpfile, File.basename(path) 1082 | ensure 1083 | File.unlink tmpfile if File.exist?(tmpfile) 1084 | end 1085 | end 1086 | class Shebang 1087 | def Shebang.load(path) 1088 | line = nil 1089 | File.open(path) {|f| 1090 | line = f.gets 1091 | } 1092 | return nil unless /\A#!/ =~ line 1093 | parse(line) 1094 | end 1095 | def Shebang.parse(line) 1096 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1097 | new(cmd, args) 1098 | end 1099 | def initialize(cmd, args = []) 1100 | @cmd = cmd 1101 | @args = args 1102 | end 1103 | attr_reader :cmd 1104 | attr_reader :args 1105 | def to_s 1106 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1107 | end 1108 | end 1109 | end 1110 | end 1111 | module Setup 1112 | class Tester < Base 1113 | RUBYSCRIPT = META_EXTENSION_DIR + '/testrc.rb' 1114 | SHELLSCRIPT = 'script/test' 1115 | def testable? 1116 | return false if config.no_test 1117 | return true if File.exist?(RUBYSCRIPT) 1118 | return true if File.exist?(SHELLSCRIPT) 1119 | false 1120 | end 1121 | def test 1122 | return true if !testable? 1123 | if File.exist?(RUBYSCRIPT) 1124 | test_rubyscript 1125 | elsif File.exist?(SHELLSCRIPT) 1126 | test_shellscript 1127 | end 1128 | end 1129 | def test_shellscript 1130 | bash(SHELLSCRIPT) 1131 | end 1132 | def test_rubyscript 1133 | ruby(RUBYSCRIPT) 1134 | end 1135 | end 1136 | end 1137 | module Setup 1138 | class Uninstaller < Base 1139 | def uninstall 1140 | return unless File.exist?(INSTALL_RECORD) 1141 | files = [] 1142 | dirs = [] 1143 | paths.each do |path| 1144 | dirs << path if File.dir?(path) 1145 | files << path if File.file?(path) 1146 | end 1147 | if dirs.empty? && files.empty? 1148 | io.outs "Nothing to remove." 1149 | return 1150 | end 1151 | files.sort!{ |a,b| b.size <=> a.size } 1152 | dirs.sort!{ |a,b| b.size <=> a.size } 1153 | if !force? && !trial? 1154 | puts (files + dirs).collect{ |f| "#{f}" }.join("\n") 1155 | puts 1156 | puts "Must use --force option to remove these files and directories that become empty." 1157 | return 1158 | end 1159 | files.each do |file| 1160 | rm_f(file) 1161 | end 1162 | dirs.each do |dir| 1163 | entries = Dir.entries(dir) 1164 | entries.delete('.') 1165 | entries.delete('..') 1166 | rmdir(dir) if entries.empty? 1167 | end 1168 | rm_f(INSTALL_RECORD) 1169 | end 1170 | private 1171 | def paths 1172 | @paths ||= ( 1173 | lines = File.read(INSTALL_RECORD).split(/\s*\n/) 1174 | lines = lines.map{ |line| line.strip } 1175 | lines = lines.uniq 1176 | lines = lines.reject{ |line| line.empty? } # skip blank lines 1177 | lines = lines.reject{ |line| line[0,1] == '#' } # skip blank lines 1178 | lines 1179 | ) 1180 | end 1181 | end 1182 | end 1183 | require 'optparse' 1184 | module Setup 1185 | class Command 1186 | def self.run(*argv) 1187 | new.run(*argv) 1188 | end 1189 | def self.tasks 1190 | @tasks ||= {} 1191 | end 1192 | def self.order 1193 | @order ||= [] 1194 | end 1195 | def self.task(name, description) 1196 | tasks[name] = description 1197 | order << name 1198 | end 1199 | task 'all' , "config, setup, test, install" 1200 | task 'config' , "saves your configuration" 1201 | task 'show' , "show current configuration" 1202 | task 'make' , "compile ruby extentions" 1203 | task 'test' , "run test suite" 1204 | task 'doc' , "generate ri documentation" 1205 | task 'install' , "install project files" 1206 | task 'uninstall', "uninstall previously installed files" 1207 | task 'clean' , "does `make clean' for each extention" 1208 | task 'distclean', "does `make distclean' for each extention" 1209 | def run(*argv) 1210 | ARGV.replace(argv) unless argv.empty? 1211 | task = ARGV.find{ |a| a !~ /^[-]/ } 1212 | task = 'all' unless task 1213 | unless task_names.include?(task) 1214 | $stderr.puts "Not a valid task -- #{task}" 1215 | exit 1 1216 | end 1217 | parser = OptionParser.new 1218 | options = {} 1219 | parser.banner = "Usage: #{File.basename($0)} [TASK] [OPTIONS]" 1220 | optparse_header(parser, options) 1221 | case task 1222 | when 'all' 1223 | optparse_all(parser, options) 1224 | when 'config' 1225 | optparse_config(parser, options) 1226 | when 'install' 1227 | optparse_install(parser, options) 1228 | end 1229 | optparse_common(parser, options) 1230 | begin 1231 | parser.parse!(ARGV) 1232 | rescue OptionParser::InvalidOption 1233 | $stderr.puts $!.to_s.capitalize 1234 | exit 1 1235 | end 1236 | rootdir = session.project.rootdir 1237 | print_header 1238 | begin 1239 | session.__send__(task) 1240 | rescue Error => err 1241 | raise err if $DEBUG 1242 | $stderr.puts $!.message 1243 | $stderr.puts "Try 'setup.rb --help' for detailed usage." 1244 | abort $!.message #exit 1 1245 | end 1246 | puts unless session.quiet? 1247 | end 1248 | def session 1249 | @session ||= Session.new(:io=>$stdout) 1250 | end 1251 | def configuration 1252 | @configuration ||= session.configuration 1253 | end 1254 | def optparse_header(parser, options) 1255 | parser.banner = "USAGE: #{File.basename($0)} [command] [options]" 1256 | end 1257 | def optparse_all(parser, options) 1258 | optparse_config(parser, options) 1259 | end 1260 | def optparse_config(parser, options) 1261 | parser.separator "" 1262 | parser.separator "Configuration options:" 1263 | configuration.options.each do |args| 1264 | args = args.dup 1265 | desc = args.pop 1266 | type = args.pop 1267 | name, shortcut = *args 1268 | optname = name.to_s.gsub('_', '-') 1269 | case type 1270 | when :bool 1271 | if optname.index('no-') == 0 1272 | optname = "[no-]" + optname.sub(/^no-/, '') 1273 | opts = shortcut ? ["-#{shortcut}", "--#{optname}", desc] : ["--#{optname}", desc] 1274 | parser.on(*opts) do |val| 1275 | configuration.__send__("#{name}=", !val) 1276 | end 1277 | else 1278 | optname = "[no-]" + optname.sub(/^no-/, '') 1279 | opts = shortcut ? ["-#{shortcut}", "--#{optname}", desc] : ["--#{optname}", desc] 1280 | parser.on(*opts) do |val| 1281 | configuration.__send__("#{name}=", val) 1282 | end 1283 | end 1284 | else 1285 | opts = shortcut ? ["-#{shortcut}", "--#{optname} #{type.to_s.upcase}", desc] : 1286 | ["--#{optname} #{type.to_s.upcase}", desc] 1287 | parser.on(*opts) do |val| 1288 | configuration.__send__("#{name}=", val) 1289 | end 1290 | end 1291 | end 1292 | end 1293 | def optparse_install(parser, options) 1294 | parser.separator "" 1295 | parser.separator "Install options:" 1296 | parser.on("--prefix PATH", "Installation prefix") do |val| 1297 | configuration.install_prefix = val 1298 | end 1299 | end 1300 | def optparse_common(parser, options) 1301 | parser.separator "" 1302 | parser.separator "General options:" 1303 | parser.on("-q", "--quiet", "Suppress output") do 1304 | session.quiet = true 1305 | end 1306 | parser.on("-f", "--force", "Force operation") do 1307 | session.force = true 1308 | end 1309 | parser.on("--trace", "--verbose", "Watch execution") do |val| 1310 | session.trace = true 1311 | end 1312 | parser.on("--trial", "--no-harm", "Do not write to disk") do |val| 1313 | session.trial = true 1314 | end 1315 | parser.on("--debug", "Turn on debug mode") do |val| 1316 | $DEBUG = true 1317 | end 1318 | parser.separator "" 1319 | parser.separator "Inform options:" 1320 | parser.on_tail("-h", "--help", "display this help information") do 1321 | puts parser 1322 | exit 1323 | end 1324 | parser.on_tail("--version", "-v", "Show version") do 1325 | puts File.basename($0) + ' v' + Setup::VERSION #Version.join('.') 1326 | exit 1327 | end 1328 | parser.on_tail("--copyright", "Show copyright") do 1329 | puts Setup::COPYRIGHT #opyright 1330 | exit 1331 | end 1332 | end 1333 | def task_names 1334 | self.class.tasks.keys 1335 | end 1336 | def print_header 1337 | end 1338 | end 1339 | end 1340 | Setup::Command.run 1341 | --------------------------------------------------------------------------------