teapot 0.9.10 → 1.0.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -3
  3. data/README.md +3 -4
  4. data/Rakefile +3 -6
  5. data/bin/teapot +3 -7
  6. data/lib/teapot/build.rb +166 -18
  7. data/lib/teapot/configuration.rb +0 -1
  8. data/lib/teapot/context.rb +37 -21
  9. data/lib/teapot/controller/build.rb +24 -7
  10. data/lib/teapot/controller/fetch.rb +1 -1
  11. data/lib/teapot/controller.rb +1 -0
  12. data/lib/teapot/definition.rb +1 -1
  13. data/lib/teapot/dependency.rb +2 -6
  14. data/lib/teapot/environment/base.rb +7 -15
  15. data/lib/teapot/environment/constructor.rb +28 -0
  16. data/lib/teapot/environment/flatten.rb +42 -1
  17. data/lib/teapot/environment/system.rb +3 -3
  18. data/lib/teapot/extractors/linker_extractor.rb +2 -2
  19. data/lib/teapot/loader.rb +9 -11
  20. data/lib/teapot/name.rb +4 -0
  21. data/lib/teapot/package.rb +5 -4
  22. data/lib/teapot/repository.rb +29 -1
  23. data/lib/teapot/rule.rb +196 -0
  24. data/lib/teapot/rulebook.rb +91 -0
  25. data/lib/teapot/target.rb +15 -40
  26. data/lib/teapot/version.rb +1 -1
  27. data/{lib/teapot/build/targets/application.rb → spec/teapot/build_spec.rb} +26 -29
  28. data/{test/test_teapot.rb → spec/teapot/context_spec.rb} +13 -13
  29. data/spec/teapot/dependency_spec.rb +113 -0
  30. data/spec/teapot/environment_spec.rb +91 -0
  31. data/{lib/teapot/build/graph.rb → spec/teapot/name_spec.rb} +26 -26
  32. data/{test/test_substitutions.rb → spec/teapot/substitutions_spec.rb} +36 -36
  33. data/{test → spec/teapot}/teapot.rb +1 -1
  34. data/teapot.gemspec +16 -9
  35. metadata +98 -51
  36. data/lib/teapot/build/component.rb +0 -69
  37. data/lib/teapot/build/file_list.rb +0 -67
  38. data/lib/teapot/build/linker.rb +0 -49
  39. data/lib/teapot/build/target.rb +0 -76
  40. data/lib/teapot/build/targets/compiler.rb +0 -83
  41. data/lib/teapot/build/targets/directory.rb +0 -63
  42. data/lib/teapot/build/targets/executable.rb +0 -56
  43. data/lib/teapot/build/targets/external.rb +0 -91
  44. data/lib/teapot/build/targets/files.rb +0 -82
  45. data/lib/teapot/build/targets/library.rb +0 -117
  46. data/lib/teapot/commands.rb +0 -139
  47. data/lib/teapot/controller/run.rb +0 -43
  48. data/lib/teapot/graph.rb +0 -136
  49. data/test/test_dependency.rb +0 -112
  50. data/test/test_environment.rb +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 878219ed331477566d41ce4f34e5342bd20d3b54
4
- data.tar.gz: fb855b6abbc1225ea619a4ba4e17e821a8c37b62
3
+ metadata.gz: d5f2d266ab25fcae9ab8d5582147d0a74d05d747
4
+ data.tar.gz: 202f83855d31f83ac023107ef3e123e95af83795
5
5
  SHA512:
6
- metadata.gz: d12945e3ba6e0aa7debca0ebbe7642e4edad9b412df3d538f0aa7c4c1195afabad9204c5c9954d52d9563d690c0a8918795e0382c8324c55a86be7f1d6e6195d
7
- data.tar.gz: b2faf14aa6e156a92e041b082ce790de98425cab5f606f81c0705afea8327f1064540f14d24df84468189f5c6c375ddeef2ab29c5d93df244d730abac09fec08
6
+ metadata.gz: 4f3c17b8a55d5aab3a5adb1061d518fd45da2bef8a31471b8e7797169345e6a7fec9cb73441d6cf922e1efb16cbdc1fc8f57e8371866c9aa64c2ac019f373f98
7
+ data.tar.gz: 50318c19defce391b36cd24056d987eeb9f6c9a1f3c61df53ee53dd8e44fd6484b8effa576cf8791467178a77b96c6dd334d073e92eb522094b6512643b552b5
data/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - "1.9.2"
4
- - "1.9.3"
5
- - rbx-19mode
3
+ - "1.9"
4
+ - "2.0"
5
+ - "2.1"
6
+ matrix:
7
+ allow_failures:
8
+ - rvm: "1.9"
data/README.md CHANGED
@@ -60,9 +60,8 @@ You need to make sure any basic tools, e.g. compilers, system libraries, are ins
60
60
 
