rake-pipeline 0.5.0 → 0.7.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 (55) hide show
  1. data/.travis.yml +12 -0
  2. data/Gemfile +1 -0
  3. data/README.markdown +1 -1
  4. data/README.yard +61 -32
  5. data/Rakefile +9 -0
  6. data/bin/rakep +1 -24
  7. data/lib/generators/rake/pipeline/install/install_generator.rb +70 -0
  8. data/lib/rake-pipeline.rb +117 -53
  9. data/lib/rake-pipeline/cli.rb +56 -0
  10. data/lib/rake-pipeline/dsl.rb +3 -140
  11. data/lib/rake-pipeline/dsl/pipeline_dsl.rb +168 -0
  12. data/lib/rake-pipeline/dsl/project_dsl.rb +108 -0
  13. data/lib/rake-pipeline/dynamic_file_task.rb +188 -0
  14. data/lib/rake-pipeline/file_wrapper.rb +1 -1
  15. data/lib/rake-pipeline/filter.rb +45 -15
  16. data/lib/rake-pipeline/filters.rb +3 -1
  17. data/lib/rake-pipeline/filters/{concat.rb → concat_filter.rb} +0 -0
  18. data/lib/rake-pipeline/filters/ordering_concat_filter.rb +38 -0
  19. data/lib/rake-pipeline/filters/pipeline_finalizing_filter.rb +19 -0
  20. data/lib/rake-pipeline/graph.rb +178 -0
  21. data/lib/rake-pipeline/manifest.rb +63 -0
  22. data/lib/rake-pipeline/manifest_entry.rb +34 -0
  23. data/lib/rake-pipeline/matcher.rb +65 -30
  24. data/lib/rake-pipeline/middleware.rb +15 -12
  25. data/lib/rake-pipeline/precompile.rake +8 -0
  26. data/lib/rake-pipeline/project.rb +280 -0
  27. data/lib/rake-pipeline/railtie.rb +16 -1
  28. data/lib/rake-pipeline/server.rb +15 -0
  29. data/lib/rake-pipeline/version.rb +2 -2
  30. data/rake-pipeline.gemspec +2 -0
  31. data/spec/cli_spec.rb +71 -0
  32. data/spec/concat_filter_spec.rb +1 -27
  33. data/spec/{dsl_spec.rb → dsl/pipeline_dsl_spec.rb} +32 -18
  34. data/spec/dsl/project_dsl_spec.rb +41 -0
  35. data/spec/dynamic_file_task_spec.rb +111 -0
  36. data/spec/encoding_spec.rb +6 -8
  37. data/spec/file_wrapper_spec.rb +19 -2
  38. data/spec/filter_spec.rb +120 -22
  39. data/spec/graph_spec.rb +56 -0
  40. data/spec/manifest_entry_spec.rb +51 -0
  41. data/spec/manifest_spec.rb +67 -0
  42. data/spec/matcher_spec.rb +35 -2
  43. data/spec/middleware_spec.rb +123 -75
  44. data/spec/ordering_concat_filter_spec.rb +39 -0
  45. data/spec/pipeline_spec.rb +95 -34
  46. data/spec/project_spec.rb +293 -0
  47. data/spec/rake_acceptance_spec.rb +307 -67
  48. data/spec/rake_tasks_spec.rb +21 -0
  49. data/spec/spec_helper.rb +11 -48
  50. data/spec/support/spec_helpers/file_utils.rb +35 -0
  51. data/spec/support/spec_helpers/filters.rb +16 -0
  52. data/spec/support/spec_helpers/input_helpers.rb +23 -0
  53. data/spec/support/spec_helpers/memory_file_wrapper.rb +31 -0
  54. data/tools/perfs +107 -0
  55. metadata +100 -12
@@ -0,0 +1,12 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ree
6
+ - jruby
7
+ - rbx
8
+
9
+ notifications:
10
+ email:
11
+ - wycats@gmail.com
12
+ - dudley@steambone.org
data/Gemfile CHANGED
@@ -8,3 +8,4 @@ gem "flog"
8
8
  gem "simplecov", :require => false
9
9
  gem "yard"
10
10
  gem "rdiscount"
11
+ gem "pry"
@@ -1,4 +1,4 @@
1
1
  # Rake::Pipeline
