/
project.rb
320 lines (275 loc) · 9.74 KB
/
project.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
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
require "digest"
module Rake
class Pipeline
# A Project controls the lifecycle of a series of Pipelines,
# creating them from an Assetfile and recreating them if the
# Assetfile changes.
class Project
# @return [Pipeline] the list of pipelines in the project
attr_reader :pipelines
attr_reader :maps
# @return [String|nil] the path to the project's Assetfile
# or nil if it was created without an Assetfile.
attr_reader :assetfile_path
# @return [String|nil] the digest of the Assetfile the
# project was created with, or nil if the project
# was created without an Assetfile.
attr_reader :assetfile_digest
# @return [String] the directory path for temporary files
attr_reader :tmpdir
# @return [String] the directory path where pipelines will
# write their outputs by default
attr_reader :default_output_root
# @return [Array] a list of filters to be applied before
# the specified filters in every pipeline
attr_writer :before_filters
# @return [Array] a list of filters to be applied after
# the specified filters in every pipeline
attr_writer :after_filters
class << self
# Configure a new project by evaluating a block with the
# Rake::Pipeline::DSL::ProjectDSL class.
#
# @see Rake::Pipeline::Filter Rake::Pipeline::Filter
#
# @example
# Rake::Pipeline::Project.build do
# tmpdir "tmp"
# output "public"
#
# input "app/assets" do
# concat "app.js"
# end
# end
#
# @return [Rake::Pipeline::Project] the newly configured project
def build(&block)
project = new
project.build(&block)
end
# @return [Array[String]] an array of strings that will be
# appended to {#digested_tmpdir}.
def digest_additions
@digest_additions ||= []
end
# Set {.digest_additions} to a sorted copy of the given array.
def digest_additions=(additions)
@digest_additions = additions.sort
end
# Add a value to the list of strings to append to the digest
# temp directory. Libraries can use this to add (for example)
# their version numbers so that the pipeline will be rebuilt
# if the library version changes.
#
# @example
# Rake::Pipeline::Project.add_to_digest(Rake::Pipeline::Web::Filters::VERSION)
#
# @param [#to_s] str a value to append to {#digested_tmpdir}.
def add_to_digest(str)
self.digest_additions << str.to_s
self.digest_additions.sort!
end
end
# @param [String|Pipeline] assetfile_or_pipeline
# if this a String, create a Pipeline from the Assetfile at
# that path. If it's a Pipeline, just wrap that pipeline.
def initialize(assetfile_or_pipeline=nil)
reset!
if assetfile_or_pipeline.kind_of?(String)
@assetfile_path = File.expand_path(assetfile_or_pipeline)
rebuild_from_assetfile(@assetfile_path)
elsif assetfile_or_pipeline
@pipelines << assetfile_or_pipeline
end
end
# Evaluate a block using the Rake::Pipeline::DSL::ProjectDSL
# DSL against an existing project.
def build(&block)
DSL::ProjectDSL.evaluate(self, &block) if block
self
end
# Invoke all of the project's pipelines, detecting any changes
# to the Assetfile and rebuilding the pipelines if necessary.
#
# @return [void]
# @see Rake::Pipeline#invoke
def invoke
@invoke_mutex.synchronize do
last_manifest.read_manifest
if dirty?
rebuild_from_assetfile(assetfile_path) if assetfile_dirty?
pipelines.each(&:invoke)
manifest.write_manifest
end
end
end
# Remove the project's temporary and output files.
def clean
files_to_clean.each { |file| FileUtils.rm_rf(file) }
end
# Clean out old tmp directories from the pipeline's
# {Rake::Pipeline#tmpdir}.
#
# @return [void]
def cleanup_tmpdir
obsolete_tmpdirs.each { |dir| FileUtils.rm_rf(dir) }
end
# Set the default output root of this project and expand its path.
#
# @param [String] root this pipeline's output root
def default_output_root=(root)
@default_output_root = File.expand_path(root)
end
# Set the temporary directory for this project and expand its path.
#
# @param [String] root this project's temporary directory
def tmpdir=(dir)
@tmpdir = File.expand_path(dir)
end
# @return [String] A subdirectory of {#tmpdir} with the digest of
# the Assetfile's contents and any {.digest_additions} in its
# name.
def digested_tmpdir
suffix = assetfile_digest
unless self.class.digest_additions.empty?
suffix += "-#{self.class.digest_additions.join('-')}"
end
File.join(tmpdir, "rake-pipeline-#{suffix}")
end
# @return Array[String] a list of the paths to temporary directories
# that don't match the pipline's Assetfile digest.
def obsolete_tmpdirs
if File.directory?(tmpdir)
Dir["#{tmpdir}/rake-pipeline-*"].sort.reject do |dir|
dir == digested_tmpdir
end
else
[]
end
end
# @return Array[String] a list of files to delete to completely clean
# out a project's temporary and output files.
def files_to_clean
setup_pipelines
obsolete_tmpdirs + [digested_tmpdir] + output_files.map(&:fullpath)
end
# @return [Array[FileWrapper]] a list of the files that
# will be generated when this project is invoked.
def output_files
setup_pipelines
pipelines.map(&:output_files).flatten
end
# Build a new pipeline and add it to our list of pipelines.
def build_pipeline(input, glob=nil, &block)
pipeline = Rake::Pipeline.build({
:before_filters => @before_filters,
:after_filters => @after_filters,
:output_root => default_output_root,
:tmpdir => digested_tmpdir,
:project => self
}, &block)
if input.kind_of?(Array)
input.each { |x| pipeline.add_input(x) }
elsif input.kind_of?(Hash)
pipeline.inputs = input
else
pipeline.add_input(input, glob)
end
@pipelines << pipeline
pipeline
end
# @return [Manifest] the manifest to write dependency information
# to
def manifest
@manifest ||= Rake::Pipeline::Manifest.new(manifest_path)
end
# @return [Manifest] the manifest to write dependency information
# to
def last_manifest
@last_manifest ||= Rake::Pipeline::Manifest.new(manifest_path)
end
# @return [String] the path to the dynamic dependency manifest
def manifest_path
File.join(digested_tmpdir, "manifest.json")
end
private
# Reset this project's internal state to the default values.
#
# @return [void]
def reset!
@pipelines = []
@maps = {}
@tmpdir = "tmp"
@invoke_mutex = Mutex.new
@default_output_root = @assetfile_digest = @assetfile_path = nil
@manifest = @last_manifest = nil
end
# Reconfigure this project based on the Assetfile at path.
#
# @param [String] path the path to the Assetfile
# to use to configure the project.
# @param [String] source if given, this string is
# evaluated instead of reading the file at assetfile_path.
#
# @return [void]
def rebuild_from_assetfile(path, source=nil)
reset!
source ||= File.read(path)
@assetfile_digest = digest(source)
@assetfile_path = path
build { instance_eval(source, path, 1) }
end
# Setup the pipeline so its output files will be up to date.
def setup_pipelines
pipelines.map(&:setup_filters)
end
# @return [String] the SHA1 digest of the given string.
def digest(str)
Digest::SHA1.hexdigest(str)
end
def dirty?
assetfile_dirty? || files_dirty?
end
def assetfile_dirty?
if assetfile_path
source = File.read(assetfile_path)
digest(source) != assetfile_digest
else
false
end
end
# Returns true if any of these conditions are met:
# The pipeline hasn't been invoked yet
# The input files have been modified
# Any of the input files have been deleted
# There are new input files
def files_dirty?
return true if manifest.empty?
previous_files = manifest.files
input_files.each do |input_file|
if !File.exists? input_file
return true # existing input file has been deleted
elsif !previous_files[input_file]
return true # there is a new file in the pipeline
elsif File.mtime(input_file).to_i != previous_files[input_file]
return true # existing file has been changed
end
end
false
end
def input_files
static_input_files = pipelines.collect do |p|
p.input_files.reject { |file| file.in_directory? tmpdir }.map(&:fullpath)
end.flatten
dynamic_input_files = static_input_files.collect do |file|
if manifest[file]
manifest[file].deps.keys
else
nil
end
end.flatten.compact
static_input_files + dynamic_input_files
end
end
end
end