749ccb2afe842a34327d5f4079c5b2a6b6c7a1bb
[selectricity] / vendor / plugins / engines / lib / engines.rb
1 require 'logger'
2
3 # We need to know the version of Rails that we are running before we
4 # can override any of the dependency stuff, since Rails' own behaviour
5 # has changed over the various releases. We need to explicily make sure
6 # that the Rails::VERSION constant is loaded, because such things could
7 # not automatically be achieved prior to 1.1, and the location of the
8 # file moved in 1.1.1!
9 def load_rails_version
10   # At this point, we can't even rely on RAILS_ROOT existing, so we have to figure
11   # the path to RAILS_ROOT/vendor/rails manually
12   rails_base = File.expand_path(
13     File.join(File.dirname(__FILE__), # RAILS_ROOT/vendor/plugins/engines/lib
14     '..', # RAILS_ROOT/vendor/plugins/engines
15     '..', # RAILS_ROOT/vendor/plugins
16     '..', # RAILS_ROOT/vendor
17     'rails', 'railties', 'lib')) # RAILS_ROOT/vendor/rails/railties/lib
18   begin
19     load File.join(rails_base, 'rails', 'version.rb')
20     #puts 'loaded 1.1.1+ from vendor: ' + File.join(rails_base, 'rails', 'version.rb')
21   rescue MissingSourceFile # this means they DON'T have Rails 1.1.1 or later installed in vendor
22     begin
23       load File.join(rails_base, 'rails_version.rb')
24       #puts 'loaded 1.1.0- from vendor: ' + File.join(rails_base, 'rails_version.rb')
25     rescue MissingSourceFile # this means they DON'T have Rails 1.1.0 or previous installed in vendor
26       begin
27         # try and load version information for Rails 1.1.1 or later from the $LOAD_PATH
28         require 'rails/version'
29         #puts 'required 1.1.1+ from load path'
30       rescue LoadError
31         # try and load version information for Rails 1.1.0 or previous from the $LOAD_PATH
32         require 'rails_version'
33         #puts 'required 1.1.0- from load path'
34       end
35     end
36   end
37 end
38
39 # Actually perform the load
40 load_rails_version
41 #puts "Detected Rails version: #{Rails::VERSION::STRING}"
42
43 require 'engines/ruby_extensions'
44 # ... further files are required at the bottom of this file
45
46 # Holds the Rails Engine loading logic and default constants
47 module Engines
48
49   class << self
50     # Return the version string for this plugin
51     def version
52       "#{Version::Major}.#{Version::Minor}.#{Version::Release}"
53     end
54     
55     # For holding the rails configuration object
56     attr_accessor :rails_config
57     
58     # A flag to stop searching for views in the application
59     attr_accessor :disable_app_views_loading
60     
61     # A flag to stop code being mixed in from the application
62     attr_accessor :disable_app_code_mixing
63   end
64
65   # The DummyLogger is a class which might pass through to a real Logger
66   # if one is assigned. However, it can gracefully swallow any logging calls
67   # if there is now Logger assigned.
68   class LoggerWrapper
69     def initialize(logger=nil)
70       set_logger(logger)
71     end
72     # Assign the 'real' Logger instance that this dummy instance wraps around.
73     def set_logger(logger)
74       @logger = logger
75     end
76     # log using the appropriate method if we have a logger
77     # if we dont' have a logger, ignore completely.
78     def method_missing(name, *args)
79       if @logger && @logger.respond_to?(name)
80         @logger.send(name, *args)
81       end
82     end
83   end
84
85   LOGGER = Engines::LoggerWrapper.new
86
87   class << self
88     # Create a new Logger instance for Engines, with the given outputter and level    
89     def create_logger(outputter=STDOUT, level=Logger::INFO)
90       LOGGER.set_logger(Logger.new(outputter, level))
91     end
92     # Sets the Logger instance that Engines will use to send logging information to
93     def set_logger(logger)
94       Engines::LOGGER.set_logger(logger) # TODO: no need for Engines:: part
95     end
96     # Retrieves the current Logger instance
97     def log
98       Engines::LOGGER # TODO: no need for Engines:: part
99     end
100     alias :logger :log
101   end
102   
103   # An array of active engines. This should be accessed via the Engines.active method.
104   ActiveEngines = []
105   
106   # The root directory for engines
107   config :root, File.join(RAILS_ROOT, "vendor", "plugins")
108   
109   # The name of the public folder under which engine files are copied
110   config :public_dir, "engine_files"
111   
112   class << self
113   
114     # Initializes a Rails Engine by loading the engine's init.rb file and
115     # ensuring that any engine controllers are added to the load path.
116     # This will also copy any files in a directory named 'public'
117     # into the public webserver directory. Example usage:
118     #
119     #   Engines.start :login
120     #   Engines.start :login_engine  # equivalent
121     #
122     # A list of engine names can be specified:
123     #
124     #   Engines.start :login, :user, :wiki
125     #
126     # The engines will be loaded in the order given.
127     # If no engine names are given, all engines will be started.
128     #
129     # Options can include:
130     # * :copy_files => true | false
131     #
132     # Note that if a list of engines is given, the options will apply to ALL engines.
133     def start(*args)
134       
135       options = (args.last.is_a? Hash) ? args.pop : {}
136       
137       if args.empty?
138         start_all
139       else
140         args.each do |engine_name|
141           start_engine(engine_name, options)
142         end
143       end
144     end
145
146     # Starts all available engines. Plugins are considered engines if they
147     # include an init_engine.rb file, or they are named <something>_engine.
148     def start_all
149       plugins = Dir[File.join(config(:root), "*")]
150       Engines.log.debug "considering plugins: #{plugins.inspect}"
151       plugins.each { |plugin|
152         engine_name = File.basename(plugin)
153         if File.exist?(File.join(plugin, "init_engine.rb")) || # if the directory contains init_engine.rb
154           (engine_name =~ /_engine$/) || # or it engines in '_engines'
155           (engine_name =~ /_bundle$/)    # or even ends in '_bundle'
156           
157           start(engine_name) # start the engine...
158         
159         end
160       }
161     end
162
163     # Initialize the routing controller paths. 
164     def initialize_routing
165       # See lib/engines/routing_extensions.rb for more information.
166       ActionController::Routing.controller_paths = Engines.rails_config.controller_paths
167     end
168
169     def start_engine(engine_name, options={})
170       
171       # Create a new Engine and put this engine at the front of the ActiveEngines list
172       current_engine = Engine.new(engine_name)
173       Engines.active.unshift current_engine
174       Engines.log.info "Starting engine '#{current_engine.name}' from '#{File.expand_path(current_engine.root)}'"
175
176       # add the code directories of this engine to the load path
177       add_engine_to_load_path(current_engine)
178
179       # add the controller & component path to the Dependency system
180       engine_controllers = File.join(current_engine.root, 'app', 'controllers')
181       engine_components = File.join(current_engine.root, 'components')
182
183
184       # This mechanism is no longer required in Rails trunk
185       if Rails::VERSION::STRING =~ /^1.0/ && !Engines.config(:edge)
186         Controllers.add_path(engine_controllers) if File.exist?(engine_controllers)
187         Controllers.add_path(engine_components) if File.exist?(engine_components)
188       else      
189         ActionController::Routing.controller_paths << engine_controllers
190         ActionController::Routing.controller_paths << engine_components
191       end
192         
193       # copy the files unless indicated otherwise
194       if options[:copy_files] != false
195         current_engine.mirror_engine_files
196       end
197
198       # load the engine's init.rb file
199       startup_file = File.join(current_engine.root, "init_engine.rb")
200       if File.exist?(startup_file)
201         eval(IO.read(startup_file), binding, startup_file)
202         # possibly use require_dependency? Hmm.
203       else
204         Engines.log.debug "No init_engines.rb file found for engine '#{current_engine.name}'..."
205       end
206     end
207
208     # Adds all directories in the /app and /lib directories within the engine
209     # to the load path
210     def add_engine_to_load_path(engine)
211       
212       # remove the lib directory added by load_plugin, and place it in the corrent
213       # location *after* the application/lib. This can be removed when 
214       # http://dev.rubyonrails.org/ticket/2910 is fixed.
215       app_lib_index = $LOAD_PATH.index(File.join(RAILS_ROOT, "lib"))
216       engine_lib = File.join(engine.root, "lib")
217       if app_lib_index
218         $LOAD_PATH.delete(engine_lib)
219         $LOAD_PATH.insert(app_lib_index+1, engine_lib)
220       end
221       
222       # Add ALL paths under the engine root to the load path
223       app_dirs = %w(controllers helpers models).collect { |d|
224         File.join(engine.root, 'app', d)
225       }
226       other_dirs = %w(components lib).collect { |d| 
227         File.join(engine.root, d)
228       }
229       load_paths  = (app_dirs + other_dirs).select { |d| File.directory?(d) }
230
231       # Remove other engines from the $LOAD_PATH by matching against the engine.root values
232       # in ActiveEngines. Store the removed engines in the order they came off.
233       
234       old_plugin_paths = []
235       # assumes that all engines are at the bottom of the $LOAD_PATH
236       while (File.expand_path($LOAD_PATH.last).index(File.expand_path(Engines.config(:root))) == 0) do
237         old_plugin_paths.unshift($LOAD_PATH.pop)
238       end
239
240
241       # add these LAST on the load path.
242       load_paths.reverse.each { |dir| 
243         if File.directory?(dir)
244           Engines.log.debug "adding #{File.expand_path(dir)} to the load path"
245           #$LOAD_PATH.push(File.expand_path(dir))
246           $LOAD_PATH.push dir
247         end
248       }
249       
250       # Add the other engines back onto the bottom of the $LOAD_PATH. Put them back on in
251       # the same order.
252       $LOAD_PATH.push(*old_plugin_paths)
253       $LOAD_PATH.uniq!
254     end
255
256     # Returns the directory in which all engine public assets are mirrored.
257     def public_engine_dir
258       File.expand_path(File.join(RAILS_ROOT, "public", Engines.config(:public_dir)))
259     end
260   
261     # create the /public/engine_files directory if it doesn't exist
262     def create_base_public_directory
263       if !File.exists?(public_engine_dir)
264         # create the public/engines directory, with a warning message in it.
265         Engines.log.debug "Creating public engine files directory '#{public_engine_dir}'"
266         FileUtils.mkdir(public_engine_dir)
267         File.open(File.join(public_engine_dir, "README"), "w") do |f|
268           f.puts <<EOS
269 Files in this directory are automatically generated from your Rails Engines.
270 They are copied from the 'public' directories of each engine into this directory
271 each time Rails starts (server, console... any time 'start_engine' is called).
272 Any edits you make will NOT persist across the next server restart; instead you
273 should edit the files within the <engine_name>/public/ directory itself.
274 EOS
275         end
276       end
277     end
278     
279     # Returns the Engine object for the specified engine, e.g.:
280     #    Engines.get(:login)  
281     def get(name)
282       active.find { |e| e.name == name.to_s || e.name == "#{name}_engine" }
283     end
284     alias_method :[], :get
285     
286     # Returns the Engine object for the current engine, i.e. the engine
287     # in which the currently executing code lies.
288     def current
289       current_file = caller[0]
290       active.find do |engine|
291         File.expand_path(current_file).index(File.expand_path(engine.root)) == 0
292       end
293     end
294     
295     # Returns an array of active engines
296     def active
297       ActiveEngines
298     end
299     
300     # Pass a block to perform an operation on each engine. You may pass an argument
301     # to determine the order:
302     # 
303     # * :load_order - in the order they were loaded (i.e. lower precidence engines first).
304     # * :precidence_order - highest precidence order (i.e. last loaded) first
305     def each(ordering=:precidence_order, &block)
306       engines = (ordering == :load_order) ? active.reverse : active
307       engines.each { |e| yield e }
308     end
309   end 
310 end
311
312 # A simple class for holding information about loaded engines
313 class Engine
314   
315   # Returns the base path of this engine
316   attr_accessor :root
317   
318   # Returns the name of this engine
319   attr_reader :name
320   
321   # An attribute for holding the current version of this engine. There are three
322   # ways of providing an engine version. The simplest is using a string:
323   #
324   #   Engines.current.version = "1.0.7"
325   #
326   # Alternatively you can set it to a module which contains Major, Minor and Release
327   # constants:
328   #
329   #   module LoginEngine::Version
330   #     Major = 1; Minor = 0; Release = 6;
331   #   end
332   #   Engines.current.version = LoginEngine::Version
333   #
334   # Finally, you can set it to your own Proc, if you need something really fancy:
335   #
336   #   Engines.current.version = Proc.new { File.open('VERSION', 'r').readlines[0] }
337   # 
338   attr_writer :version
339   
340   # Engine developers can store any information they like in here.
341   attr_writer :info
342   
343   # Creates a new object holding information about an Engine.
344   def initialize(name)
345
346     @root = ''
347     suffixes = ['', '_engine', '_bundle']
348     while !File.exist?(@root) && !suffixes.empty?
349       suffix = suffixes.shift
350       @root = File.join(Engines.config(:root), name.to_s + suffix)
351     end
352
353     if !File.exist?(@root)
354       raise "Cannot find the engine '#{name}' in either /vendor/plugins/#{name}, " +
355         "/vendor/plugins/#{name}_engine or /vendor/plugins/#{name}_bundle."
356     end      
357     
358     @name = File.basename(@root)
359   end
360     
361   # Returns the version string of this engine
362   def version
363     case @version
364     when Module
365       "#{@version::Major}.#{@version::Minor}.#{@version::Release}"
366     when Proc         # not sure about this
367       @version.call
368     when NilClass
369       'unknown'
370     else
371       @version
372     end
373   end
374   
375   # Returns a string describing this engine
376   def info
377     @info || '(none)'
378   end
379     
380   # Returns a string representation of this engine
381   def to_s
382     "Engine<'#{@name}' [#{version}]:#{root.gsub(RAILS_ROOT, '')}>"
383   end
384   
385   # return the path to this Engine's public files (with a leading '/' for use in URIs)
386   def public_dir
387     File.join("/", Engines.config(:public_dir), name)
388   end
389   
390   # Replicates the subdirectories under the engine's /public directory into
391   # the corresponding public directory.
392   def mirror_engine_files
393     
394     begin
395       Engines.create_base_public_directory
396   
397       source = File.join(root, "public")
398       Engines.log.debug "Attempting to copy public engine files from '#{source}'"
399   
400       # if there is no public directory, just return after this file
401       return if !File.exist?(source)
402
403       source_files = Dir[source + "/**/*"]
404       source_dirs = source_files.select { |d| File.directory?(d) }
405       source_files -= source_dirs  
406     
407       Engines.log.debug "source dirs: #{source_dirs.inspect}"
408
409       # Create the engine_files/<something>_engine dir if it doesn't exist
410       new_engine_dir = File.join(RAILS_ROOT, "public", public_dir)
411       if !File.exists?(new_engine_dir)
412         # Create <something>_engine dir with a message
413         Engines.log.debug "Creating #{public_dir} public dir"
414         FileUtils.mkdir_p(new_engine_dir)
415       end
416
417       # create all the directories, transforming the old path into the new path
418       source_dirs.uniq.each { |dir|
419         begin        
420           # strip out the base path and add the result to the public path, i.e. replace 
421           #   ../script/../vendor/plugins/engine_name/public/javascript
422           # with
423           #   engine_name/javascript
424           #
425           relative_dir = dir.gsub(File.join(root, "public"), name)
426           target_dir = File.join(Engines.public_engine_dir, relative_dir)
427           unless File.exist?(target_dir)
428             Engines.log.debug "creating directory '#{target_dir}'"
429             FileUtils.mkdir_p(target_dir)
430           end
431         rescue Exception => e
432           raise "Could not create directory #{target_dir}: \n" + e
433         end
434       }
435
436       # copy all the files, transforming the old path into the new path
437       source_files.uniq.each { |file|
438         begin
439           # change the path from the ENGINE ROOT to the public directory root for this engine
440           target = file.gsub(File.join(root, "public"), 
441                              File.join(Engines.public_engine_dir, name))
442           unless File.exist?(target) && FileUtils.identical?(file, target)
443             Engines.log.debug "copying file '#{file}' to '#{target}'"
444             FileUtils.cp(file, target)
445           end 
446         rescue Exception => e
447           raise "Could not copy #{file} to #{target}: \n" + e 
448         end
449       }
450     rescue Exception => e
451       Engines.log.warn "WARNING: Couldn't create the engine public file structure for engine '#{name}'; Error follows:"
452       Engines.log.warn e
453     end
454   end  
455 end
456
457
458 # These files must be required after the Engines module has been defined.
459 require 'engines/dependencies_extensions'
460 require 'engines/routing_extensions'
461 require 'engines/action_view_extensions'
462 require 'engines/action_mailer_extensions'
463 require 'engines/migration_extensions'
464 require 'engines/active_record_extensions'
465
466 # only load the testing extensions if we are in the test environment
467 require 'engines/testing_extensions' if %w(test).include?(RAILS_ENV)

Benjamin Mako Hill || Want to submit a patch?