tap 0.12.4 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. data/History +34 -0
  2. data/README +62 -41
  3. data/bin/tap +36 -40
  4. data/cmd/console.rb +14 -6
  5. data/cmd/manifest.rb +62 -58
  6. data/cmd/run.rb +49 -31
  7. data/doc/API +84 -0
  8. data/doc/Class Reference +83 -115
  9. data/doc/Examples/Command Line +36 -0
  10. data/doc/Examples/Workflow +40 -0
  11. data/lib/tap/app.rb +293 -214
  12. data/lib/tap/app/node.rb +43 -0
  13. data/lib/tap/app/queue.rb +77 -0
  14. data/lib/tap/app/stack.rb +16 -0
  15. data/lib/tap/app/state.rb +22 -0
  16. data/lib/tap/constants.rb +2 -2
  17. data/lib/tap/env.rb +400 -314
  18. data/lib/tap/env/constant.rb +227 -0
  19. data/lib/tap/env/gems.rb +63 -0
  20. data/lib/tap/env/manifest.rb +89 -0
  21. data/lib/tap/env/minimap.rb +292 -0
  22. data/lib/tap/{support → env}/string_ext.rb +2 -2
  23. data/lib/tap/exe.rb +113 -125
  24. data/lib/tap/join.rb +175 -0
  25. data/lib/tap/joins.rb +9 -0
  26. data/lib/tap/joins/switch.rb +44 -0
  27. data/lib/tap/joins/sync.rb +99 -0
  28. data/lib/tap/root.rb +100 -491
  29. data/lib/tap/root/utils.rb +220 -0
  30. data/lib/tap/{support → root}/versions.rb +31 -29
  31. data/lib/tap/schema.rb +248 -0
  32. data/lib/tap/schema/parser.rb +413 -0
  33. data/lib/tap/schema/utils.rb +82 -0
  34. data/lib/tap/support/intern.rb +19 -6
  35. data/lib/tap/support/templater.rb +8 -3
  36. data/lib/tap/task.rb +175 -171
  37. data/lib/tap/tasks/dump.rb +58 -0
  38. data/lib/tap/tasks/load.rb +62 -0
  39. metadata +30 -73
  40. data/cmd/destroy.rb +0 -27
  41. data/cmd/generate.rb +0 -27
  42. data/doc/Command Reference +0 -105
  43. data/doc/Syntax Reference +0 -234
  44. data/doc/Tutorial +0 -348
  45. data/lib/tap/dump.rb +0 -142
  46. data/lib/tap/file_task.rb +0 -384
  47. data/lib/tap/generator/arguments.rb +0 -13
  48. data/lib/tap/generator/base.rb +0 -176
  49. data/lib/tap/generator/destroy.rb +0 -60
  50. data/lib/tap/generator/generate.rb +0 -93
  51. data/lib/tap/generator/generators/command/command_generator.rb +0 -21
  52. data/lib/tap/generator/generators/command/templates/command.erb +0 -32
  53. data/lib/tap/generator/generators/config/config_generator.rb +0 -98
  54. data/lib/tap/generator/generators/generator/generator_generator.rb +0 -37
  55. data/lib/tap/generator/generators/generator/templates/task.erb +0 -27
  56. data/lib/tap/generator/generators/generator/templates/test.erb +0 -26
  57. data/lib/tap/generator/generators/root/root_generator.rb +0 -84
  58. data/lib/tap/generator/generators/root/templates/MIT-LICENSE +0 -22
  59. data/lib/tap/generator/generators/root/templates/README +0 -14
  60. data/lib/tap/generator/generators/root/templates/Rakefile +0 -84
  61. data/lib/tap/generator/generators/root/templates/Rapfile +0 -11
  62. data/lib/tap/generator/generators/root/templates/gemspec +0 -27
  63. data/lib/tap/generator/generators/root/templates/test/tap_test_helper.rb +0 -3
  64. data/lib/tap/generator/generators/task/task_generator.rb +0 -25
  65. data/lib/tap/generator/generators/task/templates/task.erb +0 -14
  66. data/lib/tap/generator/generators/task/templates/test.erb +0 -19
  67. data/lib/tap/generator/manifest.rb +0 -20
  68. data/lib/tap/generator/preview.rb +0 -69
  69. data/lib/tap/load.rb +0 -64
  70. data/lib/tap/spec.rb +0 -41
  71. data/lib/tap/support/aggregator.rb +0 -65
  72. data/lib/tap/support/audit.rb +0 -333
  73. data/lib/tap/support/constant.rb +0 -143
  74. data/lib/tap/support/constant_manifest.rb +0 -126
  75. data/lib/tap/support/dependencies.rb +0 -54
  76. data/lib/tap/support/dependency.rb +0 -44
  77. data/lib/tap/support/executable.rb +0 -198
  78. data/lib/tap/support/executable_queue.rb +0 -125
  79. data/lib/tap/support/gems.rb +0 -43
  80. data/lib/tap/support/join.rb +0 -144
  81. data/lib/tap/support/joins.rb +0 -12
  82. data/lib/tap/support/joins/switch.rb +0 -27
  83. data/lib/tap/support/joins/sync_merge.rb +0 -38
  84. data/lib/tap/support/manifest.rb +0 -171
  85. data/lib/tap/support/minimap.rb +0 -90
  86. data/lib/tap/support/node.rb +0 -176
  87. data/lib/tap/support/parser.rb +0 -450
  88. data/lib/tap/support/schema.rb +0 -385
  89. data/lib/tap/support/shell_utils.rb +0 -67
  90. data/lib/tap/test.rb +0 -77
  91. data/lib/tap/test/assertions.rb +0 -38
  92. data/lib/tap/test/env_vars.rb +0 -29
  93. data/lib/tap/test/extensions.rb +0 -73
  94. data/lib/tap/test/file_test.rb +0 -362
  95. data/lib/tap/test/file_test_class.rb +0 -15
  96. data/lib/tap/test/regexp_escape.rb +0 -87
  97. data/lib/tap/test/script_test.rb +0 -46
  98. data/lib/tap/test/script_tester.rb +0 -115
  99. data/lib/tap/test/subset_test.rb +0 -260
  100. data/lib/tap/test/subset_test_class.rb +0 -99
  101. data/lib/tap/test/tap_test.rb +0 -109
  102. data/lib/tap/test/utils.rb +0 -231