61
61
  ## Dependency Graph
62
62
 
63
- Dependency analysis is an important part of efficiently building a source tree. A dependency graph constructed from a single output file depends on the set of input files and their associated implicit dependencies (e.g. `#include` directives), and additionally the environment and task that is being performed. Specifically, the environment can specify additional header files, libraries, or other dependent input files and needs to be considered explicitly.
64
-
65
- Teapot assumes per-environment dependency graphs and in addition, parts of the dependency graph are generated dynamically. The process of extracting implicit dependencies can be found under `teapot/extractors` and is used extensively in the build graph `teapot/build`.
63
+ - Should packages be built into a shared prefix or should they be built into unique prefixes and joined together either via install or `-L` and `-I`?
64
+ - Should packages expose the tools required to build themselves as dependencies? e.g. should `build-cmake` as required by, say, `OpenCV`, be exposed to all who depend on `OpenCV`? Should there be a mechanism for non-public dependencies, i.e. dependencies which are not exposed to dependants?
66
65
 
67
66
  ## Contributing
68
67
 
@@ -76,7 +75,7 @@ Teapot assumes per-environment dependency graphs and in addition, parts of the d
76
75
 
77
76
  Released under the MIT license.
78
77
 
79
- Copyright, 2012, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
78
+ Copyright, 2012, 2014, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
80
79
 
81
80
  Permission is hereby granted, free of charge, to any person obtaining a copy
82
81
  of this software and associated documentation files (the "Software"), to deal
data/Rakefile CHANGED
@@ -1,9 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
- require "rake/testtask"
2
+ require "rspec/core/rake_task"
3
3
 
4
- Rake::TestTask.new do |t|
5
- t.libs << 'test'
6
- end
4
+ RSpec::Core::RakeTask.new(:spec)
7
5
 
8
- desc "Run tests"
9
- task :default => :test
6
+ task :default => :spec
data/bin/teapot CHANGED
@@ -27,7 +27,6 @@ require 'teapot/controller/create'
27
27
  require 'teapot/controller/fetch'
28
28
  require 'teapot/controller/generate'
29
29
  require 'teapot/controller/list'
30
- require 'teapot/controller/run'
31
30
  require 'teapot/controller/visualize'
32
31
 
33
32
  require 'teapot/repository'
@@ -39,6 +38,8 @@ OPTIONS = Trollop::options do
39
38
  version "teapot v#{Teapot::VERSION}"
40
39
 
41
40
  opt :only, "Only compiled direct dependencies."
41
+ opt :continuous, "Run the build graph continually.", :type => :boolean
42
+
42
43
  opt :in, "Work in the given directory.", :type => :string
43
44
  opt :unlock, "Don't use package lockfile when fetching."
44
45
 
@@ -67,11 +68,7 @@ module Application
67
68
  def self.build(targets = ARGV)
68
69
  make_controller.build(targets)
69
70
  end
70
-
71
- def self.run(targets = ARGV)
72
- make_controller.run(targets)
73
- end
74
-
71
+
75
72
  def self.list(only = ARGV)
76
73
  if only.size > 0
77
74
  make_controller.list(Set.new(only))
@@ -114,7 +111,6 @@ def track_time
114
111
  start_time = Time.now
115
112
 
116
113
  yield
117
-
118
114
  ensure
119
115
  end_time = Time.now
120
116
  elapsed_time = end_time - start_time
data/lib/teapot/build.rb CHANGED
@@ -18,34 +18,182 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require 'teapot/build/targets/directory'
22
- require 'teapot/build/targets/files'
23
- require 'teapot/build/targets/library'
24
- require 'teapot/build/targets/executable'
25
- require 'teapot/build/targets/application'
26
- require 'teapot/build/targets/external'
21
+ require 'teapot/rulebook'
22
+
23
+ require 'build/files'
24
+ require 'build/graph'
25
+ require 'build/makefile'
26
+
27
+ require 'teapot/name'
28
+
29
+ require 'process/group'
30
+ require 'system'
27
31
 
