# =========================================================================== # Project: Abbot - SproutCore Build Tools # Copyright: ©2009 Apple, Inc. # portions copyright @2006-2009 Sprout Systems, Inc. # and contributors # =========================================================================== require File.join(File.dirname(__FILE__), 'models', 'hash_struct') require File.join(File.dirname(__FILE__), 'buildfile', 'cloneable') require File.join(File.dirname(__FILE__), 'buildfile', 'task_manager') module SC # A Buildfile is a special type of file that contains the configurations and # build tasks used for a particular project or project target. Buildfiles # are based on Rake but largely use their own syntax and helper methods. # # Whenever you create a project, you will often also add a Buildfile, # sc-config, or sc-config.rb file. All of these files are laoded into the # build system using this class. The other model objects will then # reference their buildfile to extract configuration information and to find # key tasks required for the build process. # # == Loading a Buildfile # # To load a buildfile, just use the load() method: # # buildfile = Buildfile.load('/path/to/buildfile') # # You can also load multiple buildfiles by calling the load!() method on # an exising Buildfile object or by passing a directory with several # buildfiles in it: # # buildfile = Buildfile.new # buildfile.load!('buildfile1').load!('buildfile2') # # == Defining a Buildfile # # Finally, you can also define new settings on a buildfile directly. Simply # use the define() method: # # buildfile = Buildfile.define do # task :demo_task # end # # You can also define additional tasks on an existing buildfile object like # so: # # buildfile = Buildfile.new # buildfile.define! do # task :demo_task # end # # When you call define!() on a buildfile, the block is executed in the # context of the buildfile object, just like a Buildfile loaded from disk. # You will not usually use define!() on a buildfile in normal code, but it # is very useful for unit testing. # # == Executing Tasks # # Once a buildfile is loaded, you can execute tasks on the buildfile using # the invoke() method. You should pass the name of the task you want # to execute along with any constants you want set for the task to access: # # buildfile.invoke :demo_task, :context => my_context # # With the above example, the demo_task could access the "context" as a # global constant like do: # # task :demo_task do # CONTEXT.name = "demo!" # end # # == Accessing Configs # # Configs are stored in a "normalized" state in the "configs" property. You # can access configs directly this way, but the more useful way to access # configs is through the config_for() method. Pass the name of the target # that is the current "focus" of the config: # # config = buildfile.config_for('/sproutcore') # target always starts w / # # Configs can be specified in different "contexts" by the buildfile. When # you call this method, the configs will be merged together, layering any # configs that are targeted specifically at the /sproutcore target over the # top of globally defined configs. The current build mode is also reflected # in this call. # class Buildfile # Default buildfile names. Override with SC.env.buildfile_names BUILDFILE_NAMES = %w(Buildfile sc-config sc-config.rb) include Cloneable include TaskManager # The location of the buildfile represented by this object. attr_accessor :path ################################################ # CLASS METHODS # # Determines if this directory has a buildfile or not... def self.has_buildfile?(dir_path, buildfile_names=nil) buildfile_names ||= (SC.env.buildfile_names || BUILDFILE_NAMES) buildfile_names.each do |path| path = File.join(dir_path, path) return true if File.exist?(path) && !File.directory?(path) end return false end # Loads the buildfile at the specified path. This simply creates a new # instance and loads it. # # === Params # path:: the path to laod at # # === Returns # A new Buildfile instance # def self.load(path) self.new.load!(path) end # Creates a new buildfile and then gives you an opportunity to define # its contents by executing the passed block in the context of the # buildfile. # # === Returns # A new buildfile instance # def self.define(&block) self.new.define!(&block) end ################################################ # TASK METHODS # attr_reader :current_path # Extend the buildfile dynamically by executing the named task. This # will yield the block if given after making the buildfile the current # build file. # # === Params # string:: optional string to eval # &block:: optional block to execute # # === Returns # self # def define!(string=nil, &block) context = reset_define_context :current_mode => :all instance_eval(string) if string instance_eval(&block) if block_given? load_imports reset_define_context context return self end def task_defined?(task_name) !!lookup(task_name) end # Loads the contents of the passed file into the buildfile object. The # contents will be executed in the context of the buildfile object. If # the filename passed is nil or the file does not exist, this will simply # do nothing. # # === Params # filename:: the buildfile to load or a directory # buildfile_names:: optional array of names to search in directory # # === Returns # self def load!(filename=nil, buildfile_names=nil) # If a directory is passed, look for any buildfile and load them... if File.directory?(filename) # search directory for buildfiles and load them. buildfile_names ||= (SC.env.buildfile_names || BUILDFILE_NAMES) buildfile_names.each do |path| path = File.join(filename, path) next unless File.exist?(path) && !File.directory?(path) load!(path) end elsif File.exist?(filename) old_path = @current_path @current_path = filename loaded_paths << filename # save loaded paths SC.logger.debug "Loading buildfile at #{filename}" define!(File.read(filename)) if filename && File.exist?(filename) @current_path = old_path end return self end def loaded_paths; @loaded_paths ||= []; end # Executes the name task. Unlike invoke_task, this method will execute # the task even if it has already been executed before. You can also # pass a hash of additional constants that will be set on the global # namespace before the task is invoked. # # === Params # task_name:: the full name of the task, including namespaces # consts:: Optional hash of constant values to set on the env def invoke(task_name, consts = nil) consts = set_kernel_consts consts # save to restore self[task_name.to_s].invoke set_kernel_consts consts # clear constants end # Returns true if the buildfile has the named task defined # # === Params # task_name:: the full name of the task, including namespaces def has_task?(task_name) !self[task_name.to_s].nil? end ################################################ # RAKE SUPPORT # # Add a file to the list of files to be imported. def add_import(fn) @pending_imports << fn end # Load the pending list of imported files. def load_imports while fn = @pending_imports.shift next if @imported.member?(fn) if fn_task = lookup(fn) fn_task.invoke end load!(fn) @imported << fn end end # Support redefining a task... def intern(task_class, task_name) ret = super(task_class, task_name) ret.clear if @is_redefining return ret end # Application options from the command line attr_reader :options ################################################ # CONFIG METHODS # def current_mode @define_context.current_mode end def current_mode=(new_mode) @define_context.current_mode = new_mode end # Configures the buildfile for use with the specified target. Call this # BEFORE you load any actual file contents. # # === Returns # self # def for_target(target) @target_name = target.target_name.to_s return self end # The namespace for this buildfile. This should be name equal to the # namespace of the target that owns the buildfile, if there is one attr_reader :target_name # The hash of configs as loaded from the files. The configs are stored # by mode and then by config name. To get a merged config, use # config_for(). attr_reader :configs # The hash of proxy commands # Merge the passed hash of options into the config hash. This method # is usually used by the config global helper # # === Params # config_name:: the name of the config to set # config_mode:: the mode to store the config. If omitted use current # opts: the config options to merge in # # === Returns # receiver # def add_config(config_name, config_mode, opts=nil) # Normalize Params if opts.nil? opts = config_mode; config_mode = nil end config_mode = current_mode if config_mode.nil? # Normalize the config name -- :all or 'all' is OK, absolute OK. config_name = config_name.to_s if config_name != 'all' && (config_name[0..0] != '/') if target_name && (config_name == File.basename(target_name)) config_name = target_name else config_name = [target_name, config_name].join('/') end end # Perform Merge mode_configs = (self.configs[config_mode.to_sym] ||= HashStruct.new) config = (mode_configs[config_name.to_sym] ||= HashStruct.new) config.merge!(opts) end # Returns the merged config setting for the config name and mode. If # no mode is specified the :all mode is assumed. # # This will merge config hashes in the following order (mode/name): # # all:all -> mode:all -> all:config -> mode:config # # # === Params # config_name:: The config name # mode_name:: optional mode name # # === Returns # merged config -- a HashStruct def config_for(config_name, mode_name=nil) mode_name = :all if mode_name.nil? || mode_name.to_s.size == 0 config_name = :all if config_name.nil? || config_name.to_s.size == 0 # collect the hashes all_configs = configs[:all] cur_configs = configs[mode_name] ret = HashStruct.new # now merge em! -- note that this assumes the merge method will handle # self.merge(self) & self.merge(nil) gracefully ret.merge!(all_configs[:all]) if all_configs ret.merge!(cur_configs[:all]) if cur_configs ret.merge!(all_configs[config_name]) if all_configs ret.merge!(cur_configs[config_name]) if cur_configs # Done -- return result return ret end ################################################ # PROJECT & TARGET METHODS # # This is set if you use the project helper method in your buildfile. attr_accessor :project_name def project_type; @project_type || :default; end attr_writer :project_type # Returns YES if this buildfile appears to represent a project. If you # use the project() helper method, it will set this def project?; @is_project || false; end def project!; @is_project = true; end ################################################ # PROXY METHODS # # The hash of all proxies paths and their options attr_reader :proxies # Adds a proxy to the list of proxy paths. These are used only in server # mode to proxy certain URLs. If you call this method with the same # proxy path more than once, the options will be merged. # # === Params # :proxy_path the URL to proxy # :opts any proxy options # # === Returns # receiver # def add_proxy(proxy_path, opts={}) @proxies[proxy_path] = HashStruct.new(opts) return self end ################################################ # INTERNAL SUPPORT # def initialize super @configs = HashStruct.new @proxies = HashStruct.new @pending_imports = [] @imported = [] @options = HashStruct.new end # When dup'ing, rewrite the @tasks hash to use clones of the tasks # the point to the new application object. def dup ret = super # Make sure the tasks themselves are cloned tasks = ret.instance_variable_get('@tasks') tasks.each do | key, task | tasks[key] = task.dup(ret) end # Deep clone the config and proxy hashes as well... %w(@configs @proxies @options).each do |ivar| cloned_ivar = instance_variable_get(ivar).deep_clone ret.instance_variable_set(ivar, cloned_ivar) end ret.instance_variable_set('@is_project', false) # transient - do not dup return ret end protected # Save off the old define context and replace it with the passed context # This is used during a call to define() def reset_define_context(context=nil) ret = @define_context @define_context = HashStruct.new(context || {}) return ret end # For each key in the passed hash, this will register a global def set_kernel_consts(env = nil) return env if env.nil? # for each item in the passed environment, convert to uppercase constant # and set in global namespace. Save the old value so that it can be # restored later. ret = {} env.each do |key, value| const_key = key.to_s.upcase.to_sym # Save the old const value ret[key] = Kernel.const_get(const_key) rescue nil # Reset Kernel.const_reset(const_key, value) end # Also, save env. Used mostly for logging ret['TASK_ENV'] = Kernel.const_get('TASK_ENV') rescue nil Kernel.const_reset('TASK_ENV', env) return ret end end end # Add public method to kernel to remove defined constant using private # method. module Kernel def const_reset(key, value) remove_const(key) if const_defined?(key) const_set key, value end end SC.require_all_libs_relative_to(__FILE__)