@@ -0,0 +1,43 @@
1
+ module Tap
2
+ class App
3
+
4
+ # Node adds the node API[link:files/doc/API.html] to objects responding
5
+ # to call. Additional helper methods are added to simplify the
6
+ # construction of workflows; they are not required by the API.
7
+ module Node
8
+
9
+ # The joins called when call completes
10
+ attr_accessor :joins
11
+
12
+ # An array of node dependencies
13
+ attr_reader :dependencies
14
+
15
+ # Interns a new node by extending the block with Node.
16
+ def self.intern(&block)
17
+ block.extend self
18
+ end
19
+
20
+ # Sets up required variables for extended objects.
21
+ def self.extended(obj) # :nodoc:
22
+ obj.instance_variable_set(:@joins, [])
23
+ obj.instance_variable_set(:@dependencies, [])
24
+ end
25
+
26
+ # Sets the block as a join for self.
27
+ def on_complete(&block) # :yields: result
28
+ self.joins << block if block
29
+ self
30
+ end
31
+
32
+ # Adds the dependency to self. Dependencies are resolved by an app
33
+ # during App#dispatch and must be valid nodes.
34
+ def depends_on(dependency)
35
+ raise "cannot depend on self" if dependency == self
36
+ unless dependencies.include?(dependency)
37
+ dependencies << dependency
38
+ end
39
+ self
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,77 @@
1
+ require 'monitor'
2
+
3
+ module Tap
4
+ class App
5
+
6
+ # Queue allows thread-safe enqueing and dequeing of nodes and inputs for
7
+ # execution.
8
+ class Queue < Monitor
9
+
10
+ attr_reader :queue
11
+
12
+ # Creates a new Queue
13
+ def initialize
14
+ super
15
+ @queue = []
16
+ end
17
+
18
+ # Clears self and returns an array of the enqueued methods and inputs,
19
+ # organized by round.
20
+ def clear
21
+ synchronize do
22
+ current = queue
23
+ @queue = []
24
+ current
25
+ end
26
+ end
27
+
28
+ # Returns the number of enqueued methods
29
+ def size
30
+ synchronize do
31
+ queue.size
32
+ end
33
+ end
34
+
35
+ # True if no methods are enqueued
36
+ def empty?
37
+ synchronize { size == 0 }
38
+ end
39
+
40
+ # Enqueues the method and inputs.
41
+ def enq(method, inputs)
42
+ synchronize do
43
+ queue.push [method, inputs]
44
+ end
45
+ end
46
+
47
+ # Enqueues the method and inputs, but to the top of the queue.
48
+ def unshift(method, inputs)
49
+ synchronize do
50
+ queue.unshift [method, inputs]
51
+ end
52
+ end
53
+
54
+ # Concats an array of [method, input] entries to self.
55
+ def concat(entries)
56
+ synchronize do
57
+ entries.each do |method, inputs|
58
+ enq(method, inputs)
59
+ end
60
+ end
61
+ end
62
+
63
+ # Dequeues the next method and inputs as an array like
64
+ # [method, inputs]. Returns nil if the queue is empty.
65
+ def deq
66
+ synchronize { queue.shift }
67
+ end
68
+
69
+ # Converts self to an array.
70
+ def to_a
71
+ synchronize do
72
+ queue.dup
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,16 @@
1
+ module Tap
2
+ class App
3
+
4
+ # The base of the application call stack.
5
+ class Stack
6
+
7
+ # Calls the node with the inputs:
8
+ #
9
+ # node.call(*inputs)
10
+ #
11
+ def call(node, inputs)
12
+ node.call(*inputs)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Tap
2
+ class App
3
+ # The constants defining the possible App states.
4
+ module State
5
+ READY = 0
6
+ RUN = 1
7
+ STOP = 2
8
+ TERMINATE = 3
9
+
10
+ module_function
11
+
12
+ # Returns a string corresponding to the input state value.
13
+ # Returns nil for unknown states.
14
+ #
15
+ # State.state_str(0) # => 'READY'
16
+ # State.state_str(12) # => nil
17
+ def state_str(state)
18
+ constants.inject(nil) {|str, s| const_get(s) == state ? s.to_s : str}
19
+ end
20
+ end
21
+ end
22
+ end
data/lib/tap/constants.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  module Tap
2
2
  MAJOR = 0
3
- MINOR = 12
4
- TINY = 4
3
+ MINOR = 17
4
+ TINY = 0
5
5
 
6
6
  VERSION="#{MAJOR}.#{MINOR}.#{TINY}"
7
7
  WEBSITE="http://tap.rubyforge.org"