28
32
  module Teapot
29
33
  module Build
30
- def self.top(path)
31
- Targets::Directory.target(nil, path)
32
- end
34
+ Graph = ::Build::Graph
35
+ Files = ::Build::Files
36
+ Paths = ::Build::Files::Paths
37
+ Makefile = ::Build::Makefile
33
38
 
34
- module Helpers
35
- def build_directory(root, directory, *args)
36
- target = Build.top(root)
39
+ class Node < Graph::Node
40
+ def initialize(controller, rule, arguments, &block)
41
+ @arguments = arguments
42
+ @rule = rule
43
+
44
+ @callback = block
45
+
46
+ inputs, outputs = rule.files(arguments)
47
+
48
+ super(controller, inputs, outputs)
49
+ end
50
+
51
+ attr :arguments
52
+ attr :rule
53
+ attr :callback
54
+
55
+ def hash
56
+ [@rule.name, @arguments].hash
57
+ end
58
+
59
+ def eql?(other)
60
+ other.kind_of?(self.class) and @rule.eql?(other.rule) and @arguments.eql?(other.arguments)
61
+ end
62
+
63
+ def apply!(scope)
64
+ @rule.apply!(scope, @arguments)
65
+
66
+ if @callback
67
+ scope.instance_exec(@arguments, &@callback)
68
+ end
69
+ end
70
+ end
37
71
 
38
- target.add_directory(directory)
72
+ class Top < Graph::Node
73
+ def initialize(controller, task_class, &update)
74
+ @update = update
75
+ @task_class = task_class
76
+
77
+ super(controller, Paths::NONE, Paths::NONE)
78
+ end
79
+
80
+ attr :task_class
81
+
82
+ def apply!(scope)
83
+ scope.instance_exec(&@update)
84
+ end
85
+
86
+ # Top level nodes are always considered dirty. This ensures that enclosed nodes are run if they are dirty. The top level node has no inputs or outputs by default, so children who become dirty wouldn't mark it as dirty and thus wouldn't be run.
87
+ def requires_update?
88
+ true
89
+ end
90
+ end
39
91
 
40
- target.execute(:build, *args)
92
+ class Task < Graph::Task
93
+ def initialize(controller, walker, node, group = nil)
94
+ super(controller, walker, node)
95
+
96
+ @group = group
97
+
98
+ if wet?
99
+ #@file_system = FileUtils
100
+ @file_system = FileUtils::Verbose
101
+ else
102
+ @file_system = FileUtils::NoWrite
103
+ end
104
+ end
105
+
106
+ attr :file_system
107
+ alias fs file_system
108
+
109
+ def wet?
110
+ @group && @node.requires_update?
111
+ end
112
+
113
+ def update(rule, arguments, &block)
114
+ arguments = rule.normalize(arguments)
115
+
116
+ # A sub-graph for a particular build is isolated based on the task class used to instantiate it, so we use this as part of the key.
117
+ child_node = @controller.nodes.fetch([self.class, rule.name, arguments]) do |key|
118
+ @controller.nodes[key] = Node.new(@controller, rule, arguments, &block)
119
+ end
120
+
121
+ @children << child_node
122
+
123
+ child_node.update!(@walker)
124
+
125
+ return child_node.rule.result(arguments)
126
+ end
127
+
128
+ def run!(*arguments)
129
+ if wet?
130
+ # puts Rainbow("Scheduling #{arguments.inspect}").blue
131
+ status = @group.spawn(*arguments)
132
+ # puts Rainbow("Finished #{arguments.inspect} with status #{status}").blue
133
+
134
+ if status != 0
135
+ raise Graph::CommandFailure.new(arguments, status)
136
+ end
137
+ end
138
+ end
139
+
140
+ def visit
141
+ super do
142
+ @node.apply!(self)
143
+ end
41
144
  end
145
+ end
42
146
 
43
- def build_external(root, directory, *args, &block)
44
- target = Build.top(root)
147
+ class Controller < Graph::Controller
148
+ def initialize
149
+ @module = Module.new
150
+
151
+ @top = []
152
+
153
+ yield self
154
+
155
+ @top.freeze
156
+
157
+ @task_class = nil
158
+
159
+ super()
160
+ end
45
161
 
