lib/tap/env.rb in tap-0.10.0 vs lib/tap/env.rb in tap-0.10.1

- old
+ new

@@ -1,36 +1,40 @@ -require 'tap/root' +require 'tap/support/manifest' require 'tap/support/constant' require 'tap/support/summary' -require 'tap/support/manifest' +require 'tap/support/gems' module Tap #-- # Note that gems and env_paths reset envs -- custom modifications to envs will be lost # whenever these configs are reset. class Env include Support::Configurable include Enumerable - @@instance = nil - @@instances = {} - @@manifests = {} - - class << self + class << self + # Returns the active instance of Env. def instance @@instance 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 + # A hash of predefined manifest classes that can be initialized + # from an env. These classes are instantiated by instances + # of Env, as needed. + def manifests + @@manifests + 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. # @@ -43,72 +47,94 @@ # # File.expand_path("./path/to/dir/#{Tap::Env::DEFAULT_CONFIG_FILE}") => e2 } # # The Env is initialized using configurations read from the env config file using # load_config, and a Root initialized to the config file directory. An instance # will be initialized regardless of whether the config file or directory exists. - def instantiate(path_or_root, default_config={}, logger=nil) + def instantiate(path_or_root, default_config={}, logger=nil, &block) path = path_or_root.kind_of?(Root) ? path_or_root.root : path_or_root path = pathify(path) begin root = path_or_root.kind_of?(Root) ? path_or_root : Root.new(File.dirname(path)) config = default_config.merge(load_config(path)) # note the assignment of env to instances MUST occur before # reconfigure to prevent infinite looping - (instances[path] = Env.new({}, root, logger)).reconfigure(config) do |unhandled_configs| - yield(unhandled_configs) if block_given? - end + (instances[path] = new({}, root, logger)).reconfigure(config, &block) rescue(Exception) raise Env::ConfigError.new($!, path) end end + def instance_for(path) + path = pathify(path) + instances.has_key?(path) ? instances[path] : instantiate(path) + end + def pathify(path) if File.directory?(path) || (!File.exists?(path) && File.extname(path) == "") path = File.join(path, DEFAULT_CONFIG_FILE) end File.expand_path(path) end - def instance_for(path) - path = pathify(path) - instances.has_key?(path) ? instances[path] : instantiate(path) - end - - # Returns the gemspec for the specified gem. A gem version - # can be specified in the name, like 'gem >= 1.2'. The gem - # will be activated using +gem+ if necessary. - def gemspec(gem_name) - return gem_name if gem_name.kind_of?(Gem::Specification) + def manifest(name, pattern, default_paths=[], &block) # :yields: search_path + manifest_class = Class.new(Support::Manifest) + manifest_class.send(:define_method, :entries_for, &block) if block_given? + manifest_class.send(:attr_reader, :env) + manifest_class.send(:define_method, :initialize) do |env| + @env = env + search_paths = default_paths.collect {|path| env.root[path] } + search_paths += env.root.glob(:root, pattern) + super search_paths.sort_by {|p| File.basename(p) } + end - # figure the version of the gem, by default >= 0.0.0 - gem_name.to_s =~ /^([^<=>]*)(.*)$/ - name, version = $1.strip, $2 - version = ">= 0.0.0" if version.empty? - - return nil if name.empty? - - # load the gem and get the spec - gem(name, version) - Gem.loaded_specs[name] + manifests[name] = manifest_class end - # Returns the gem name for all installed gems with a DEFAULT_CONFIG_FILE. - # If latest==true, then only the names for the most current gem specs - # will be returned. - def known_gems(latest=true) - index = latest ? - Gem.source_index.latest_specs : - Gem.source_index.gems.collect {|(name, spec)| spec } - - index.select do |spec| - File.exists?(File.join(spec.full_gem_path, DEFAULT_CONFIG_FILE)) || - File.exists?(File.join(spec.full_gem_path, DEFAULT_TASK_FILE)) - end.sort + #-- + # To manifest simply requires an glob_<name> method which + # yields each (key, path) pair for the manifested object in + # a predictable order. + # + #-- + # Alternate implementation would create the manifest for each individual + # env, then merge the manifests. On the plus side, each env would then + # carry it's own slice of the manifest without having to recalculate. + # On the down side, the merging would have to occur in some separate + # method that cannot be defined here. + def path_manifest(name, paths_key, pattern, default_paths=[], &block) # :yields: search_path_root, search_path + manifest_class = Class.new(Support::Manifest) + manifest_class.send(:define_method, :entries_for, &block) if block_given? + manifest_class.send(:attr_reader, :env) + manifest_class.send(:define_method, :initialize) do |env| + @env = env + search_paths = default_paths.collect do |path| + [env.root.root, env.root[path]] + end + + env.send(paths_key).each do |search_path_root| + env.root.glob(search_path_root, pattern).each do |search_path| + search_paths << [search_path_root, search_path] + end + end + + super search_paths.sort_by {|pr, p| File.basename(p) } + end + manifests[name] = manifest_class end + # Returns the gemspecs for all installed gems with a DEFAULT_TASK_FILE + # or DEFAULT_CONFIG_FILE. If latest==true, then only the specs for the + # most current gems will be returned. + def gemspecs(latest=true) + Support::Gems.select_gems(latest) do |spec| + File.exists?(File.join(spec.full_gem_path, DEFAULT_TASK_FILE)) || + File.exists?(File.join(spec.full_gem_path, DEFAULT_CONFIG_FILE)) + end + end + protected # Defines a config that raises an error if set when the # instance is active. static_config MUST take a block # and raises an error if a block is not given. @@ -135,50 +161,25 @@ config_attr(key, value) do |input| check_configurable instance_variable_set(instance_variable, [*input].compact.collect {|path| root[path]}.uniq) end end - - #-- - # To manifest simply requires an glob_<name> method which - # yields each (key, path) pair for the manifested object in - # a predictable order. - # - #-- - # Alternate implementation would create the manifest for each individual - # env, then merge the manifests. On the plus side, each env would then - # carry it's own slice of the manifest without having to recalculate. - # On the down side, the merging would have to occur in some separate - # method that cannot be defined here. - def manifest(name, paths_key, pattern, &block) - return manifest(name, paths_key, pattern) do |context, path| - [[path.chomp(File.extname(path)), path]] - end unless block_given? - - glob_method = Support::Manifest.glob_method(name) - module_eval %Q{ - def #{glob_method} - paths = [] - self.#{paths_key}.each do |manifest_path| - root.glob(manifest_path, "#{pattern}").each do |path| - next if File.directory?(path) - paths << [manifest_path, path] - end - end - paths.sort_by {|mp, p| File.basename(p)} - end - } - - map_method = Support::Manifest.map_method(name) - define_method(map_method, &block) - - protected glob_method, map_method + end + + class Manifest < Support::Manifest + def initialize(env) + super([]) + @entries = env.collect {|e| [e.root.root, e] } end end + @@instance = nil + @@instances = {} + @@manifests = {:envs => Manifest} + # The global config file path - GLOBAL_CONFIG_FILE = File.join(Gem.user_home, ".tap.yml") + GLOBAL_CONFIG_FILE = File.join(Support::Gems.user_home, ".tap.yml") # The default config file path DEFAULT_CONFIG_FILE = "tap.yml" # The default task file path @@ -202,11 +203,11 @@ # Note that the gems are resolved to gemspecs using Env.gemspec, # so self.gems returns an array of gemspecs. config_attr :gems, [] do |input| check_configurable @gems = [*input].compact.collect do |gem_name| - spec = Env.gemspec(gem_name) + spec = Support::Gems.gemspec(gem_name) case spec when nil then log(:warn, "unknown gem: #{gem_name}", Logger::WARN) else Env.instance_for(spec.full_gem_path) end @@ -222,45 +223,45 @@ @env_paths = [*input].compact.collect do |path| Env.instance_for(root[path]).env_path end.uniq reset_envs end - - # Designate load paths. If use_dependencies == true, then - # load_paths will be used for automatic loading of modules - # through the active_support Dependencies module. + + # Designate load paths. path_config :load_paths, ["lib"] # Designate paths for discovering and executing commands. path_config :command_paths, ["cmd"] # Designate paths for discovering generators. path_config :generator_paths, ["lib"] - manifest(:tasks, :load_paths, "**/*.rb") do |load_path, path| - next unless document = Support::Lazydoc.scan_doc(path, 'manifest') + path_manifest(:tasks, :load_paths, "**/*.rb", [DEFAULT_TASK_FILE]) do |load_path, path| + next unless File.file?(path) && document = Support::Lazydoc.scan_doc(path, 'manifest') document.const_names.collect do |const_name| if const_name.empty? - key = root.relative_filepath(load_path, path).chomp('.rb') + key = env.root.relative_filepath(load_path, path).chomp('.rb') [key, Support::Constant.new(key.camelize, path)] else [const_name.underscore, Support::Constant.new(const_name, path)] end end end - manifest(:commands, :command_paths, "**/*.rb") + path_manifest(:commands, :command_paths, "**/*.rb") do |command_path, path| + File.file?(path) ? [[path, path]] : nil + end - manifest(:generators, :generator_paths, '**/*_generator.rb') do |load_path, path| + path_manifest(:generators, :generator_paths, '**/*_generator.rb') do |generator_path, path| dirname = File.dirname(path) - next unless "#{File.basename(dirname)}_generator.rb" == File.basename(path) + next unless File.file?(path) && "#{File.basename(dirname)}_generator.rb" == File.basename(path) next unless document = Support::Lazydoc.scan_doc(path, 'generator') document.const_names.collect do |const_name| if const_name.empty? - key = root.relative_filepath(load_path, dirname) + key = env.root.relative_filepath(generator_path, dirname) [key, Support::Constant.new((key + '_generator').camelize, path)] else [const_name.underscore, Support::Constant.new(const_name, path)] end end @@ -302,21 +303,21 @@ # Self cannot be unshifted onto self. def unshift(env) unless env == self || envs[0] == env self.envs = envs.dup.unshift(env) end - envs + 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 - envs + self end # Passes each nested env to the block in order, starting with self. def each envs(true).each {|e| yield(e) } @@ -325,22 +326,44 @@ # Passes each nested env to the block in reverse order, ending with self. def reverse_each envs(true).reverse_each {|e| yield(e) } end + # Visits each nested env in order, starting with self, and passing + # to the block the env and any arguments generated by the parent of + # the env. The initial arguments are set when recursive_each is + # first called; subsequent arguements are the return values of the + # block. + # + # e0, e1, e2, e3, e4 = ('a'..'e').collect {|name| Tap::Env.new(:name => name) } + # + # e0.push(e1).push(e2) + # e1.push(e3).push(e4) + # + # lines = [] + # e0.recursive_each(0) do |env, nesting_depth| + # lines << "\n#{'..' * nesting_depth}#{env.config[:name]} (#{nesting_depth})" + # nesting_depth + 1 + # end + # + # lines.join + # # => %Q{ + # # a (0) + # # ..b (1) + # # ....d (2) + # # ....e (2) + # # ..c (1)} + # + def recursive_each(*args, &block) # :yields: env, *parent_args + each_nested_env(self, [], args, &block) + end + # Returns the total number of unique envs nested in self (including self). def count envs(true).length end - # Returns a list of arrays that receive load_paths on activate, - # by default [$LOAD_PATH]. If use_dependencies == true, then - # Dependencies.load_paths will also be included. - def load_path_targets - [$LOAD_PATH] - end - # Processes and resets the input configurations for both root # and self. Reconfiguration consists of the following steps: # # * partition overrides into env, root, and other configs # * reconfigure root with the root configs @@ -410,11 +433,11 @@ # is already active. def activate return false if active? @active = true - @@instance = self unless @@instance + @@instance = self if @@instance == nil # freeze array configs like load_paths config.each_pair do |key, value| case value when Array then value.freeze @@ -424,19 +447,16 @@ # activate nested envs envs.reverse_each do |env| env.activate end - # add load paths to load_path_targets - load_path_targets.each do |target| - load_paths.reverse_each do |path| - target.unshift(path) - end - - target.uniq! + # add load paths + load_paths.reverse_each do |path| + $LOAD_PATH.unshift(path) end + $LOAD_PATH.uniq! true end # Deactivates self by clearing manifests and deleting load_paths for self # from the load_path_targets. Env.instance will no longer reference self @@ -444,17 +464,15 @@ # # Returns true if deactivate succeeded, or false if self is not active. def deactivate return false unless active? - # remove load paths from load_path_targets - load_path_targets.each do |target| - load_paths.each do |path| - target.delete(path) - end + # remove load paths + load_paths.each do |path| + $LOAD_PATH.delete(path) end - + # unfreeze array configs by duplicating self.config.class_config.each_pair do |key, value| value = send(key) case value when Array then instance_variable_set("@#{key}", value.dup) @@ -476,61 +494,80 @@ # Return true if self has been activated. def active? @active end - # Cycles through all items yielded by the iterate_<name> method and - # adds each to the manifests[name] hash. Freezes the hash when complete. - # Simply returns the manifests[name] hash if frozen. - def manifest(name) - manifest = manifests[name] ||= Support::Manifest.new(name, self) - - manifest.entries.each do |key, path| - yield(key, path) - end if block_given? - - manifest.each_path do |context, path| - next unless keys = send(manifest.map_method, context, path) - - keys.each {|entry| manifest.store(entry) } - keys.each {|key, value| yield(key, value) } if block_given? - end unless manifest.complete? - + # Returns the manifest in manifests by the specified name. Yields + # each entry in the manifest to the block, if given, or simply + # builds and returns the manifest. + # + # If the specified manifest does not exists, the manifest class + # in self.class.manifests will be instatiated with self to make + # the manifest. Raises an error if no manifest could be found + # or instantiated. + def manifest(name, build=false) + manifest = manifests[name] ||= case + when manifests_class = self.class.manifests[name] + manifests_class.new(self) + else + raise "unknown manifest: #{name}" + end + + manifest.build if build manifest end - def find(name, pattern) - manifest(name) do |key, path| - return path if Root.minimal_match?(key, pattern) + # Returns the first value in the specified manifest where the key + # mini-matches the input pattern. See Tap::Root.minimal_match? + # for details on mini-matching. + def find(name, pattern, value_only=true) + manifest(name).each do |key, value| + return(value_only ? value : [key, value]) if Root.minimal_match?(key, pattern) end nil end - def search(name, pattern) - return find(name, pattern) if name == :envs + # Like find, but searches across all envs for the matching value. + # An env pattern can be provided in pattern, to select a single + # env to search. + # + # The :envs manifest cannot be searched; use find instead. + def search(name, pattern, value_only=true) + if name == :envs + raise ArgumentError, "cannot search the :envs manifest; use find instead" + end envs = case pattern when /^(.*):([^:]+)$/ env_pattern = $1 pattern = $2 - find(:envs, env_pattern) or raise(ArgumentError, "could not find env: #{env_pattern}") + find(:envs, env_pattern) else manifest(:envs).values end envs.each do |env| - if result = env.find(name, pattern) + if result = env.find(name, pattern, value_only) return result end - end + end if envs nil end + def constantize(name, *patterns) + patterns.collect do |pattern| + case const = search(name, pattern) + when Support::Constant then const.constantize + else raise "could not constantize: #{pattern} (#{name})" + end + end + end + def summary(name) summary = Support::Summary.new - manifest(:envs).mini_map.each do |(key, env)| - summary.add(key, env, env.manifest(name).mini_map) + manifest(:envs, true).minimize.each do |(key, env)| + summary.add(key, env, env.manifest(name, true).minimize) end summary end def summarize(name, &block) @@ -544,55 +581,13 @@ end def to_s inspect(true) end - - #-- - # Under construction - #++ - def handle_error(err) - case - when $DEBUG - puts err.message - puts - puts err.backtrace - else puts err.message - end - end - protected - # Iterates over each nested env, yielding the root path and env. - # This is the manifest method for envs. - def manifest_glob_envs - collect {|env| [env.root.root, env] }.sort_by {|root, env| File.basename(root) } - end - - def manifest_map(context, path) - [[context, path]] - end - - alias default_manifest_glob_tasks manifest_glob_tasks - - def manifest_glob_tasks - paths = default_manifest_glob_tasks - - # very odd behaviors -- - # * OS X is case-insensitive, apparently. Tapfile.rb and tapfile.rb are the same. - # * require 'tapfile' does not work - # * require 'tapfile.rb' works - # * load 'tapfile' works - # - root.glob(:root, DEFAULT_TASK_FILE).each do |path| - next if File.directory?(path) - paths.unshift [root.root, path] - end - paths - end - # Raises an error if self is already active (and hence, configurations # should not be modified) def check_configurable raise "path configurations are disabled when active" if active? end @@ -617,10 +612,23 @@ end end target end - + + private + + def each_nested_env(env, visited, args, &block) + return if visited.include?(env) + + visited << env + next_args = yield(env, *args) + next_args = [] if next_args == nil + env.envs.each do |nested_env| + each_nested_env(nested_env, visited, next_args, &block) + end + end + # Raised when there is a Env-level configuration error. class ConfigError < StandardError attr_reader :original_error, :env_path def initialize(original_error, env_path) \ No newline at end of file