data/lib/tap/env.rb CHANGED
@@ -1,257 +1,246 @@
1
- require 'tap/support/constant_manifest'
1
+ require 'tap/root'
2
+ require 'tap/env/manifest'
3
+ require 'tap/support/templater'
2
4
  autoload(:YAML, 'yaml')
3
5
 
4
6
  module Tap
5
- module Support
6
- autoload(:Templater, 'tap/support/templater')
7
- autoload(:Gems, 'tap/support/gems')
8
- end
9
-
10
- # Envs are locations on the filesystem that have resources associated with
11
- # them (commands, tasks, generators, etc). Envs may point to files, but it's
12
- # more commonly environments are set to a directory and resources are various
13
- # files within the directory.
14
- #
15
- #
16
- #--
17
- # Note that gems and env_paths reset envs -- custom modifications to envs will be lost
18
- # whenever these configs are reset.
7
+ # Env abstracts an execution environment that spans many directories.
19
8
  class Env
20
- include Enumerable
21
- include Configurable
22
- include Support::Minimap
23
-
9
+ autoload(:Gems, 'tap/env/gems')
10
+
24
11
  class << self
12
+ attr_writer :instance
25
13
 
26
- # Returns the active instance of Env.
27
- def instance
28
- @@instance
29
- end
30
-
31
- # A hash of (path, Env instance) pairs, generated by Env#instantiate. Used
32
- # to prevent infinite loops of Env dependencies by assigning a single Env
33
- # to a given path.
34
- def instances
35
- @@instances
14
+ def instance(auto_initialize=true)
15
+ @instance ||= (auto_initialize ? new : nil)
36
16
  end
37
17
 
38
- # Creates a new Env for the specified path and adds it to Env#instances, or
39
- # returns the existing instance for the path. Paths can point to an env config
40
- # file, or to a directory. If a directory is provided, instantiate treats
41
- # path as the DEFAULT_CONFIG_FILE in that directory. All paths are expanded.
42
- #
43
- # e1 = Env.instantiate("./path/to/config.yml")
44
- # e2 = Env.instantiate("./path/to/dir")
45
- #
46
- # Env.instances
47
- # # => {
48
- # # File.expand_path("./path/to/config.yml") => e1,
49
- # # File.expand_path("./path/to/dir/#{Tap::Env::DEFAULT_CONFIG_FILE}") => e2 }
50
- #
51
- # The Env is initialized using configurations read from the env config
52
- # file. An instance will be initialized regardless of whether the config
53
- # file or directory exists.
54
- def instantiate(path_or_root)
55
- path = config_path(path_or_root.kind_of?(Root) ? path_or_root.root : path_or_root)
56
- return instances[path] if instances.has_key?(path)
18
+ def from_gemspec(spec, context={})
19
+ path = spec.full_gem_path
20
+ basename = context[:basename]
57
21
 
58
- config = load_config(path)
59
- root = path_or_root.kind_of?(Root) ? path_or_root : File.dirname(path)
60
-
61
- # note the assignment of env to instances MUST occur
62
- # before reconfigure to prevent infinite looping
63
- (instances[path] = new(root)).reconfigure(config)
64
- end
65
-
66
- def manifest(name, &block) # :yields: env (and should return a manifest)
67
- name = name.to_sym
68
- define_method(name) do
69
- self.manifests[name] ||= block.call(self).bind(self, name)
22
+ dependencies = []
23
+ spec.dependencies.each do |dependency|
24
+ unless dependency.type == :runtime
25
+ next
26
+ end
27
+
28
+ unless gemspec = Gems.gemspec(dependency)
29
+ # this error may result when a dependency has
30
+ # been uninstalled for a particular gem
31
+ warn "missing gem dependency: #{dependency.to_s} (#{spec.full_name})"
32
+ next
33
+ end
34
+
35
+ if basename && !File.exists?(File.join(gemspec.full_gem_path, basename))
36
+ next
37
+ end
38
+
39
+ dependencies << gemspec
70
40
  end
71
- end
72
-
73
- private
74
-
75
- def config_path(path) # :nodoc:
76
- if File.directory?(path) || (!File.exists?(path) && File.extname(path) == "")
77
- path = File.join(path, DEFAULT_CONFIG_FILE)
41
+
42
+ config = {
43
+ 'root' => path,
44
+ 'gems' => dependencies,
45
+ 'load_paths' => spec.require_paths,
46
+ 'set_load_paths' => false
47
+ }
48
+
49
+ if context[:basename]
50
+ config.merge!(Env.load_config(File.join(path, context[:basename])))
78
51
  end
79
52
 
80
- File.expand_path(path)
53
+ new(config, context)
81
54
  end
82
55
 
83
- # helper to load path as YAML. load_file returns a hash if the path
84
- # loads to nil or false (as happens for empty files)
85
- def load_config(path) # :nodoc:
56
+ # Loads configurations from path as YAML. Returns an empty hash if the path
57
+ # loads to nil or false (as happens for empty files), or doesn't exist.
58
+ def load_config(path)
59
+ return {} unless path
60
+
86
61
  begin
87
- Root.trivial?(path) ? {} : (YAML.load_file(path) || {})
62
+ Root::Utils.trivial?(path) ? {} : (YAML.load_file(path) || {})
88
63
  rescue(Exception)