46
- target << Targets::External.new(target, directory, &block)
162
+ attr :top
163
+
164
+ # Because we do a depth first traversal, we can capture global state per branch, such as `@task_class`.
165
+ def traverse!(walker)
166
+ @top.each do |node|
167
+ # Capture the task class for each top level node:
168
+ @task_class = node.task_class
169
+
170
+ node.update!(walker)
171
+ end
172
+ end
173
+
174
+ def add_target(target, environment, &block)
175
+ task_class = Rulebook.for(environment).with(Task, environment: environment, target: target)
176
+
177
+ # Not sure if this is a good idea - makes debugging slightly easier.
178
+ Object.const_set("TaskClassFor#{Name.from_target(target.name).identifier}_#{self.object_id}", task_class)
179
+
180
+ @top << Top.new(self, task_class, &target.build)
181
+ end
182
+
183
+ def build_graph!
184
+ super do |walker, node|
185
+ @task_class.new(self, walker, node)
186
+ end
187
+ end
47
188
 
48
- target.execute(:build, *args)
189
+ def update!
190
+ group = Process::Group.new
191
+
192
+ super do |walker, node|
193
+ @task_class.new(self, walker, node, group)
194
+ end
195
+
196
+ group.wait
49
197
  end
50
198
  end
51
199
  end
@@ -25,7 +25,6 @@ require 'yaml/store'
25
25
 
26
26
  require 'teapot/context'
27
27
  require 'teapot/environment'
28
- require 'teapot/commands'
29
28
 
30
29
  require 'teapot/definition'
31
30
 
@@ -21,9 +21,11 @@
21
21
  require 'teapot/loader'
22
22
  require 'teapot/package'
23
23
 
24
+ require 'teapot/rulebook'
25
+
24
26
  module Teapot
25
- TEAPOT_FILE = "teapot.rb"
26
- DEFAULT_CONFIGURATION_NAME = 'default'
27
+ TEAPOT_FILE = 'teapot.rb'.freeze
28
+ DEFAULT_CONFIGURATION_NAME = 'default'.freeze
27
29
 
28
30
  class AlreadyDefinedError < StandardError
29
31
  def initialize(definition, previous)
@@ -39,33 +41,23 @@ module Teapot
39
41
 
40
42
  class Context
41
43
  def initialize(root, options = {})
42
- @root = Pathname(root)
44
+ @root = Path[root]
43
45
  @options = options
44
46
 
45
47
  @targets = {}
46
48
  @generators = {}
47
49
  @configurations = {}
48
50
  @projects = {}
51
+ @rules = Rulebook.new
49
52
 
50
53
  @dependencies = []
51
54
  @selection = Set.new
52
55
 
53
56
  @loaded = {}
54
57
 
55
- # Load the root package:
56
- defined = load(root_package)
57
-
58
- # Find the default configuration, if it exists:
59
- @default_configuration = defined.default_configuration
60
-
61
- if options[:configuration]
62
- @configuration = @configurations[options[:configuration]]
63
- else
64
- @configuration = @default_configuration
58
+ unless options[:fake]
59
+ load_root_package(options)
65
60
  end
66
-
67
- # Materialize the configuration:
68
- @configuration.materialize if @configuration
69
61
  end
70
62
 
71
63
  attr :root
@@ -78,6 +70,8 @@ module Teapot
78
70
  # All public configurations.
79
71
  attr :configurations
80
72
 
73
+ attr :rules
74
+
81
75
  # The context's primary configuration.
82
76
  attr :configuration
83
77
 
@@ -111,6 +105,7 @@ module Teapot
111
105
  end.compact
112
106
  end
113
107
 
108
+ # Add a definition to the current context.
114
109
  def << definition
115
110
  case definition
116
111
  when Target
@@ -139,6 +134,10 @@ module Teapot
139
134
  @project ||= definition
140
135
 
141
136
  @projects[definition.name] = definition
137
+ when Rule
138
+ AlreadyDefinedError.check(definition, @rules)
139
+
140
+ @rules << definition
142
141
  end
143
142
  end
144
143
 
@@ -146,9 +145,9 @@ module Teapot
146
145
  # In certain cases, a package record might be loaded twice. This typically occurs when multiple configurations are loaded in the same context, or if a package has already been loaded (as is typical with the root package).
147
146
  @loaded.fetch(package) do
148
147
  loader = Loader.new(self, package)
149
-
148
+
150
149
  loader.load(TEAPOT_FILE)
151
-
150
+
152
151
  # Load the definitions into the current context:
