/
compiler.rb
304 lines (258 loc) · 8.96 KB
/
compiler.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
module Nanoc::Int
# Responsible for compiling a site’s item representations.
#
# The compilation process makes use of notifications (see
# {Nanoc::Int::NotificationCenter}) to track dependencies between items,
# layouts, etc. The following notifications are used:
#
# * `compilation_started` — indicates that the compiler has started
# compiling this item representation. Has one argument: the item
# representation itself. Only one item can be compiled at a given moment;
# therefore, it is not possible to get two consecutive
# `compilation_started` notifications without also getting a
# `compilation_ended` notification in between them.
#
# * `compilation_ended` — indicates that the compiler has finished compiling
# this item representation (either successfully or with failure). Has one
# argument: the item representation itself.
#
# @api private
class Compiler
include Nanoc::Int::ContractsSupport
# @api private
attr_reader :site
# @api private
attr_reader :compiled_content_cache
# @api private
attr_reader :checksum_store
# @api private
attr_reader :rule_memory_store
# @api private
attr_reader :action_provider
# @api private
attr_reader :dependency_store
# @api private
attr_reader :outdatedness_checker
# @api private
attr_reader :reps
def initialize(site, compiled_content_cache:, checksum_store:, rule_memory_store:, action_provider:, dependency_store:, outdatedness_checker:, reps:)
@site = site
@compiled_content_cache = compiled_content_cache
@checksum_store = checksum_store
@rule_memory_store = rule_memory_store
@dependency_store = dependency_store
@outdatedness_checker = outdatedness_checker
@reps = reps
@action_provider = action_provider
end
def run_all
@action_provider.preprocess(@site)
build_reps
prune
run
@action_provider.postprocess(@site, @reps)
end
def run
load_stores
@site.freeze
# Determine which reps need to be recompiled
forget_dependencies_if_outdated
compile_reps
store
ensure
Nanoc::Int::TempFilenameFactory.instance.cleanup(
Nanoc::Filter::TMP_BINARY_ITEMS_DIR,
)
Nanoc::Int::TempFilenameFactory.instance.cleanup(
Nanoc::Int::ItemRepWriter::TMP_TEXT_ITEMS_DIR,
)
end
def load_stores
# FIXME: icky hack to update the dependency/checksum store’s list of objects
# (does not include preprocessed objects otherwise)
dependency_store.objects = site.items.to_a + site.layouts.to_a
checksum_store.objects = site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
stores.each(&:load)
end
# Store the modified helper data used for compiling the site.
#
# @return [void]
def store
# Calculate rule memory
(@reps.to_a + @site.layouts.to_a).each do |obj|
rule_memory_store[obj] = action_provider.memory_for(obj).serialize
end
# Calculate checksums
objects_to_checksum =
site.items.to_a + site.layouts.to_a + site.code_snippets + [site.config]
objects_to_checksum.each { |obj| checksum_store.add(obj) }
# Store
stores.each(&:store)
end
def build_reps
builder = Nanoc::Int::ItemRepBuilder.new(
site, action_provider, @reps
)
builder.run
end
# @param [Nanoc::Int::ItemRep] rep The item representation for which the
# assigns should be fetched
#
# @return [Hash] The assigns that should be used in the next filter/layout
# operation
#
# @api private
def assigns_for(rep, dependency_tracker)
content_or_filename_assigns =
if rep.binary?
{ filename: rep.snapshot_contents[:last].filename }
else
{ content: rep.snapshot_contents[:last].string }
end
view_context = create_view_context(dependency_tracker)
content_or_filename_assigns.merge(
item: Nanoc::ItemWithRepsView.new(rep.item, view_context),
rep: Nanoc::ItemRepView.new(rep, view_context),
item_rep: Nanoc::ItemRepView.new(rep, view_context),
items: Nanoc::ItemCollectionWithRepsView.new(site.items, view_context),
layouts: Nanoc::LayoutCollectionView.new(site.layouts, view_context),
config: Nanoc::ConfigView.new(site.config, view_context),
)
end
def create_view_context(dependency_tracker)
Nanoc::ViewContext.new(
reps: @reps,
items: @site.items,
dependency_tracker: dependency_tracker,
compiler: self,
)
end
# @api private
def filter_name_and_args_for_layout(layout)
mem = action_provider.memory_for(layout)
if mem.nil? || mem.size != 1 || !mem[0].is_a?(Nanoc::Int::ProcessingActions::Filter)
# FIXME: Provide a nicer error message
raise Nanoc::Int::Errors::Generic, "No rule memory found for #{layout.identifier}"
end
[mem[0].filter_name, mem[0].params]
end
private
def prune
if site.config[:prune][:auto_prune]
Nanoc::Pruner.new(site.config, reps, exclude: prune_config_exclude).run
end
end
def prune_config
site.config[:prune] || {}
end
def prune_config_exclude
prune_config[:exclude] || {}
end
def compile_reps
# Assign snapshots
@reps.each do |rep|
rep.snapshot_defs = action_provider.snapshots_defs_for(rep)
end
# Find item reps to compile and compile them
outdated_reps = @reps.select { |r| outdatedness_checker.outdated?(r) }
selector = Nanoc::Int::ItemRepSelector.new(outdated_reps)
selector.each do |rep|
handle_errors_while(rep) { compile_rep(rep) }
end
end
def handle_errors_while(item_rep)
yield
rescue => e
raise Nanoc::Int::Errors::CompilationError.new(e, item_rep)
end
# Compiles the given item representation.
#
# This method should not be called directly; please use
# {Nanoc::Int::Compiler#run} instead, and pass this item representation's item
# as its first argument.
#
# @param [Nanoc::Int::ItemRep] rep The rep that is to be compiled
#
# @return [void]
def compile_rep(rep)
fiber = fiber_for(rep)
while fiber.alive?
Nanoc::Int::NotificationCenter.post(:compilation_started, rep)
res = fiber.resume
case res
when Nanoc::Int::Errors::UnmetDependency
Nanoc::Int::NotificationCenter.post(:compilation_suspended, rep, res)
raise(res)
when Proc
fiber.resume(res.call)
else
# TODO: raise
end
end
Nanoc::Int::NotificationCenter.post(:compilation_ended, rep)
end
contract Nanoc::Int::ItemRep => Fiber
def fiber_for(rep)
@fibers ||= {}
@fibers[rep] ||=
Fiber.new do
begin
dependency_tracker = Nanoc::Int::DependencyTracker.new(@dependency_store)
dependency_tracker.enter(rep.item)
if can_reuse_content_for_rep?(rep)
Nanoc::Int::NotificationCenter.post(:cached_content_used, rep)
rep.snapshot_contents = compiled_content_cache[rep]
else
recalculate_content_for_rep(rep, dependency_tracker)
end
rep.compiled = true
compiled_content_cache[rep] = rep.snapshot_contents
@fibers.delete(rep)
ensure
dependency_tracker.exit
end
end
@fibers[rep]
end
# @return [Boolean]
def can_reuse_content_for_rep?(rep)
!outdatedness_checker.outdated?(rep) && compiled_content_cache[rep]
end
# @return [void]
def recalculate_content_for_rep(rep, dependency_tracker)
executor = Nanoc::Int::Executor.new(self, dependency_tracker)
action_provider.memory_for(rep).each do |action|
case action
when Nanoc::Int::ProcessingActions::Filter
executor.filter(rep, action.filter_name, action.params)
when Nanoc::Int::ProcessingActions::Layout
executor.layout(rep, action.layout_identifier, action.params)
when Nanoc::Int::ProcessingActions::Snapshot
executor.snapshot(rep, action.snapshot_name, final: action.final?, path: action.path)
else
raise "Internal inconsistency: unknown action #{action.inspect}"
end
end
end
# Clears the list of dependencies for items that will be recompiled.
#
# @return [void]
def forget_dependencies_if_outdated
@site.items.each do |i|
if @reps[i].any? { |r| outdatedness_checker.outdated?(r) }
@dependency_store.forget_dependencies_for(i)
end
end
end
# Returns all stores that can load/store data that can be used for
# compilation.
def stores
[
checksum_store,
compiled_content_cache,
@dependency_store,
rule_memory_store,
]
end
end
end