89
- raise Env::ConfigError.new($!, path)
64
+ raise ConfigError.new($!, path)
65
+ end
66
+ end
67
+
68
+ def scan(load_path, pattern='**/*.rb')
69
+ Dir.chdir(load_path) do
70
+ Dir.glob(pattern).each do |require_path|
71
+ next unless File.file?(require_path)
72
+
73
+ default_const_name = require_path.chomp('.rb').camelize
74
+
75
+ # note: the default const name has to be set here to allow for implicit
76
+ # constant attributes (because a dir is needed to figure the relative path).
77
+ # A conflict could arise if the same path is globed from two different
78
+ # dirs... no surefire solution.
79
+ document = Lazydoc[require_path]
80
+ case document.default_const_name
81
+ when nil then document.default_const_name = default_const_name
82
+ when default_const_name
83
+ else raise "found a conflicting default const name"
84
+ end
85
+
86
+ # scan for constants
87
+ Lazydoc::Document.scan(File.read(require_path)) do |const_name, type, comment|
88
+ const_name = default_const_name if const_name.empty?
89
+ constant = Constant.new(const_name, require_path, comment)
90
+ yield(type, constant)
91
+
92
+ ###############################################################
93
+ # [depreciated] manifest as a task key will be removed at 1.0
94
+ if type == 'manifest'
95
+ warn "depreciation: ::task should be used instead of ::manifest as a resource key (#{require_path})"
96
+ yield('task', constant)
97
+ end
98
+ ###############################################################
99
+ end
100
+ end
90
101
  end
91
102
  end
92
103
  end
104
+ self.instance = nil
93
105
 
94
- @@instance = nil
95
- @@instances = {}
96
-
97
- # The default config file path
98
- DEFAULT_CONFIG_FILE = "tap.yml"
106
+ include Enumerable
107
+ include Configurable
108
+ include Minimap
99
109
 
110
+ # Matches a compound registry search key. After the match, if the key is
111
+ # compound then:
112
+ #
113
+ # $1:: env_key
114
+ # $2:: key
115
+ #
116
+ # If the key is not compound, $2 is nil and $1 is the key.
117
+ COMPOUND_KEY = /^((?:[A-z]:(?:\/|\\))?.*?)(?::(.*))?$/
118
+
100
119
  # An array of nested Envs, by default comprised of the env_path
101
- # + gem environments (in that order). Nested environments are
102
- # activated/deactivated with self.
120
+ # + gem environments (in that order).
103
121
  attr_reader :envs
104
122
 
105
- # The Root directory structure for self.
106
- nest(:root, Tap::Root, :set_default => false)
123
+ attr_reader :context
124
+
125
+ attr_reader :manifests
107
126
 
108
- # Specify gems to load as nested Envs. Gems may be specified
127
+ # The Root directory structure for self.
128
+ nest(:root, Root, :set_default => false)
129
+
130
+ # Specify gems to add as nested Envs. Gems may be specified
109
131
  # by name and/or version, like 'gemname >= 1.2'; by default the
110
- # latest version of the gem is selected.
111
- #
112
- # Gems are immediately loaded (via gem) through this method.
132
+ # latest version of the gem is selected. Gems are not activated
133
+ # by Env.
113
134
  config_attr :gems, [] do |input|
114
- specs_by_name = {}
115
-
116
- input = YAML.load(input) if input.kind_of?(String)
117
- input = case input
135
+ input = yaml_load(input) if input.kind_of?(String)
136
+
137
+ @gems = case input
138
+ when false, nil, :NONE, :none
139
+ []
140
+ when :LATEST, :ALL
141
+ # latest and all, no filter
142
+ Gems.select_gems(input == :LATEST)
118
143
  when :latest, :all
119
- Support::Gems.select_gems(input == :latest) do |spec|
120
- env_config = File.join(spec.full_gem_path, Tap::Env::DEFAULT_CONFIG_FILE)
121
- File.exists?(env_config)
122
- end
123
- else input
124
- end
125
-
126
- @gems = [*input].compact.collect do |gem_name|
127
- spec = Support::Gems.gemspec(gem_name)
128
-
129
- case spec
130
- when nil then log(:warn, "unknown gem: #{gem_name}", Logger::WARN)
131
- else Env.instantiate(spec.full_gem_path)
144
+ # latest and all, filtering by basename
145
+ Gems.select_gems(input == :latest) do |spec|
146
+ basename == nil || File.exists?(File.join(spec.full_gem_path, basename))
132
147
  end
133
-
134
- (specs_by_name[spec.name] ||= []) << spec
135
- spec.name
136
- end.uniq
137
-
138
- # this song and dance is to ensure that the latest spec for a
139
- # given gem appears first in the manifest
140
- specs_by_name.each_pair do |name, specs|
141
- specs_by_name[name] = specs.uniq.sort_by {|spec| spec.version }.reverse
148
+ else
149
+ # resolve gem names manually
150
+ [*input].collect do |name|
151
+ Gems.gemspec(name)
152
+ end.compact
142
153
  end
143
-
144
- @gems.collect! do |name|
145
- specs_by_name[name]
146
- end.flatten!
147
-
154
+
148
155
  reset_envs
149
156
  end
150
157
 
151
- # Specify configuration files to load as nested Envs.
158
+ # Specify directories to load as nested Envs.
152
159
  config_attr :env_paths, [] do |input|
153
- input = YAML.load(input) if input.kind_of?(String)
154
- @env_paths = [*input].compact.collect do |path|
155
- Env.instantiate(root[path]).env_path
156
- end.uniq
160
+ @env_paths = resolve_paths(input)
157
161
  reset_envs
158
162
  end
159
163
 
160
- # Designate load paths.
161
- config_attr :load_paths, ["lib"] do |paths|
164
+ # Designates paths added to $LOAD_PATH on activation (see set_load_paths).
165
+ # These paths are also the default directories searched for resources.
166
+ config_attr :load_paths, [:lib] do |input|
162
167
  raise "load_paths cannot be modified once active" if active?
