lib/tap/env.rb in tap-0.12.4 vs lib/tap/env.rb in tap-0.17.0

- old
+ new

@@ -1,283 +1,274 @@ -require 'tap/support/constant_manifest' +require 'tap/root' +require 'tap/env/manifest' +require 'tap/support/templater' autoload(:YAML, 'yaml') module Tap - module Support - autoload(:Templater, 'tap/support/templater') - autoload(:Gems, 'tap/support/gems') - end - - # Envs are locations on the filesystem that have resources associated with - # them (commands, tasks, generators, etc). Envs may point to files, but it's - # more commonly environments are set to a directory and resources are various - # files within the directory. - # - # - #-- - # Note that gems and env_paths reset envs -- custom modifications to envs will be lost - # whenever these configs are reset. + # Env abstracts an execution environment that spans many directories. class Env - include Enumerable - include Configurable - include Support::Minimap - + autoload(:Gems, 'tap/env/gems') + class << self + attr_writer :instance - # Returns the active instance of Env. - def instance - @@instance + def instance(auto_initialize=true) + @instance ||= (auto_initialize ? new : nil) end - # A hash of (path, Env instance) pairs, generated by Env#instantiate. Used - # to prevent infinite loops of Env dependencies by assigning a single Env - # to a given path. - def instances - @@instances - end - - # Creates a new Env for the specified path and adds it to Env#instances, or - # returns the existing instance for the path. Paths can point to an env config - # file, or to a directory. If a directory is provided, instantiate treats - # path as the DEFAULT_CONFIG_FILE in that directory. All paths are expanded. - # - # e1 = Env.instantiate("./path/to/config.yml") - # e2 = Env.instantiate("./path/to/dir") - # - # Env.instances - # # => { - # # File.expand_path("./path/to/config.yml") => e1, - # # File.expand_path("./path/to/dir/#{Tap::Env::DEFAULT_CONFIG_FILE}") => e2 } - # - # The Env is initialized using configurations read from the env config - # file. An instance will be initialized regardless of whether the config - # file or directory exists. - def instantiate(path_or_root) - path = config_path(path_or_root.kind_of?(Root) ? path_or_root.root : path_or_root) - return instances[path] if instances.has_key?(path) + def from_gemspec(spec, context={}) + path = spec.full_gem_path + basename = context[:basename] - config = load_config(path) - root = path_or_root.kind_of?(Root) ? path_or_root : File.dirname(path) + dependencies = [] + spec.dependencies.each do |dependency| + unless dependency.type == :runtime + next + end + + unless gemspec = Gems.gemspec(dependency) + # this error may result when a dependency has + # been uninstalled for a particular gem + warn "missing gem dependency: #{dependency.to_s} (#{spec.full_name})" + next + end + + if basename && !File.exists?(File.join(gemspec.full_gem_path, basename)) + next + end + + dependencies << gemspec + end - # note the assignment of env to instances MUST occur - # before reconfigure to prevent infinite looping - (instances[path] = new(root)).reconfigure(config) - end - - def manifest(name, &block) # :yields: env (and should return a manifest) - name = name.to_sym - define_method(name) do - self.manifests[name] ||= block.call(self).bind(self, name) + config = { + 'root' => path, + 'gems' => dependencies, + 'load_paths' => spec.require_paths, + 'set_load_paths' => false + } + + if context[:basename] + config.merge!(Env.load_config(File.join(path, context[:basename]))) end - end - - private - - def config_path(path) # :nodoc: - if File.directory?(path) || (!File.exists?(path) && File.extname(path) == "") - path = File.join(path, DEFAULT_CONFIG_FILE) - end - File.expand_path(path) + new(config, context) end - # helper to load path as YAML. load_file returns a hash if the path - # loads to nil or false (as happens for empty files) - def load_config(path) # :nodoc: + # Loads configurations from path as YAML. Returns an empty hash if the path + # loads to nil or false (as happens for empty files), or doesn't exist. + def load_config(path) + return {} unless path + begin - Root.trivial?(path) ? {} : (YAML.load_file(path) || {}) + Root::Utils.trivial?(path) ? {} : (YAML.load_file(path) || {}) rescue(Exception) - raise Env::ConfigError.new($!, path) + raise ConfigError.new($!, path) end end + + def scan(load_path, pattern='**/*.rb') + Dir.chdir(load_path) do + Dir.glob(pattern).each do |require_path| + next unless File.file?(require_path) + + default_const_name = require_path.chomp('.rb').camelize + + # note: the default const name has to be set here to allow for implicit + # constant attributes (because a dir is needed to figure the relative path). + # A conflict could arise if the same path is globed from two different + # dirs... no surefire solution. + document = Lazydoc[require_path] + case document.default_const_name + when nil then document.default_const_name = default_const_name + when default_const_name + else raise "found a conflicting default const name" + end + + # scan for constants + Lazydoc::Document.scan(File.read(require_path)) do |const_name, type, comment| + const_name = default_const_name if const_name.empty? + constant = Constant.new(const_name, require_path, comment) + yield(type, constant) + + ############################################################### + # [depreciated] manifest as a task key will be removed at 1.0 + if type == 'manifest' + warn "depreciation: ::task should be used instead of ::manifest as a resource key (#{require_path})" + yield('task', constant) + end + ############################################################### + end + end + end + end end + self.instance = nil - @@instance = nil - @@instances = {} - - # The default config file path - DEFAULT_CONFIG_FILE = "tap.yml" + include Enumerable + include Configurable + include Minimap + # Matches a compound registry search key. After the match, if the key is + # compound then: + # + # $1:: env_key + # $2:: key + # + # If the key is not compound, $2 is nil and $1 is the key. + COMPOUND_KEY = /^((?:[A-z]:(?:\/|\\))?.*?)(?::(.*))?$/ + # An array of nested Envs, by default comprised of the env_path - # + gem environments (in that order). Nested environments are - # activated/deactivated with self. + # + gem environments (in that order). attr_reader :envs - # The Root directory structure for self. - nest(:root, Tap::Root, :set_default => false) + attr_reader :context - # Specify gems to load as nested Envs. Gems may be specified + attr_reader :manifests + + # The Root directory structure for self. + nest(:root, Root, :set_default => false) + + # Specify gems to add as nested Envs. Gems may be specified # by name and/or version, like 'gemname >= 1.2'; by default the - # latest version of the gem is selected. - # - # Gems are immediately loaded (via gem) through this method. + # latest version of the gem is selected. Gems are not activated + # by Env. config_attr :gems, [] do |input| - specs_by_name = {} + input = yaml_load(input) if input.kind_of?(String) - input = YAML.load(input) if input.kind_of?(String) - input = case input + @gems = case input + when false, nil, :NONE, :none + [] + when :LATEST, :ALL + # latest and all, no filter + Gems.select_gems(input == :LATEST) when :latest, :all - Support::Gems.select_gems(input == :latest) do |spec| - env_config = File.join(spec.full_gem_path, Tap::Env::DEFAULT_CONFIG_FILE) - File.exists?(env_config) + # latest and all, filtering by basename + Gems.select_gems(input == :latest) do |spec| + basename == nil || File.exists?(File.join(spec.full_gem_path, basename)) end - else input + else + # resolve gem names manually + [*input].collect do |name| + Gems.gemspec(name) + end.compact end - - @gems = [*input].compact.collect do |gem_name| - spec = Support::Gems.gemspec(gem_name) - - case spec - when nil then log(:warn, "unknown gem: #{gem_name}", Logger::WARN) - else Env.instantiate(spec.full_gem_path) - end - - (specs_by_name[spec.name] ||= []) << spec - spec.name - end.uniq - - # this song and dance is to ensure that the latest spec for a - # given gem appears first in the manifest - specs_by_name.each_pair do |name, specs| - specs_by_name[name] = specs.uniq.sort_by {|spec| spec.version }.reverse - end - - @gems.collect! do |name| - specs_by_name[name] - end.flatten! - + reset_envs end - # Specify configuration files to load as nested Envs. + # Specify directories to load as nested Envs. config_attr :env_paths, [] do |input| - input = YAML.load(input) if input.kind_of?(String) - @env_paths = [*input].compact.collect do |path| - Env.instantiate(root[path]).env_path - end.uniq + @env_paths = resolve_paths(input) reset_envs end - # Designate load paths. - config_attr :load_paths, ["lib"] do |paths| + # Designates paths added to $LOAD_PATH on activation (see set_load_paths). + # These paths are also the default directories searched for resources. + config_attr :load_paths, [:lib] do |input| raise "load_paths cannot be modified once active" if active? - @load_paths = resolve_paths(paths) + @load_paths = resolve_paths(input) end - # Designate paths for discovering and executing commands. - config_attr :command_paths, ["cmd"] do |paths| - @command_paths = resolve_paths(paths) + # If set to true load_paths are added to $LOAD_PATH on activation. + config_attr :set_load_paths, true do |input| + raise "set_load_paths cannot be modified once active" if active? + @set_load_paths = Configurable::Validation.boolean[input] end - # Designate paths for discovering generators. - config_attr :generator_paths, ["lib"] do |paths| - @generator_paths = resolve_paths(paths) - end - - manifest(:commands) do |env| - paths = [] - env.command_paths.each do |path_root| - paths.concat env.root.glob(path_root) - end + # Initializes a new Env linked to the specified directory. A config file + # basename may be specified to load configurations from 'dir/basename' as + # YAML. If a basename is specified, the same basename will be used to + # load configurations for nested envs. + # + # Configurations may be manually provided in the place of dir. In that + # case, the same rules apply for loading configurations for nested envs, + # but no configurations will be loaded for the current instance. + # + # The cache is used internally to prevent infinite loops of nested envs, + # and to optimize the generation of manifests. + def initialize(config_or_dir=Dir.pwd, context={}) + @active = false + @manifests = {} + @context = context - paths = paths.sort_by {|path| File.basename(path) } - Support::Manifest.new(paths) - end - - manifest(:tasks) do |env| - tasks = Support::ConstantManifest.new('manifest') - env.load_paths.each do |path_root| - tasks.register(path_root, '**/*.rb') + # setup root + config = nil + @root = case config_or_dir + when Root then config_or_dir + when String then Root.new(config_or_dir) + else + config = config_or_dir + + if config.has_key?(:root) && config.has_key?('root') + raise "multiple values mapped to :root" + end + + root = config.delete(:root) || config.delete('root') || Dir.pwd + root.kind_of?(Root) ? root : Root.new(root) end - # tasks.cache = env.cache[:tasks] - tasks - end - - manifest(:generators) do |env| - generators = Support::ConstantManifest.intern('generator') do |manifest, const| - const.name.underscore.chomp('_generator') - end - env.generator_paths.each do |path_root| - generators.register(path_root, '**/*_generator.rb') + if basename && !config + config = Env.load_config(File.join(@root.root, basename)) end - # generators.cache = env.cache[:generators] - generators - end - - def initialize(path_root_or_config=Dir.pwd) - @envs = [] - @active = false - @manifests = {} - - # initialize these for reset_env - @gems = [] - @env_paths = [] - @root = case path_root_or_config - when Root then path_root_or_config - when String then Root.new(path_root_or_config) - else Root.new + if instance(@root.root) + raise "context already has an env for: #{@root.root}" end + instances << self - unless path_root_or_config.kind_of?(Hash) - path_root_or_config = {} - end - initialize_config(path_root_or_config) + # set these for reset_env + @gems = nil + @env_paths = nil + initialize_config(config || {}) end - # Clears manifests so they may be regenerated. - def reset - @manifests.clear + # The minikey for self (root.root). + def minikey + root.root end - # Returns the key for self in Env.instances. - def env_path - Env.instances.each_pair {|path, env| return path if env == self } - nil - end - # Sets envs removing duplicates and instances of self. Setting envs # overrides any environments specified by env_path and gem. def envs=(envs) raise "envs cannot be modified once active" if active? - @envs = envs.uniq.delete_if {|e| e == self } + @envs = envs.uniq.delete_if {|env| env == self } end - - # Unshifts env onto envs, removing duplicates. - # Self cannot be unshifted onto self. + + # Unshifts env onto envs. Self cannot be unshifted onto self. def unshift(env) unless env == self || envs[0] == env self.envs = envs.dup.unshift(env) end self end - + # Pushes env onto envs, removing duplicates. # Self cannot be pushed onto self. def push(env) unless env == self || envs[-1] == env envs = self.envs.reject {|e| e == env } self.envs = envs.push(env) end self end - + # Passes each nested env to the block in order, starting with self. def each visit_envs.each {|e| yield(e) } end - + # Passes each nested env to the block in reverse order, ending with self. def reverse_each visit_envs.reverse_each {|e| yield(e) } end # Recursively injects the memo to each env of self. Each env in envs - # receives the same memo from the parent. + # receives the same memo from the parent. This is different from the + # inject provided via Enumerable, where each subsequent env receives + # the memo from the previous, not the parent, env. # - # a,b,c,d,e = ('a'..'e').collect {|name| Tap::Env.new(:name => name) } + # a,b,c,d,e = ('a'..'e').collect {|name| Env.new(:name => name) } # # a.push(b).push(c) # b.push(d).push(e) # # lines = [] @@ -300,20 +291,22 @@ # Activates self by doing the following, in order: # # * sets Env.instance to self (unless already set) # * activate nested environments - # * unshift load_paths to $LOAD_PATH + # * unshift load_paths to $LOAD_PATH (if set_load_paths is true) # # Once active, the current envs and load_paths are frozen and cannot be # modified until deactivated. Returns true if activate succeeded, or # false if self is already active. def activate return false if active? @active = true - @@instance = self if @@instance == nil + unless self.class.instance(false) + self.class.instance = self + end # freeze envs and load paths @envs.freeze @load_paths.freeze @@ -321,23 +314,25 @@ envs.reverse_each do |env| env.activate end # add load paths - load_paths.reverse_each do |path| - $LOAD_PATH.unshift(path) + if set_load_paths + load_paths.reverse_each do |path| + $LOAD_PATH.unshift(path) + end + + $LOAD_PATH.uniq! end - $LOAD_PATH.uniq! - true end # Deactivates self by doing the following in order: # # * deactivates nested environments - # * removes load_paths from $LOAD_PATH + # * removes load_paths from $LOAD_PATH (if set_load_paths is true) # * sets Env.instance to nil (if set to self) # * clears cached manifest data # # Once deactivated, envs and load_paths are unfrozen and may be modified. # Returns true if deactivate succeeded, or false if self is not active. @@ -351,191 +346,282 @@ end # remove load paths load_paths.each do |path| $LOAD_PATH.delete(path) - end + end if set_load_paths # unfreeze envs and load paths @envs = @envs.dup @load_paths = @load_paths.dup # clear cached data - @@instance = nil if @@instance == self - @manifests.clear + klass = self.class + if klass.instance(false) == self + klass.instance = nil + end true end # Return true if self has been activated. def active? @active end - # Searches each env for the first existing file or directory at - # env.root.filepath(dir, path). Paths are expanded, and search - # checks to make sure the file is, in fact, relative to env.root[dir]. - # An optional block may be used to check the file; the file will only - # be returned if the block returns true. - # - # Returns nil if no file can be found. - def search(dir, path, strict=true) + def hlob(dir, pattern="**/*") + results = {} each do |env| - directory = env.root.filepath(dir) - file = env.root.filepath(dir, path) - next unless File.exists?(file) - - # check the file is relative to directory - if strict && file.rindex(directory, 0) != 0 - raise "not relative to search dir: #{file} (#{directory})" + root = env.root + root.glob(dir, pattern).each do |path| + relative_path = root.relative_path(dir, path) + results[relative_path] ||= path end + end + results + end + + def glob(dir, pattern="**/*") + hlob(dir, pattern).values.sort! + end + + def path(dir, *paths) + each do |env| + path = env.root.path(dir, *paths) + return path if !block_given? || yield(path) + end + nil + end + + # Retrieves a path associated with the inheritance hierarchy of an object. + # An array of modules (which naturally can include classes) are provided + # and module_path traverses each, forming paths like: + # + # path(dir, module_path, *paths) + # + # By default, 'module_path' is 'module.to_s.underscore', but modules can + # specify an alternative by providing a module_path method. + # + # The paths are yielded to the block and when the block returns true, + # the path will be returned. If no block is given, the first module path + # is returned. Returns nil if the block never returns true. + # + def module_path(dir, modules, *paths, &block) + paths.compact! + while current = modules.shift + module_path = if current.respond_to?(:module_path) + current.module_path + else + current.to_s.underscore + end - # filter - return file if !block_given? || yield(file) + if path = self.path(dir, module_path, *paths, &block) + return path + end end - + nil end - # - TEMPLATES = {} - TEMPLATES[:commands] = %Q{<% if count > 1 %> -<%= env_name %>: -<% end %> -<% entries.each do |name, const| %> - <%= name.ljust(width) %> -<% end %>} - TEMPLATES[:tasks] = %Q{<% if count > 1 %> -<%= env_name %>: -<% end %> -<% entries.each do |name, const| %> -<% desc = const.document[const.name]['manifest'] %> - <%= name.ljust(width) %><%= desc.empty? ? '' : ' # ' %><%= desc %> -<% end %>} - TEMPLATES[:generators] = %Q{<% if count > 1 %> -<%= env_name %>: -<% end %> -<% entries.each do |name, const| %> -<% desc = const.document[const.name]['generator'] %> - <%= name.ljust(width) %><%= desc.empty? ? '' : ' # ' %><%= desc %> -<% end %>} + # Returns the module_path traversing the inheritance hierarchy for the + # class of obj (or obj if obj is a Class). Included modules are not + # visited, only the superclasses. + def class_path(dir, obj, *paths, &block) + klass = obj.kind_of?(Class) ? obj : obj.class + superclasses = klass.ancestors - klass.included_modules + module_path(dir, superclasses, *paths, &block) + end - def summarize(name, template=TEMPLATES[name]) - count = 0 - width = 10 + def registry(build=false) + builders.each_pair do |type, builder| + registry[type] ||= builder.call(self) + end if build - env_names = {} - minimap.each do |env_name, env| - env_names[env] = env_name - end - - inspect(template) do |templater, share| - env = templater.env - entries = env.send(name).minimap - next(false) if entries.empty? - - templater.env_name = env_names[env] - templater.entries = entries - - count += 1 - entries.each do |entry_name, entry| - width = entry_name.length if width < entry_name.length + registries[minikey] ||= begin + registry = {} + load_paths.each do |load_path| + next unless File.directory?(load_path) + + Env.scan(load_path) do |type, constant| + entries = registry[type.to_sym] ||= [] + entries << constant + end end - share[:count] = count - share[:width] = width - true + registry end end - def inspect(template=nil) # :yields: templater, attrs - return "#<#{self.class}:#{object_id} root='#{root.root}'>" if template == nil + def register(type, override=false, &block) + type = type.to_sym - attrs = {} - collect do |env| - templater = Support::Templater.new(template, :env => env) - block_given? ? (yield(templater, attrs) ? templater : nil) : templater - end.compact.collect do |templater| - templater.build(attrs) - end.join + # error for existing, or overwrite + case + when override + builders.delete(type) + registries.each {|root, registry| registry.delete(type) } + when builders.has_key?(type) + raise "a builder is already registered for: #{type.inspect}" + when registries.any? {|root, registry| registry.has_key?(type) } + raise "entries are already registered for: #{type.inspect}" + end + + builders[type] = block end - def recursive_inspect(template=nil, *args) # :yields: templater, attrs - return "#<#{self.class}:#{object_id} root='#{root.root}'>" if template == nil + def manifest(type) # :yields: env + type = type.to_sym - attrs = {} - templaters = [] - recursive_inject(args) do |argv, env| - templater = Support::Templater.new(template, :env => env) - next_args = block_given? ? yield(templater, attrs, *argv) : argv - templaters << templater if next_args - - next_args + registry[type] ||= begin + builder = builders[type] + builder ? builder.call(self) : [] end - templaters.collect do |templater| - templater.build(attrs) + manifests[type] ||= Manifest.new(self, type) + end + + def [](type) + manifest(type) + end + + def reset + manifests.clear + registries.clear + end + + #-- + # Environment-seek + def eeek(type, key) + key =~ COMPOUND_KEY + envs = if $2 + # compound key, match for env + key = $2 + [minimatch($1)].compact + else + # not a compound key, search all envs by iterating self + self + end + + # traverse envs looking for the first + # manifest entry matching key + envs.each do |env| + if result = env.manifest(type).minimatch(key) + return [env, result] + end + end + + nil + end + + # Searches across each for the first registered object minimatching key. A + # single env can be specified by using a compound key like 'env_key:key'. + # + # Returns nil if no matching object is found. + def seek(type, key, &block) # :yields: env, key + env, result = eeek(type, key, &block) + result + end + + # All templaters are yielded to the block before any are built. This + # allows globals to be determined for all environments. + def inspect(template=nil, globals={}, filename=nil) # :yields: templater, globals + if template == nil + return "#<#{self.class}:#{object_id} root='#{root.root}'>" + end + + env_keys = minihash(true) + collect do |env| + templater = Support::Templater.new(template, :env => env, :env_key => env_keys[env]) + yield(templater, globals) if block_given? + templater + end.collect! do |templater| + templater.build(globals, filename) end.join end protected - # A hash of the manifests for self. - attr_reader :manifests + def registries # :nodoc: + context[:registries] ||= {} + end - def minikey(env) - env.root.root + def basename # :nodoc: + context[:basename] end - # Resets envs using the current env_paths and gems. - def reset_envs - self.envs = env_paths.collect do |path| - Env.instantiate(path) - end + gems.collect do |spec| - Env.instantiate(spec.full_gem_path) - end + def builders # :nodoc: + context[:builders] ||= {} end - # Arrayifies, compacts, and resolves input paths using root, and - # removes duplicates. In short + def instances # :nodoc: + context[:instances] ||= [] + end + + def instance(path) # :nodoc: + instances.find {|env| env.root.root == path } + end + + # resets envs using the current env_paths and gems. does nothing + # until both env_paths and gems are set. + def reset_envs # :nodoc: + if env_paths && gems + self.envs = env_paths.collect do |path| + instance(path) || Env.new(path, context) + end + gems.collect do |spec| + instance(spec.full_gem_path) || Env.from_gemspec(spec, context) + end + end + end + + # arrayifies, compacts, and resolves input paths using root, and + # removes duplicates. in short: # # resolve_paths ['lib', nil, 'lib', 'alt] # => [root['lib'], root['alt']] # def resolve_paths(paths) # :nodoc: - paths = YAML.load(paths) if paths.kind_of?(String) - [*paths].compact.collect {|path| root[path]}.uniq + paths = yaml_load(paths) if paths.kind_of?(String) + [*paths].compact.collect {|path| root[path] }.uniq end - - # Recursively iterates through envs, starting with self, and - # collects the visited envs in order. + + # helper to recursively iterate through envs, starting with self. + # visited envs are collected in order and are used to ensure a + # given env will only be visited once. def visit_envs(visited=[], &block) # :nodoc: unless visited.include?(self) visited << self yield(self) if block_given? - + envs.each do |env| env.visit_envs(visited, &block) end end - + visited end - + # helper to recursively inject a memo to the children of env - def inject_envs(memo, visited=[], &block) # :nodoc: + def inject_envs(memo, visited=[], &block) # :nodoc: unless visited.include?(self) visited << self next_memo = yield(memo, self) envs.each do |env| env.inject_envs(next_memo, visited, &block) end end - + visited end - # Raised when there is a Env-level configuration error. - class ConfigError < StandardError + private + + # A 'quick' yaml load where empty strings will not cause YAML to autoload. + # This is a silly song and dance, but provides for optimal launch times. + def yaml_load(str) # :nodoc: + str.empty? ? false : YAML.load(str) + end + + # Raised when there is a configuration error from Env.load_config. + class ConfigError < StandardError # :nodoc: attr_reader :original_error, :env_path def initialize(original_error, env_path) @original_error = original_error @env_path = env_path \ No newline at end of file