153
152
  loader.defined.each do |definition|
154
153
  self << definition
@@ -173,12 +172,29 @@ module Teapot
173
172
 
174
173
  return failed_to_load
175
174
  end
176
-
177
- private
178
-
175
+
179
176
  # The root package is a special package which is used to load definitions from a given root path.
180
177
  def root_package
181
178
  @root_package ||= Package.new(@root, "root")
182
179
  end
180
+
181
+ private
182
+
183
+ def load_root_package(options)
184
+ # Load the root package:
185
+ defined = load(root_package)
186
+
187
+ # Find the default configuration, if it exists:
188
+ @default_configuration = defined.default_configuration
189
+
190
+ if options[:configuration]
191
+ @configuration = @configurations[options[:configuration]]
192
+ else
193
+ @configuration = @default_configuration
194
+ end
195
+
196
+ # Materialize the configuration:
197
+ @configuration.materialize if @configuration
198
+ end
183
199
  end
184
200
  end
@@ -19,6 +19,7 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'teapot/controller'
22
+ require 'teapot/build'
22
23
 
23
24
  module Teapot
24
25
  class Controller
@@ -30,16 +31,32 @@ module Teapot
30
31
  if @options[:only]
31
32
  ordered = context.direct_targets(ordered)
32
33
  end
33
-
34
- ordered.each do |(target, dependency)|
35
- if target.respond_to?(:build!) and !@options[:dry]
36
- log "Building #{target.name} for dependency #{dependency}...".color(:cyan)
34
+
35
+ controller = Teapot::Build::Controller.new do |controller|
36
+ ordered.each do |(target, dependency)|
37
+ environment = target.environment_for_configuration(context.configuration)
37
38
 
38
- target.build!(context.configuration)
39
+ if target.build
40
+ controller.add_target(target, environment.flatten)
41
+ end
42
+ end
43
+ end
44
+
45
+ controller.run do
46
+ # The graph has been dirtied because files have changed, traverse and update it:
47
+ controller.update_with_log
48
+
49
+ # Only run once is asked:
50
+ unless @options[:continuous]
51
+ break
52
+ end
53
+
54
+ if $TEAPOT_DEBUG_GRAPH
55
+ controller.nodes.each do |key, node|
56
+ puts "#{node.status} #{node.inspect}"# unless node.clean?
57
+ end
39
58
  end
40
59
  end
41
-
42
- log "Completed build successfully.".color(:green)
43
60
 
44
61
  return chain, ordered
45
62
  end
@@ -28,7 +28,7 @@ module Teapot
28
28
  configuration = context.configuration
29
29
  unresolved = context.unresolved(configuration.packages)
30
30
  tries = 0
31
-
31
+
32
32
  while tries < @options[:maximum_fetch_depth]
33
33
  configuration.packages.each do |package|
34
34
  next if resolved.include? package
@@ -23,6 +23,7 @@ require 'teapot/version'
23
23
 
24
24
  require 'uri'
25
25
  require 'rainbow'
26
+ require 'rainbow/ext/string'
26
27
  require 'fileutils'
27
28
 
28
29
  module Teapot
@@ -37,7 +37,7 @@ module Teapot
37
37
 
38
38
  # The name of the definition:
39
39
  attr :name
40
-
40
+
41
41
  # A textual description of the definition, possibly in markdown format:
42
42
  attr :description, true
43
43
 
@@ -45,16 +45,12 @@ module Teapot
45
45
  if String === name_or_aliases || Symbol === name_or_aliases
46
46
  name = name_or_aliases
47
47
 
48
- if block_given?
49
- provisions[name] = Provision.new(block)
50
- else
51
- provisions[name] = Provision.new(nil)
52
- end
48
+ provisions[name] = Provision.new(block)
53
49
  else
54
50
  aliases = name_or_aliases
55
51
 
56
52
  aliases.each do |name, dependencies|
57
- provisions[name] = Alias.new(Array dependencies)
53
+ provisions[name] = Alias.new(Array(dependencies))
58
54
  end
59
55
  end
60
56
  end
@@ -19,10 +19,10 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Teapot
22
- # This is the basic environment data structure which is essentially a linked list of hashes. It is primarily used for organising build configurations across a wide range of different sub-systems, e.g. platform configuration, target configuration, local project configuration, etc. The majority of the actual functionality is exposed in the `environment/*.rb` files.
22
+ # This is the basic environment data structure which is essentially a linked list of hashes. It is primarily used for organising build configurations across a wide range of different sub-systems, e.g. platform configuration, target configuration, local project configuration, etc.
23
23
  class Environment