163
- @load_paths = resolve_paths(paths)
164
- end
165
-
166
- # Designate paths for discovering and executing commands.
167
- config_attr :command_paths, ["cmd"] do |paths|
168
- @command_paths = resolve_paths(paths)
169
- end
170
-
171
- # Designate paths for discovering generators.
172
- config_attr :generator_paths, ["lib"] do |paths|
173
- @generator_paths = resolve_paths(paths)
174
- end
175
-
176
- manifest(:commands) do |env|
177
- paths = []
178
- env.command_paths.each do |path_root|
179
- paths.concat env.root.glob(path_root)
180
- end
181
-
182
- paths = paths.sort_by {|path| File.basename(path) }
183
- Support::Manifest.new(paths)
168
+ @load_paths = resolve_paths(input)
184
169
  end
185
170
 
186
- manifest(:tasks) do |env|
187
- tasks = Support::ConstantManifest.new('manifest')
188
- env.load_paths.each do |path_root|
189
- tasks.register(path_root, '**/*.rb')
190
- end
191
- # tasks.cache = env.cache[:tasks]
192
- tasks
193
- end
194
-
195
- manifest(:generators) do |env|
196
- generators = Support::ConstantManifest.intern('generator') do |manifest, const|
197
- const.name.underscore.chomp('_generator')
198
- end
199
-
200
- env.generator_paths.each do |path_root|
201
- generators.register(path_root, '**/*_generator.rb')
202
- end
203
- # generators.cache = env.cache[:generators]
204
- generators
171
+ # If set to true load_paths are added to $LOAD_PATH on activation.
172
+ config_attr :set_load_paths, true do |input|
173
+ raise "set_load_paths cannot be modified once active" if active?
174
+ @set_load_paths = Configurable::Validation.boolean[input]
205
175
  end
206
176
 
207
- def initialize(path_root_or_config=Dir.pwd)
208
- @envs = []
177
+ # Initializes a new Env linked to the specified directory. A config file
178
+ # basename may be specified to load configurations from 'dir/basename' as
179
+ # YAML. If a basename is specified, the same basename will be used to
180
+ # load configurations for nested envs.
181
+ #
182
+ # Configurations may be manually provided in the place of dir. In that
183
+ # case, the same rules apply for loading configurations for nested envs,
184
+ # but no configurations will be loaded for the current instance.
185
+ #
186
+ # The cache is used internally to prevent infinite loops of nested envs,
187
+ # and to optimize the generation of manifests.
188
+ def initialize(config_or_dir=Dir.pwd, context={})
209
189
  @active = false
210
190
  @manifests = {}
211
-
212
- # initialize these for reset_env
213
- @gems = []
214
- @env_paths = []
191
+ @context = context
192
+
193
+ # setup root
194
+ config = nil
195
+ @root = case config_or_dir
196
+ when Root then config_or_dir
197
+ when String then Root.new(config_or_dir)
198
+ else
199
+ config = config_or_dir
200
+
201
+ if config.has_key?(:root) && config.has_key?('root')
202
+ raise "multiple values mapped to :root"
203
+ end
204
+
205
+ root = config.delete(:root) || config.delete('root') || Dir.pwd
206
+ root.kind_of?(Root) ? root : Root.new(root)
207
+ end
215
208
 
216
- @root = case path_root_or_config
217
- when Root then path_root_or_config
218
- when String then Root.new(path_root_or_config)
219
- else Root.new
209
+ if basename && !config
210
+ config = Env.load_config(File.join(@root.root, basename))
220
211
  end
221
212
 
222
- unless path_root_or_config.kind_of?(Hash)
223
- path_root_or_config = {}
213
+ if instance(@root.root)
214
+ raise "context already has an env for: #{@root.root}"
224
215
  end
225
- initialize_config(path_root_or_config)
226
- end
227
-
228
- # Clears manifests so they may be regenerated.
229
- def reset
230
- @manifests.clear
216
+ instances << self
217
+
218
+ # set these for reset_env
219
+ @gems = nil
220
+ @env_paths = nil
221
+ initialize_config(config || {})
231
222
  end
232
223
 
233
- # Returns the key for self in Env.instances.
234
- def env_path
235
- Env.instances.each_pair {|path, env| return path if env == self }
236
- nil
224
+ # The minikey for self (root.root).
225
+ def minikey
226
+ root.root
237
227
  end
238
228
 
239
229
  # Sets envs removing duplicates and instances of self. Setting envs
240
230
  # overrides any environments specified by env_path and gem.
241
231
  def envs=(envs)
242
232
  raise "envs cannot be modified once active" if active?
243
- @envs = envs.uniq.delete_if {|e| e == self }
233
+ @envs = envs.uniq.delete_if {|env| env == self }
244
234
  end
245
-
246
- # Unshifts env onto envs, removing duplicates.
247
- # Self cannot be unshifted onto self.
235
+
236
+ # Unshifts env onto envs. Self cannot be unshifted onto self.
248
237
  def unshift(env)
249
238
  unless env == self || envs[0] == env
250
239
  self.envs = envs.dup.unshift(env)
251
240
  end
252
241
  self
253
242
  end
254
-
243
+
255
244
  # Pushes env onto envs, removing duplicates.
256
245
  # Self cannot be pushed onto self.
257
246
  def push(env)
@@ -261,21 +250,23 @@ module Tap
261
250
  end