2
2
 
3
3
  The canonical documentation for Rake::Pipeline is hosted at
4
- <a href="http://rubydoc.info/github/livingsocial/rake-pipeline/master/file/README.yard">rubydoc.info</a>
4
+ <a href="http://rubydoc.info/github/livingsocial/rake-pipeline/master/file/README.yard">rubydoc.info</a>.
@@ -6,45 +6,72 @@ output files based on input changes.
6
6
 
7
7
  = Usage
8
8
 
9
- The easiest way to use Rake::Pipeline is via a +Assetfile+ file in the
9
+ The easiest way to use Rake::Pipeline is via an +Assetfile+ file in the
10
10
  root of your project.
11
11
 
12
12
  A sample +Assetfile+ looks like this:
13
13
 
14
14
  !!!ruby
15
- input "assets"
16
15
  output "public"
17
16
 
18
- # this block will take all JS inputs, wrap them in a closure,
19
- # add some additional metadata, and concatenate them all into
20
- # application.scripts.js.
21
- match "*.js" do
22
- filter ClosureWrapper
23
- filter DataWrapper
24
- filter Rake::Pipeline::ConcatFilter, "application.scripts.js"
25
- end
17
+ input "assets" do
18
+ # this block will take all JS inputs, wrap them in a closure,
19
+ # add some additional metadata, and concatenate them all into
20
+ # application.scripts.js.
21
+ match "*.js" do
22
+ filter ClosureWrapper
23
+ filter DataWrapper
24
+ concat "application.scripts.js"
25
+ end
26
26
 
27
- # this block will take all HTML and CSS inputs, convert them
28
- # into JavaScript
29
- match "*/*.{html,css}" do
30
- filter DataWrapper
31
- filter Rake::Pipeline::ConcatFilter, "application.assets.js"
32
- end
27
+ # this block will take all HTML and CSS inputs, convert them
28
+ # into JavaScript
29
+ match "*/*.{html,css}" do
30
+ filter DataWrapper
31
+ concat "application.assets.js"
32
+ end
33
33
 
34
- match "*.js" do
35
- filter Rake::Pipeline::ConcatFilter, "application.js"
34
+ match "*.js" do
35
+ concat "application.js"
36
+ end
36
37
  end
37
38
 
38
- # copy any unprocessed files over to the output directory
39
- filter Rake::Pipeline::ConcatFilter
39
+ Each +input+ block defines a collection of files, and a pipeline
40
+ that transforms those files. Within each pipeline, you can specify
41
+ a series of filters to describe the transformations you'd like to
42
+ apply to the files.
43
+
44
+ = Upgrading from Previous Versions
45
+
46
+ The +Assetfile+ syntax has changed in version 0.6.0. In previous
47
+ versions, each +Assetfile+ defined a single pipeline, and +input+
48
+ statements would add input files to that pipeline. After version
49
+ 0.6.0, multiple pipelines can be defined in an +Assetfile+. The
50
+ +input+ method now takes a block, and this block defines a pipeline.
51
+ This means that any +match+ blocks or filters must be defined
52
+ inside an +input+ block, and no longer at the top level. For example,
53
+ this:
54
+
55
+ !!!ruby
56
+ # Prior to 0.6.0
57
+ output "public"
58
+ input "assets"
59
+
60
+ match "**/*.js" do
61
+ concat
62
+ end
40
63
 
41
- The available options are:
64
+ would now be written as:
42
65
 