24
- def initialize(parent = nil, values = {}, &block)
25
- @values = (values || {}).to_hash
24
+ def initialize(parent = nil, values = nil, &block)
25
+ @values = (values || {}).to_h
26
26
  @parent = parent
27
27
 
28
28
  if block_given?
@@ -30,6 +30,10 @@ module Teapot
30
30
  end
31
31
  end
32
32
 
33
+ def self.hash(**values)
34
+ self.new(nil, values)
35
+ end
36
+
33
37
  attr :values
34
38
  attr :parent
35
39
 
@@ -51,20 +55,8 @@ module Teapot
51
55
  @values[key] = value
52
56
  end
53
57
 
54
- def to_hash
55
- @values
56
- end
57
-
58
58
  def to_s
59
59
  "<#{self.class} #{self.values}>"
60
60
  end
61
-
62
- def inspect(output = $stdout, indent = "")
63
- @values.each do |(key, value)|
64
- output.puts "#{indent}#{key}: #{value}"
65
- end
66
-
67
- @parent.inspect(output, indent + "\t") if @parent
68
- end
69
61
  end
70
62
  end
@@ -23,6 +23,20 @@ module Teapot
23
23
  Default = Struct.new(:value)
24
24
  Replace = Struct.new(:value)
25
25
 
26
+ class Define
27
+ def initialize(klass, &block)
28
+ @klass = klass
29
+ @block = block
30
+ end
31
+
32
+ attr :klass
33
+ attr :block
34
+
35
+ def to_s
36
+ "<#{@klass.name} #{@block.source_location.join(':')}>"
37
+ end
38
+ end
39
+
26
40
  class Constructor
27
41
  def initialize(environment)
28
42
  @environment = environment
@@ -44,14 +58,28 @@ module Teapot
44
58
 
45
59
  def default(name)
46
60
  @environment[name] = Default.new(@environment[name])
61
+
62
+ return name
47
63
  end
48
64
 
49
65
  def replace(name)
50
66
  @environment[name] = Replace.new(@environment[name])
67
+
68
+ return name
51
69
  end
52
70
 
53
71
  def append(name)
54
72
  @environment[name] = Array(@environment[name])
73
+
74
+ return name
75
+ end
76
+
77
+ def define(klass, name, &block)
78
+ abort "#{name} isn't a string when defining #{klass}" unless String === name
79
+
80
+ @environment[name] = Define.new(klass, &block)
81
+
82
+ return name
55
83
  end
56
84
  end
57
85
 
@@ -18,9 +18,15 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
+ require 'digest/md5'
22
+
21
23
  module Teapot
22
24
  class Environment
23
- def flatten
25
+ def to_h
26
+ @values
27
+ end
28
+
29
+ def to_hash
24
30
  hash = {}
25
31
 
26
32
  # Flatten this chain of environments:
@@ -33,8 +39,43 @@ module Teapot
33
39
  Hash[hash.map{|key, value| [key, evaluator.object_value(value)]}]
34
40
  end
35
41
 
42
+ def flatten
43
+ self.class.new(nil, self.to_hash)
44
+ end
45
+
46
+ def defined
47
+ @values.select{|name,value| Define === value}
48
+ end
49
+
50
+ def inspect(output = $stdout, indent = "")
51
+ @values.each do |(key, value)|
52
+ output.puts "#{indent}#{key}: #{value}"
53
+ end
54
+
55
+ @parent.inspect(output, indent + "\t") if @parent
56
+ end
57
+
58
+ # This should be stable within environments that produce the same results.
59
+ def checksum
60
+ digester = Digest::MD5.new
61
+
62
+ checksum_recursively(digester)
63
+
64
+ return digester.hexdigest
65
+ end
66
+
36
67
  protected
37
68
 
69
+ def checksum_recursively(digester)
70
+ @values.each do |(key, value)|
71
+ digester.update(key.to_s)
72
+ digester.update(value.to_s)
73
+ end
74
+
75
+ @parent.checksum_recursively(digester) if @parent
76
+ end
77
+
78
+ # We fold in the ancestors one at a time from oldest to youngest.
38
79
  def flatten_to_hash(hash)
39
80
  if @parent
40
81
  @parent.flatten_to_hash(hash)