262
251
  self
263
252
  end
264
-
253
+
265
254
  # Passes each nested env to the block in order, starting with self.
266
255
  def each
267
256
  visit_envs.each {|e| yield(e) }
268
257
  end
269
-
258
+
270
259
  # Passes each nested env to the block in reverse order, ending with self.
271
260
  def reverse_each
272
261
  visit_envs.reverse_each {|e| yield(e) }
273
262
  end
274
263
 
275
264
  # Recursively injects the memo to each env of self. Each env in envs
276
- # receives the same memo from the parent.
265
+ # receives the same memo from the parent. This is different from the
266
+ # inject provided via Enumerable, where each subsequent env receives
267
+ # the memo from the previous, not the parent, env.
277
268
  #
278
- # a,b,c,d,e = ('a'..'e').collect {|name| Tap::Env.new(:name => name) }
269
+ # a,b,c,d,e = ('a'..'e').collect {|name| Env.new(:name => name) }
279
270
  #
280
271
  # a.push(b).push(c)
281
272
  # b.push(d).push(e)
@@ -302,7 +293,7 @@ module Tap
302
293
  #
303
294
  # * sets Env.instance to self (unless already set)
304
295
  # * activate nested environments
305
- # * unshift load_paths to $LOAD_PATH
296
+ # * unshift load_paths to $LOAD_PATH (if set_load_paths is true)
306
297
  #
307
298
  # Once active, the current envs and load_paths are frozen and cannot be
308
299
  # modified until deactivated. Returns true if activate succeeded, or
@@ -311,7 +302,9 @@ module Tap
311
302
  return false if active?
312
303
 
313
304
  @active = true
314
- @@instance = self if @@instance == nil
305
+ unless self.class.instance(false)
306
+ self.class.instance = self
307
+ end
315
308
 
316
309
  # freeze envs and load paths
317
310
  @envs.freeze
@@ -323,11 +316,13 @@ module Tap
323
316
  end
324
317
 
325
318
  # add load paths
326
- load_paths.reverse_each do |path|
327
- $LOAD_PATH.unshift(path)
328
- end
319
+ if set_load_paths
320
+ load_paths.reverse_each do |path|
321
+ $LOAD_PATH.unshift(path)
322
+ end
329
323
 
330
- $LOAD_PATH.uniq!
324
+ $LOAD_PATH.uniq!
325
+ end
331
326
 
332
327
  true
333
328
  end
@@ -335,7 +330,7 @@ module Tap
335
330
  # Deactivates self by doing the following in order:
336
331
  #
337
332
  # * deactivates nested environments
338
- # * removes load_paths from $LOAD_PATH
333
+ # * removes load_paths from $LOAD_PATH (if set_load_paths is true)
339
334
  # * sets Env.instance to nil (if set to self)
340
335
  # * clears cached manifest data
341
336
  #
@@ -353,15 +348,17 @@ module Tap
353
348
  # remove load paths
354
349
  load_paths.each do |path|
355
350
  $LOAD_PATH.delete(path)
356
- end
351
+ end if set_load_paths
357
352
 
358
353
  # unfreeze envs and load paths
359
354
  @envs = @envs.dup
360
355
  @load_paths = @load_paths.dup
361
356
 
362
357
  # clear cached data
363
- @@instance = nil if @@instance == self
364
- @manifests.clear
358
+ klass = self.class
359
+ if klass.instance(false) == self
360
+ klass.instance = nil
361
+ end
365
362
 
366
363
  true
367
364
  end
@@ -371,157 +368,238 @@ module Tap
371
368
  @active
372
369
  end
373
370
 
374
- # Searches each env for the first existing file or directory at
375
- # env.root.filepath(dir, path). Paths are expanded, and search
376
- # checks to make sure the file is, in fact, relative to env.root[dir].
377
- # An optional block may be used to check the file; the file will only
378
- # be returned if the block returns true.
379
- #
380
- # Returns nil if no file can be found.
381
- def search(dir, path, strict=true)
371
+ def hlob(dir, pattern="**/*")
372
+ results = {}
373
+ each do |env|
374
+ root = env.root
375
+ root.glob(dir, pattern).each do |path|
376
+ relative_path = root.relative_path(dir, path)
377
+ results[relative_path] ||= path
378
+ end
379
+ end
380
+ results
381
+ end
382
+
383
+ def glob(dir, pattern="**/*")
384
+ hlob(dir, pattern).values.sort!
385
+ end
386
+
387
+ def path(dir, *paths)
382
388
  each do |env|
383
- directory = env.root.filepath(dir)
384
- file = env.root.filepath(dir, path)
385
- next unless File.exists?(file)
389
+ path = env.root.path(dir, *paths)
390
+ return path if !block_given? || yield(path)
391
+ end
392
+ nil
393
+ end
394
+
395
+ # Retrieves a path associated with the inheritance hierarchy of an object.
396
+ # An array of modules (which naturally can include classes) are provided
397
+ # and module_path traverses each, forming paths like:
398
+ #
399
+ # path(dir, module_path, *paths)
400
+ #
401
+ # By default, 'module_path' is 'module.to_s.underscore', but modules can
402
+ # specify an alternative by providing a module_path method.
403
+ #
404
+ # The paths are yielded to the block and when the block returns true,
405
+ # the path will be returned. If no block is given, the first module path
406
+ # is returned. Returns nil if the block never returns true.
407
+ #
408
+ def module_path(dir, modules, *paths, &block)
409
+ paths.compact!
410
+ while current = modules.shift
411
+ module_path = if current.respond_to?(:module_path)
412
+ current.module_path
413
+ else
414
+ current.to_s.underscore
415
+ end
386
416
 