43
- * {Rake::Pipeline::DSL#input input}: the directory containing your input files
44
- * {Rake::Pipeline::DSL#output output}: the directory to place your output files
45
- like to process
46
- * if you do not specify a block, the files will be
47
- copied over directly.
66
+ !!!ruby
67
+ # After 0.6.0
68
+ output "public"
69
+
70
+ input "assets" do
71
+ match "**/*.js" do
72
+ concat
73
+ end
74
+ end
48
75
 
49
76
  = Filters
50
77
 
@@ -78,7 +105,7 @@ If you had a series of input files like:
78
105
  * +app/javascripts/three.js+
79
106
 
80
107
  and you specified the +ConcatFilter+ in your
81
- +AssetFile+ like:
108
+ +Assetfile+ like:
82
109
 
83
110
  !!!ruby
84
111
  filter ConcatFilter, "application.js"
@@ -109,15 +136,17 @@ This will stop `Rake::Pipeline` from trying to interpret the
109
136
  input files as `UTF-8`, which obviously will not work on
110
137
  binary data.
111
138
 
112
- = Built-In Filters
139
+ = Filters
113
140
 
114
- At the current time, +Rake::Pipeline+ comes with a single built-in
115
- filter: {Rake::Pipeline::ConcatFilter}. Its implementation is
116
- the same as the `ConcatFilter` shown above.
141
+ +Rake::Pipeline+ comes with a built-in filter,
142
+ {Rake::Pipeline::ConcatFilter}. Its implementation is the same as the
143
+ +ConcatFilter+ above. Other filters that are useful for web development
144
+ like a +CoffeeScriptFilter+ and +SassFilter+ are available in
145
+ [rake-pipeline-web-filters](https://github.com/wycats/rake-pipeline-web-filters).
117
146
 
118
147
  = Preview Server
119
148
 
120
- To start up the preview server, run +rakep+. This will start up
149
+ To start up the preview server, run +rakep server+. This will start up
121
150
  a server that automatically recompiles files for you on the fly
122
151
  and serves up the files you need.
123
152
 
data/Rakefile CHANGED
@@ -3,10 +3,19 @@ require "bundler/gem_tasks"
3
3
 
4
4
  directory "doc"
5
5
 
6
+ desc "generate documentation"
6
7
  task :docs => Dir["lib/**"] do
7
8
  sh "devbin/yard doc --readme README.yard --hide-void-return"
8
9
  end
9
10
 
11
+ desc "generate a dependency graph from the documentation"
10
12
  task :graph => ["doc", :docs] do
11
13
  sh "devbin/yard graph --dependencies | dot -Tpng -o doc/arch.png"
12
14
  end
15
+
16
+ desc "run the specs"
17
+ task :spec do
18
+ sh "rspec -cfs spec"
19
+ end
20
+
21
+ task :default => :spec
data/bin/rakep CHANGED
@@ -1,27 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "rake-pipeline"
4
- require "rake-pipeline/middleware"
5
- require "rack/server"
6
-
7
- module Rake
8
- class Pipeline
9
- class Server < Rack::Server
10
- def app
11
- not_found = proc { [404, { "Content-Type" => "text/plain" }, ["not found"]] }
12
- config = "Assetfile"
13
-
14
- Middleware.new(not_found, config)
15
- end
16
- end
17
- end
18
- end
19
-
20
- if ARGV[0] == "build"
21
- config = "Assetfile"
22
- pipeline_source = File.read(config)
23
- pipeline = Rake::Pipeline.class_eval "build do\n#{pipeline_source}\nend", config, 1
24
- pipeline.invoke
25
- else
26
- Rake::Pipeline::Server.new.start
27
- end
4
+ Rake::Pipeline::CLI.start
@@ -0,0 +1,70 @@
1
+ module Rake
2
+ class Pipeline
3
+ class InstallGenerator < Rails::Generators::Base
4
+
5
+ desc "Install Rake::Pipeline in this Rails app"
6
+
7
+ def disable_asset_pipeline_railtie
8
+ say_status :config, "Updating configuration to remove asset pipeline"
9
+ gsub_file app, "require 'rails/all'", <<-RUBY.strip_heredoc
10
+ # Pick the frameworks you want:
11
+ require "active_record/railtie"
12
+ require "action_controller/railtie"
13
+ require "action_mailer/railtie"
14
+ require "active_resource/railtie"
15
+ require "rails/test_unit/railtie"
16
+ RUBY
17
+ end
18
+
19
+ # TODO: Support sprockets API
20
+ def disable_asset_pipeline_config
21
+ regex = /^\n?\s*#.*\n\s*(#\s*)?config\.assets.*\n/
22
+ gsub_file app, regex, ''
23
+ gsub_file Rails.root.join("config/environments/development.rb"), regex, ''
24
+ gsub_file Rails.root.join("config/environments/production.rb"), regex, ''
25
+ end
26
+
27
+ def remove_assets_group
28
+ regex = /^\n(#.*\n)+group :assets.*\n(.*\n)*?end\n/
29
+
30
+ gsub_file "Gemfile", regex, ''
31
+ end
32
+
33
+ def enable_assets_in_development
34
+ gsub_file "config/environments/development.rb", /^end/, "\n config.rake_pipeline_enabled = true\nend"
35
+ end
36
+
37
+ # TODO: Support asset-pipeline like API
38
+ def add_assetfile
39
+ create_file "Assetfile", <<-RUBY.strip_heredoc
40
+ # NOTE: The Assetfile will eventually be replaced with an asset-pipeline
41
+ # compatible API. This is mostly important so that plugins can easily
42
+ # inject into the pipeline.
43
+ #
44
+ # Depending on demand and how the API shakes out, we may retain the
45
+ # Assetfile API but pull in the information from the Rails API.
46
+
47
+ input "app/assets"
48
+ output "public"
49
+
50
+ match "*.js" do
51
+ concat "application.js"
52
+ end
53
+
54
+ match "*.css" do
55
+ concat "application.css"
56
+ end
57
+
58
+ # copy any remaining files
59
+ concat
60
+ RUBY
61
+ end
62
+
63
+ private
64
+ def app
65
+ @app ||= Rails.root.join("config/application.rb")
66
+ end
67
+ end
68
+ end
69
+ end
70
+
@@ -1,9 +1,15 @@
1
1
  require "rake-pipeline/file_wrapper"
2
2
  require "rake-pipeline/filter"
3
+ require "rake-pipeline/manifest_entry"
4
+ require "rake-pipeline/manifest"
5
+ require "rake-pipeline/dynamic_file_task"
3
6
  require "rake-pipeline/filters"
4
7
  require "rake-pipeline/dsl"
5
8
  require "rake-pipeline/matcher"
6
9
  require "rake-pipeline/error"
10
+ require "rake-pipeline/project"
11
+ require "rake-pipeline/cli"
12
+ require "rake-pipeline/graph"
7
13
 
8
14
  if defined?(Rails::Railtie)
9
15
  require "rake-pipeline/railtie"
@@ -77,7 +83,7 @@ module Rake
77
83
  # # first, wrap all JS files in a custom filter
78
84
  # filter ClosureFilter
79
85
  # # then, concatenate all JS files into a single file
80
- # filter Rake::Pipeline::ConcatFilter, "application.js"
86
+ # concat "application.js"
81
87
  # end
82
88
  #
83
89
  # # specify filters for css and scss files
@@ -89,7 +95,7 @@ module Rake
89
95
  # input.sub(/\.scss$/, 'css')
90
96
  # end
91
97
  # # then, concatenate all CSS files into a single file
92
- # filter Rake::Pipeline::ConcatFilter, "application.css"
98
+ # concat "application.css"
93
99
  # end
94
100
  #
95
101
  # # the remaining files not specified by a matcher (the
@@ -100,17 +106,15 @@ module Rake
100
106
  # # up until this point, as well as the HTML files.
101
107
  # end
102
108
  class Pipeline
103
- # @return [String] a glob representing the input files
104
- attr_accessor :input_glob
105
-
106
- # @return [String] the directory path for the input files.
107
- attr_reader :input_root
109
+ # @return [Hash[String, String]] the directory paths for the input files
110
+ # and their matching globs.
111
+ attr_accessor :inputs
108
112
 
109
113
  # @return [String] the directory path for the output files.
110
114
  attr_reader :output_root
111
115
 
112
- # @return [String] the directory path for temporary files.
113
- attr_reader :tmpdir
116
+ # @return [String] the directory path for temporary files
117
+ attr_accessor :tmpdir
114
118
 
115
119
  # @return [Array] an Array of Rake::Task objects. This
116
120
  # property is populated by the #generate_rake_tasks
@@ -126,10 +130,33 @@ module Rake
126
130
 
127
131
  attr_writer :input_files
128
132
 
129
- def initialize
130
- @filters = []
131
- @tmpdir = "tmp"
132
- @mutex = Mutex.new
133
+ # @return [Project] the Project that created this pipeline
134
+ attr_reader :project
135
+
136
+ # @param [Hash] options
137
+ # @option options [Hash] :inputs
138
+ # set the pipeline's {#inputs}.
139
+ # @option options [String] :tmpdir
140
+ # set the pipeline's {#tmpdir}.
141
+ # @option options [String] :output_root
142
+ # set the pipeline's {#output_root}.
143
+ # @option options [Rake::Application] :rake_application
144
+ # set the pipeline's {#rake_application}.
145
+ def initialize(options={})
146
+ @filters = []
147
+ @invoke_mutex = Mutex.new
148
+ @clean_mutex = Mutex.new
149
+ @inputs = options[:inputs] || {}
150
+ @tmpdir = options[:tmpdir] || "tmp"
151
+ @project = options[:project]
152
+
153
+ if options[:output_root]
154
+ self.output_root = options[:output_root]
155
+ end
156
+
157
+ if options[:rake_application]
158
+ self.rake_application = options[:rake_application]
159
+ end
133
160
  end
134
161
 
135
162
  # Build a new pipeline taking a block. The block will
@@ -142,7 +169,7 @@ module Rake
142
169
  # input "app/assets"
143
170
  # output "public"
144
171
  #
145
- # filter Rake::Pipeline::ConcatFilter, "app.js"
172
+ # concat "app.js"
146
173
  # end
147
174
  #
148
175
  # @see DSL the Rake::Pipeline::DSL documentation.
@@ -150,10 +177,21 @@ module Rake
150
177
  # the build block.
151
178
  #
152
179
  # @return [Rake::Pipeline] the newly configured pipeline
153
- def self.build(&block)
154
- pipeline = new
155
- DSL.evaluate(pipeline, &block) if block
156
- pipeline
180
+ def self.build(options={}, &block)
181
+ pipeline = new(options)
182
+ pipeline.build(options, &block)
183
+ end
184
+
185
+ # Evaluate a block using the Rake::Pipeline DSL against an
186
+ # existing pipeline.
187
+ #
188
+ # @see Rake::Pipeline.build
189
+ #
190
+ # @return [Rake::Pipeline] this pipeline with any modifications
191
+ # made by the given block.
192
+ def build(options={}, &block)
193
+ DSL::PipelineDSL.evaluate(self, options, &block) if block
194
+ self
157
195
  end
158
196
 
159
197
  @@tmp_id = 0
@@ -168,19 +206,12 @@ module Rake
168
206
  # @api private
169
207
  def copy(target_class=self.class, &block)
170
208
  pipeline = target_class.build(&block)
171
- pipeline.input_root = input_root
209
+ pipeline.inputs = inputs
172
210
  pipeline.tmpdir = tmpdir
173
211
  pipeline.rake_application = rake_application
174
212
  pipeline
175
213
  end
176
214
 
177
- # Set the input root of this pipeline and expand its path.
178
- #
179
- # @param [String] root this pipeline's input root
180
- def input_root=(root)
181
- @input_root = File.expand_path(root)
182
- end
183
-
184
215
  # Set the output root of this pipeline and expand its path.
185
216
  #
186
217
  # @param [String] root this pipeline's output root
@@ -195,7 +226,18 @@ module Rake
195
226
  @tmpdir = File.expand_path(dir)
196
227
  end
197
228
 
198
- # If you specify a glob for #input_glob, this method will
229
+ # Add an input directory, optionally filtering which files within
230
+ # the input directory are included.
231
+ #
232
+ # @param [String] root the input root directory; required
233
+ # @param [String] pattern a pattern to match within +root+;
234
+ # optional; defaults to "**/*"
235
+ def add_input(root, pattern = nil)
236
+ pattern ||= "**/*"
237
+ @inputs[root] = pattern
238
+ end
239
+
240
+ # If you specify #inputs, this method will
199
241
  # calculate the input files for the directory. If you supply
200
242
  # input_files directly, this method will simply return the
201
243
  # input_files you supplied.
@@ -207,13 +249,19 @@ module Rake
207
249
 
208
250
  assert_input_provided
209
251
 
210
- expanded_root = File.expand_path(input_root)
211
- files = Dir[File.join(expanded_root, input_glob)].select { |f| File.file?(f) }
252
+ result = []
212
253
 
213
- files.map do |file|
214
- relative_path = file.sub(%r{^#{Regexp.escape(expanded_root)}/}, '')
215
- FileWrapper.new(expanded_root, relative_path)
254
+ @inputs.each do |root, glob|
255
+ expanded_root = File.expand_path(root)
256
+ files = Dir[File.join(expanded_root, glob)].sort.select { |f| File.file?(f) }
257
+
258
+ files.each do |file|
259
+ relative_path = file.sub(%r{^#{Regexp.escape(expanded_root)}/}, '')
260
+ result << FileWrapper.new(expanded_root, relative_path)
261
+ end
216
262
  end
263
+
264
+ result.sort
217
265
  end
218
266
 
219
267
  # for Pipelines, this is every file, but it may be overridden
@@ -240,7 +288,10 @@ module Rake
240
288
  # @param [Array<Filter>] filters a list of filters
241
289
  # @return [void]
242
290
  def add_filters(*filters)
243
- filters.each { |filter| filter.rake_application = rake_application }
291
+ filters.each do |filter|
292
+ filter.rake_application = rake_application
293
+ filter.pipeline = self
294
+ end
244
295
  @filters.concat(filters)
245
296
  end
246
297
  alias add_filter add_filters
@@ -251,7 +302,7 @@ module Rake
251
302
  #
252
303
  # @return [void]
253
304
  def invoke
254
- @mutex.synchronize do
305
+ @invoke_mutex.synchronize do
255
306
  self.rake_application = Rake::Application.new unless @rake_application
256
307
 
257
308
  setup
@@ -266,8 +317,10 @@ module Rake
266
317
  #
267
318
  # @return [void]
268
319
  def invoke_clean
269
- @rake_tasks = @rake_application = nil
270
- invoke
320
+ @clean_mutex.synchronize do
321
+ @rake_tasks = @rake_application = nil
322
+ invoke
323
+ end
271
324
  end
272
325
 
273
326
  # Set up the filters and generate rake tasks. In general, this method
@@ -280,22 +333,6 @@ module Rake
280
333
  generate_rake_tasks
281
334
  end
282
335
 
283
- # A list of the output files that invoking this pipeline will
284
- # generate.
285
- #
286
- # @return [Array<FileWrapper>]
287
- def output_files
288
- @filters.last.output_files unless @filters.empty?
289
- end
290
-
291
- protected
292
- # Generate a new temporary directory name.
293
- #
294
- # @return [String] a unique temporary directory name
295
- def self.generate_tmpname
296
- "rake-pipeline-tmp-#{@@tmp_id += 1}"
297
- end
298
-
299
336
  # Set up the filters. This will loop through all of the filters for
300
337
  # the current pipeline and wire up their input_files and output_files.
301
338
  #
@@ -303,6 +340,7 @@ module Rake
303
340
  # set up as part of this process.
304
341
  #
305
342
  # @return [void]
343
+ # @api private
306
344
  def setup_filters
307
345
  last = @filters.last
308
346
 
@@ -327,6 +365,31 @@ module Rake
327
365
  end
328
366
  end
329
367
 
368
+ # A list of the output files that invoking this pipeline will
369
+ # generate.
370
+ #
371
+ # @return [Array<FileWrapper>]
372
+ def output_files
373
+ @filters.last.output_files unless @filters.empty?
374
+ end
375
+
376
+ # Add a final filter to the pipeline that will copy the
377
+ # pipeline's generated files to the output.
378
+ #
379
+ # @return [void]
380
+ # @api private
381
+ def finalize
382
+ add_filter(Rake::Pipeline::PipelineFinalizingFilter.new)
383
+ end
384
+
385
+ protected
386
+ # Generate a new temporary directory name.
387
+ #
388
+ # @return [String] a unique temporary directory name
389
+ def self.generate_tmpname
390
+ "rake-pipeline-tmp-#{@@tmp_id += 1}"
391
+ end
392
+
330
393
  # Generate a new temporary directory name under the main tmpdir.
331
394
  #
332
395
  # @return [void]
@@ -356,10 +419,11 @@ module Rake
356
419
  # @raise Rake::Pipeline::Error if input root or glob were missing.
357
420
  # @return [void]
358
421
  def assert_input_provided
359
- if !input_root || !input_glob
422
+ if inputs.empty?
360
423
  raise Rake::Pipeline::Error, "You cannot get input files without " \
361
424
  "first providing input files and an input root"
362
425
  end
363
426
  end
427
+
364
428
  end
365
429
  end