lib/tap/env.rb in tap-0.11.1 vs lib/tap/env.rb in tap-0.12.0

- old
+ new

@@ -1,16 +1,25 @@ require 'tap/support/constant_manifest' -require 'tap/support/gems' 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. class Env include Enumerable - include Support::Configurable + include Configurable include Support::Minimap class << self # Returns the active instance of Env. @@ -36,108 +45,97 @@ # 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 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, &block) - path = path_or_root.kind_of?(Root) ? path_or_root.root : path_or_root - path = pathify(path) + # 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) - 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] = new({}, root, logger)).reconfigure(config, &block) - rescue(Exception) - raise Env::ConfigError.new($!, path) - end + config = load_config(path) + root = path_or_root.kind_of?(Root) ? path_or_root : File.dirname(path) + + # note the assignment of env to instances MUST occur + # before reconfigure to prevent infinite looping + (instances[path] = new(root)).reconfigure(config) 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 manifest(name, &block) # :yields: env (returns manifest) + 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) end end - # Returns the gemspecs for all installed gems with a 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_CONFIG_FILE)) + 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) end - protected - - # Defines a config that collects the input into a unique, - # compact array where each member has been resolved using - # root[]. In short, ['lib', nil, 'lib', 'alt] becomes - # [root['lib'], root['alt']]. - # - # Single and nil arguments are allowed; they are arrayified - # and handled as above. Path configs raise an error if - # modified when the instance is active. - def path_config(key, value=[]) - instance_variable = "@#{key}".to_sym - config_attr(key, value) do |input| - check_configurable - instance_variable_set(instance_variable, [*input].compact.collect {|path| root[path]}.uniq) + # 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: + begin + Root.trivial?(path) ? {} : (YAML.load_file(path) || {}) + rescue(Exception) + raise Env::ConfigError.new($!, path) end end end @@instance = nil @@instances = {} # The default config file path DEFAULT_CONFIG_FILE = "tap.yml" + # An array of nested Envs, by default comprised of the env_path + # + gem environments (in that order). Nested environments are + # activated/deactivated with self. + attr_reader :envs + # The Root directory structure for self. - attr_reader :root + nest(:root, Tap::Root) do |config| + case config + when Root then config + when String then Root.new(config) + else Root.new.reconfigure(config) + end + end - # Gets or sets the logger for self - attr_accessor :logger - - # Specify files to require when self is activated. - config :requires, [], &c.array_or_nil - - # Specify files to load when self is activated. - config :loads, [], &c.array_or_nil - # Specify gems to load 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. config_attr :gems, [] do |input| - check_configurable specs_by_name = {} + + input = YAML.load(input) if input.kind_of?(String) + input = case input + 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) + end + else input + 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.instance_for(spec.full_gem_path) + else Env.instantiate(spec.full_gem_path) end (specs_by_name[spec.name] ||= []) << spec spec.name end.uniq @@ -155,25 +153,32 @@ reset_envs end # Specify configuration files to load as nested Envs. config_attr :env_paths, [] do |input| - check_configurable + input = YAML.load(input) if input.kind_of?(String) @env_paths = [*input].compact.collect do |path| - Env.instance_for(root[path]).env_path + Env.instantiate(root[path]).env_path end.uniq reset_envs end # Designate load paths. - path_config :load_paths, ["lib"] + config_attr :load_paths, ["lib"] do |paths| + raise "load_paths cannot be modified once active" if active? + @load_paths = resolve_paths(paths) + end # Designate paths for discovering and executing commands. - path_config :command_paths, ["cmd"] + config_attr :command_paths, ["cmd"] do |paths| + @command_paths = resolve_paths(paths) + end # Designate paths for discovering generators. - path_config :generator_paths, ["lib"] + 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) @@ -201,43 +206,45 @@ generators.register(path_root, '**/*_generator.rb') end # generators.cache = env.cache[:generators] generators end - - def initialize(config={}, root=Tap::Root.new, logger=nil) - @root = root - @logger = logger + + def initialize(path_root_or_config=Dir.pwd) @envs = [] @active = false @manifests = {} # initialize these for reset_env @gems = [] @env_paths = [] - initialize_config(config) + initialize_config case path_root_or_config + when String, Root then {:root => path_root_or_config} + else path_root_or_config + end end - # Sets envs removing duplicates and instances of self. - def envs=(envs) - @envs = envs.uniq.delete_if {|e| e == self } - @envs.freeze - @flat_envs = nil + # Clears manifests so they may be regenerated. + def reset + @manifests.clear end - # An array of nested Envs, by default comprised of the - # env_path + gem environments (in that order). These - # nested Envs are activated/deactivated with self. - # - # Returns a flattened array of the unique nested envs - # when flat == true. - def envs(flat=false) - flat ? (@flat_envs ||= self.flatten_envs.freeze) : @envs + # Returns the key for self in Env.instances. + def env_path + Env.instances.each_pair {|path, env| return path if env == self } + nil end - # Unshifts env onto envs, removing duplicates. + # 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 } + end + + # Unshifts env onto envs, removing duplicates. # Self cannot be unshifted onto self. def unshift(env) unless env == self || envs[0] == env self.envs = envs.dup.unshift(env) end @@ -254,31 +261,28 @@ self end # Passes each nested env to the block in order, starting with self. def each - envs(true).each {|e| yield(e) } + visit_envs.each {|e| yield(e) } end # Passes each nested env to the block in reverse order, ending with self. def reverse_each - envs(true).reverse_each {|e| yield(e) } + visit_envs.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. + # Recursively injects the memo to each env of self. Each env in envs + # receives the same memo from the parent. # - # e0, e1, e2, e3, e4 = ('a'..'e').collect {|name| Tap::Env.new(:name => name) } + # a,b,c,d,e = ('a'..'e').collect {|name| Tap::Env.new(:name => name) } # - # e0.push(e1).push(e2) - # e1.push(e3).push(e4) + # a.push(b).push(c) + # b.push(d).push(e) # # lines = [] - # e0.recursive_each(0) do |env, nesting_depth| + # a.recursive_inject(0) do |nesting_depth, env| # lines << "\n#{'..' * nesting_depth}#{env.config[:name]} (#{nesting_depth})" # nesting_depth + 1 # end # # lines.join @@ -287,195 +291,112 @@ # # ..b (1) # # ....d (2) # # ....e (2) # # ..c (1)} # - def recursive_each(*args, &block) # :yields: env, *parent_args - each_nested_env(self, [], args, &block) + def recursive_inject(memo, &block) # :yields: memo, env + inject_envs(memo, &block) end - # Returns the total number of unique envs nested in self (including self). - def count - envs(true).length - end - - # Processes and resets the input configurations for both root - # and self. Reconfiguration consists of the following steps: + # Activates self by doing the following, in order: # - # * partition overrides into env, root, and other configs - # * reconfigure root with the root configs - # * reconfigure self with the env configs - # * yield other configs to the block (if given) + # * sets Env.instance to self (unless already set) + # * activate nested environments + # * unshift load_paths to $LOAD_PATH # - # Reconfigure will always yields to the block, even if there - # are no non-root, non-env configurations. Unspecified - # configurations are NOT reconfigured. (Note this means - # that existing path configurations like load_paths will - # not automatically be reset using reconfigured root.) - def reconfigure(overrides={}) - check_configurable - - # partiton config into its parts - env_configs = {} - root_configs = {} - other_configs = {} - - env_configurations = self.class.configurations - root_configurations = root.class.configurations - overrides.each_pair do |key, value| - key = key.to_sym - - partition = case - when env_configurations.key?(key) then env_configs - when root_configurations.key?(key) then root_configs - else other_configs - end - - partition[key] = value - end - - # reconfigure root so it can resolve path_configs - root.reconfigure(root_configs) - - # reconfigure self - super(env_configs) - - # handle other configs - case - when block_given? - yield(other_configs) - when !other_configs.empty? - log(:warn, "ignoring non-env configs: #{other_configs.keys.join(',')}", Logger::DEBUG) - end - - self - end - - # Returns the path for self in Env.instances. - def env_path - Env.instances.each_pair {|path, env| return path if env == self } - nil - end - - # Logs the action and message at the input level (default INFO). - # Logging is suppressed if no logger is set. - def log(action, msg="", level=Logger::INFO) - logger.add(level, msg, action.to_s) if logger - end - - # Activates self by unshifting load_paths for self to the load_path_targets. - # Once active, self can be referenced from Env.instance and the current - # configurations are frozen. Env.instance is deactivated, if set, before - # self is activated. Returns true if activate succeeded, or false if self - # is already active. + # 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 - # freeze array configs like load_paths - config.each_pair do |key, value| - case value - when Array then value.freeze - end - end + # freeze envs and load paths + @envs.freeze + @load_paths.freeze # activate nested envs envs.reverse_each do |env| env.activate end - + # add load paths load_paths.reverse_each do |path| $LOAD_PATH.unshift(path) end - + $LOAD_PATH.uniq! - # perform requires - requires.each do |path| - require path - end - - # perform loads - loads.each do |path| - load path - end - 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 - # and the configurations are unfrozen (using duplication). + # Deactivates self by doing the following in order: # + # * deactivates nested environments + # * removes load_paths from $LOAD_PATH + # * 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. def deactivate return false unless active? + @active = false + # dectivate nested envs + envs.reverse_each do |env| + env.deactivate + 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) - end - end - @active = false - @manifests.clear + # unfreeze envs and load paths + @envs = @envs.dup + @load_paths = @load_paths.dup + + # clear cached data @@instance = nil if @@instance == self + @manifests.clear - # dectivate nested envs - envs.reverse_each do |env| - env.deactivate - 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_path + # 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_path(dir, path) + def search(dir, path, strict=true) 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 the - # directory, and that the file exists. - if file.rindex(directory, 0) == 0 && - File.exists?(file) && - (!block_given? || yield(file)) - return file + # check the file is relative to directory + if strict && file.rindex(directory, 0) != 0 + raise "not relative to search dir: #{file} (#{directory})" end + + # filter + return file if !block_given? || yield(file) end nil end - # def reset(name, &block) - # each do |env| - # env.manifests[name].each(&block) - # env.manifests[name] = nil - # end - # end - # TEMPLATES = {} TEMPLATES[:commands] = %Q{<% if count > 1 %> <%= env_name %>: <% end %> @@ -540,11 +461,11 @@ def recursive_inspect(template=nil, *args) # :yields: templater, attrs return "#<#{self.class}:#{object_id} root='#{root.root}'>" if template == nil attrs = {} templaters = [] - recursive_each(*args) do |env, *argv| + 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 @@ -562,49 +483,54 @@ def minikey(env) env.root.root 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 - # Resets envs using the current env_paths and gems. def reset_envs self.envs = env_paths.collect do |path| - Env.instance_for(path) + Env.instantiate(path) end + gems.collect do |spec| - Env.instance_for(spec.full_gem_path) + Env.instantiate(spec.full_gem_path) end end - # Recursively iterates through envs collecting all envs into - # the target. The result is a unique array of all nested - # envs, in order, beginning with self. - def flatten_envs(target=[]) - unless target.include?(self) - target << self + # 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 + end + + # Recursively iterates through envs, starting with self, and + # collects the visited envs in order. + def visit_envs(visited=[], &block) # :nodoc: + unless visited.include?(self) + visited << self + yield(self) if block_given? + envs.each do |env| - env.flatten_envs(target) + env.visit_envs(visited, &block) end end - target + visited 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) + # helper to recursively inject a memo to the children of env + 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 attr_reader :original_error, :env_path \ No newline at end of file