387
- # check the file is relative to directory
388
- if strict && file.rindex(directory, 0) != 0
389
- raise "not relative to search dir: #{file} (#{directory})"
417
+ if path = self.path(dir, module_path, *paths, &block)
418
+ return path
419
+ end
420
+ end
421
+
422
+ nil
423
+ end
424
+
425
+ # Returns the module_path traversing the inheritance hierarchy for the
426
+ # class of obj (or obj if obj is a Class). Included modules are not
427
+ # visited, only the superclasses.
428
+ def class_path(dir, obj, *paths, &block)
429
+ klass = obj.kind_of?(Class) ? obj : obj.class
430
+ superclasses = klass.ancestors - klass.included_modules
431
+ module_path(dir, superclasses, *paths, &block)
432
+ end
433
+
434
+ def registry(build=false)
435
+ builders.each_pair do |type, builder|
436
+ registry[type] ||= builder.call(self)
437
+ end if build
438
+
439
+ registries[minikey] ||= begin
440
+ registry = {}
441
+ load_paths.each do |load_path|
442
+ next unless File.directory?(load_path)
443
+
444
+ Env.scan(load_path) do |type, constant|
445
+ entries = registry[type.to_sym] ||= []
446
+ entries << constant
447
+ end
390
448
  end
391
449
 
392
- # filter
393
- return file if !block_given? || yield(file)
450
+ registry
451
+ end
452
+ end
453
+
454
+ def register(type, override=false, &block)
455
+ type = type.to_sym
456
+
457
+ # error for existing, or overwrite
458
+ case
459
+ when override
460
+ builders.delete(type)
461
+ registries.each {|root, registry| registry.delete(type) }
462
+ when builders.has_key?(type)
463
+ raise "a builder is already registered for: #{type.inspect}"
464
+ when registries.any? {|root, registry| registry.has_key?(type) }
465
+ raise "entries are already registered for: #{type.inspect}"
394
466
  end
395
467
 
396
- nil
468
+ builders[type] = block
397
469
  end
398
470
 
399
- #
400
- TEMPLATES = {}
401
- TEMPLATES[:commands] = %Q{<% if count > 1 %>
402
- <%= env_name %>:
403
- <% end %>
404
- <% entries.each do |name, const| %>
405
- <%= name.ljust(width) %>
406
- <% end %>}
407
- TEMPLATES[:tasks] = %Q{<% if count > 1 %>
408
- <%= env_name %>:
409
- <% end %>
410
- <% entries.each do |name, const| %>
411
- <% desc = const.document[const.name]['manifest'] %>
412
- <%= name.ljust(width) %><%= desc.empty? ? '' : ' # ' %><%= desc %>
413
- <% end %>}
414
- TEMPLATES[:generators] = %Q{<% if count > 1 %>
415
- <%= env_name %>:
416
- <% end %>
417
- <% entries.each do |name, const| %>
418
- <% desc = const.document[const.name]['generator'] %>
419
- <%= name.ljust(width) %><%= desc.empty? ? '' : ' # ' %><%= desc %>
420
- <% end %>}
421
-
422
- def summarize(name, template=TEMPLATES[name])
423
- count = 0
424
- width = 10
471
+ def manifest(type) # :yields: env
472
+ type = type.to_sym
425
473
 
426
- env_names = {}
427
- minimap.each do |env_name, env|
428
- env_names[env] = env_name
474
+ registry[type] ||= begin
475
+ builder = builders[type]
476
+ builder ? builder.call(self) : []
429
477
  end
430
478
 
431
- inspect(template) do |templater, share|
432
- env = templater.env
433
- entries = env.send(name).minimap
434
- next(false) if entries.empty?
435
-
436
- templater.env_name = env_names[env]
437
- templater.entries = entries
438
-
439
- count += 1
440
- entries.each do |entry_name, entry|
441
- width = entry_name.length if width < entry_name.length
479
+ manifests[type] ||= Manifest.new(self, type)
480
+ end
481
+
482
+ def [](type)
483
+ manifest(type)
484
+ end
485
+
486
+ def reset
487
+ manifests.clear
488
+ registries.clear
489
+ end
490
+
491
+ #--
492
+ # Environment-seek
493
+ def eeek(type, key)
494
+ key =~ COMPOUND_KEY
495
+ envs = if $2
496
+ # compound key, match for env
497
+ key = $2
498
+ [minimatch($1)].compact
499
+ else
500
+ # not a compound key, search all envs by iterating self
501
+ self
502
+ end
503
+
504
+ # traverse envs looking for the first
505
+ # manifest entry matching key
506
+ envs.each do |env|
507
+ if result = env.manifest(type).minimatch(key)
508
+ return [env, result]
442
509
  end
443
-
444
- share[:count] = count
445
- share[:width] = width
446
- true
447
510
  end
511
+
512
+ nil
448
513
  end
449
514
 
450
- def inspect(template=nil) # :yields: templater, attrs
451
- return "#<#{self.class}:#{object_id} root='#{root.root}'>" if template == nil
452
-
453
- attrs = {}
454
- collect do |env|
455
- templater = Support::Templater.new(template, :env => env)
456
- block_given? ? (yield(templater, attrs) ? templater : nil) : templater
457
- end.compact.collect do |templater|
458
- templater.build(attrs)
459
- end.join
515
+ # Searches across each for the first registered object minimatching key. A
516
+ # single env can be specified by using a compound key like 'env_key:key'.
517
+ #
518
+ # Returns nil if no matching object is found.
519
+ def seek(type, key, &block) # :yields: env, key
520
+ env, result = eeek(type, key, &block)
521
+ result
460
522
  end
461
523
 
462
- def recursive_inspect(template=nil, *args) # :yields: templater, attrs
463
- return "#<#{self.class}:#{object_id} root='#{root.root}'>" if template == nil
464
-
465
- attrs = {}
466
- templaters = []
467
- recursive_inject(args) do |argv, env|
468
- templater = Support::Templater.new(template, :env => env)
469
- next_args = block_given? ? yield(templater, attrs, *argv) : argv
470
- templaters << templater if next_args
471
-
472
- next_args
524
+ # All templaters are yielded to the block before any are built. This
525
+ # allows globals to be determined for all environments.
526
+ def inspect(template=nil, globals={}, filename=nil) # :yields: templater, globals
527
+ if template == nil
528
+ return "#<#{self.class}:#{object_id} root='#{root.root}'>"
473
529
  end
474
530
 
475
- templaters.collect do |templater|
476
- templater.build(attrs)
531
+ env_keys = minihash(true)
532
+ collect do |env|
533
+ templater = Support::Templater.new(template, :env => env, :env_key => env_keys[env])
534
+ yield(templater, globals) if block_given?
535
+ templater
536
+ end.collect! do |templater|
537
+ templater.build(globals, filename)
477
538
  end.join
478
539
  end
479
540
 
480
541
  protected
481
542
 
482
- # A hash of the manifests for self.
483
- attr_reader :manifests
543
+ def registries # :nodoc:
544
+ context[:registries] ||= {}
545
+ end
484
546
 
485
- def minikey(env)
486
- env.root.root
547
+ def basename # :nodoc:
548
+ context[:basename]
487
549
  end
488
550
 
489
- # Resets envs using the current env_paths and gems.
490
- def reset_envs
491
- self.envs = env_paths.collect do |path|
492
- Env.instantiate(path)
493
- end + gems.collect do |spec|
494
- Env.instantiate(spec.full_gem_path)
495
- end
551
+ def builders # :nodoc:
552
+ context[:builders] ||= {}
553
+ end
554
+
555
+ def instances # :nodoc:
556
+ context[:instances] ||= []
496
557
  end
497
558
 
498
- # Arrayifies, compacts, and resolves input paths using root, and
499
- # removes duplicates. In short
559
+ def instance(path) # :nodoc:
560
+ instances.find {|env| env.root.root == path }
561
+ end
562
+
563
+ # resets envs using the current env_paths and gems. does nothing
564
+ # until both env_paths and gems are set.
565
+ def reset_envs # :nodoc:
566
+ if env_paths && gems
567
+ self.envs = env_paths.collect do |path|
568
+ instance(path) || Env.new(path, context)
569
+ end + gems.collect do |spec|
570
+ instance(spec.full_gem_path) || Env.from_gemspec(spec, context)
571
+ end
572
+ end
573
+ end
574
+
575
+ # arrayifies, compacts, and resolves input paths using root, and
576
+ # removes duplicates. in short:
500
577
  #
501
578
  # resolve_paths ['lib', nil, 'lib', 'alt] # => [root['lib'], root['alt']]
502
579
  #
503
580
  def resolve_paths(paths) # :nodoc:
504
- paths = YAML.load(paths) if paths.kind_of?(String)
505
- [*paths].compact.collect {|path| root[path]}.uniq
581
+ paths = yaml_load(paths) if paths.kind_of?(String)
582
+ [*paths].compact.collect {|path| root[path] }.uniq
506
583
  end
507
-
508
- # Recursively iterates through envs, starting with self, and
509
- # collects the visited envs in order.
584
+
585
+ # helper to recursively iterate through envs, starting with self.
586
+ # visited envs are collected in order and are used to ensure a
587
+ # given env will only be visited once.
510
588
  def visit_envs(visited=[], &block) # :nodoc:
511
589
  unless visited.include?(self)
512
590
  visited << self
513
591
  yield(self) if block_given?
514
-
592
+
515
593
  envs.each do |env|
516
594
  env.visit_envs(visited, &block)
517
595
  end
518
596
  end
519
-
597
+
520
598
  visited
521
599
  end
522
-
600
+
523
601
  # helper to recursively inject a memo to the children of env
524
- def inject_envs(memo, visited=[], &block) # :nodoc:
602
+ def inject_envs(memo, visited=[], &block) # :nodoc:
525
603
  unless visited.include?(self)
526
604
  visited << self
527
605
  next_memo = yield(memo, self)
@@ -529,12 +607,20 @@ module Tap
529
607
  env.inject_envs(next_memo, visited, &block)
530
608
  end
531
609
  end
532
-
610
+
533
611
  visited
534
612
  end
535
613
 
536
- # Raised when there is a Env-level configuration error.
537
- class ConfigError < StandardError
614
+ private
615
+
616
+ # A 'quick' yaml load where empty strings will not cause YAML to autoload.
617
+ # This is a silly song and dance, but provides for optimal launch times.
618
+ def yaml_load(str) # :nodoc:
619
+ str.empty? ? false : YAML.load(str)
620
+ end
621
+
622
+ # Raised when there is a configuration error from Env.load_config.
623
+ class ConfigError < StandardError # :nodoc:
538
624
  attr_reader :original_error, :env_path
539
625
 
540
626
  def initialize(original_error, env_path)