require 'rant/plugin_methods' require 'yaml' # Configure plugin for Rant module Rant::Plugin # === Startup of configure plugin # ==== Config file exists # The configuration file will be read and the data hash # set up accordingly. # ==== Config file doesn't exist # The configuration process is run with +init_modes+ which # has to be one of CHECK_MODES. +init_modes+ defaults to # :default, which means if the configfile doesn't exist, # all values will be set to their defaults on startup. # === Access to configuration in Rantfile # You can access all configuration values through the [] # and []= operators of the configure plugin. # # Example of configure in Rantfile: # # conf = plugin :Configure do |conf| # conf.task # define a task named :configure # conf.check "profile" do |c| # c.default "full" # c.guess { ENV["PROFILE"] } # c.interact { # conf.prompt "What build-profile should be used?" # } # end # conf.check "optimize" do |c| # c.default true # c.guess { ENV["OPTIMIZE"] } # c.interact { # conf.ask_yes_no "Optimize build?" # } # end # end # # # Set default target depending on profile: # task :default => conf["profile"] class Configure include ::Rant::PluginMethods include ::Rant::Console class << self def rant_plugin_new(app, cinf, *args, &block) if args.size > 1 app.abort(app.pos_text(cinf[:file], cinf[:ln]), "Configure plugin takes only one argument.") end self.new(app, args.first, &block) end end CHECK_MODES = [ :default, :env, :guess, :interact, ] # Name for this plugin instance. Defaults to "configure". attr_reader :name # Name of configuration file. attr_accessor :file # This flag is used to determine if data has changed and # should be saved to file. attr_accessor :modified # An array with all checks to perform. attr_reader :checklist # Decide what the configure plugin does on startup if the # configuration file doesn't exist. Initialized to # [:guess]. # # If you want to control when the plugin should initialize the # configuration values, set this to +[:explicit]+ and call the # +init+ method with the init_modes you like as argument. attr_accessor :init_modes # Decide what the configure plugin does *after* reading the # configuration file (or directly after running +init_modes+ # if the configuration file doesn't exist). # Initialized to [:env], probably the only usefull # value. attr_accessor :override_modes # Don't write to file, config values will be lost when # rant exits! attr_accessor :no_write # Don't read or write to configuration file nor run +guess+ or # +interact+ blocks if *first* target given on commandline # is in this list. This is usefull for targets that remove # the configuration file. # Defaults are "distclean", "clobber" and "clean". attr_reader :no_action_list def initialize(app, name = nil) @name = name || rant_plugin_type @app = app or raise ArgumentError, "no application given" @file = "config" @checklist = [] @init_modes = [:guess] @override_modes = [:env] @no_write = false @modified = false @no_action_list = ["distclean", "clobber", "clean"] @no_action = false @configured = false yield self if block_given? run_checklist([:default]) # we don't need to save our defaults @modified = false end # Get the value for +key+ from +checklist+ or +nil+ if there # isn't a check with the given +key+. def [](key) c = checklist.find { |c| c.key == key } c ? c.value : nil end # Creates new check with default value if key doesn't exist. def []=(key, val) c = checklist.find { |c| c.key == key } if c if c.value != val c.value = val @modified = true end else self.check(key) { |c| c.default val } end end # Sets the specified check if a check with the given key # exists. # Returns the value if it was set, nil otherwise. def set_if_exists(key, value) c = checklist.find { |c| c.key == key } if c c.value = value @modified = true else nil end end # Builds a hash with all key-value pairs from checklist. def data hsh = {} @checklist.each { |c| hsh[c.key] = c.value } hsh end # This is true, if either a configure task was run, or the # configuration file was read. def configured? @configured end # Define a task with +name+ that will run the configuration # process in the given +check_modes+. If no task name is given # or it is +nil+, the plugin name will be used as task name. def task(name = nil, check_modes = [:guess, :interact]) name ||= @name cinf = ::Rant::Lib.parse_caller_elem(caller[0]) file = cinf[:file] ln = cinf[:ln] || 0 if !Array === check_modes || check_modes.empty? @app.abort(@app.pos_text(file, ln), "check_modes given to configure task has to be an array", "containing at least one CHECK_MODE symbol") end check_modes.each { |cm| unless CHECK_MODES.include? cm @app.abort(@app.pos_text(file,ln), "Unknown checkmode `#{cm.to_s}'.") end } nt = @app.task(name) { |t| run_checklist(check_modes) save @configured = true } nt end def check(key, val = nil, &block) checklist << ConfigureCheck.new(key, val, &block) end def init modes = @init_modes if modes == [:explicit] modes = [:guess] end @no_action = @no_action_list.include? @app.cmd_targets.first @no_action ||= @app[:tasks] unless @no_action init_config modes @configured = true end end # Run the configure process in the given modes. def run_checklist(modes = [:guess, :interact]) @checklist.each { |c| c.run_check(modes) } @modified = true end # Write configuration if modified. def save return if @no_write write_yaml if @modified true end # Immediately write configuration to +file+. def write write_yaml @modified = false end ###### overriden plugin methods ############################## def rant_plugin_type "configure" end def rant_plugin_name @name end def rant_plugin_init init unless @init_modes == [:explicit] end def rant_plugin_stop @no_action || save end ############################################################## private # Returns true on success, nil on failure. def init_config modes if File.exist? @file read_yaml @configured = true elsif modes != [:default] run_checklist modes end if @override_modes && !@override_modes.empty? run_checklist @override_modes end end def write_yaml @app.msg 1, "Writing config to `#{@file}'." File.open(@file, "w") { |f| f << data.to_yaml f << "\n" } true rescue @app.abort("When writing configuration: " + $!.message, "Ensure writing to file (doesn't need to exist) `#{@file}'", "is possible and try to reconfigure!") end def read_yaml File.open(@file) { |f| YAML.load_documents(f) { |doc| if doc.is_a? Hash doc.each_pair { |k, v| self[k] = v } else @app.abort("Invalid config file `#{@file}'.", "Please remove this file or reconfigure.") end } } rescue @app.abort("When attempting to read config: " + $!.message) end end # class Configure class ConfigureCheck include ::Rant::Console public :msg, :prompt, :ask_yes_no attr_reader :key attr_accessor :value attr_writer :default attr_accessor :guess_block attr_accessor :interact_block attr_accessor :react_block def initialize(key, val = nil) @key = key or raise ArgumentError, "no key given" @value = @default = val @guess_block = nil @interact_block = nil @react_block = nil yield self if block_given? end def default(val) @default = val @value = @default if @value.nil? end def guess(&block) @guess_block = block end def interact(&block) @interact_block = block end def react(&block) @react_block = block end # Run checks as specified by +modes+. +modes+ has to be a list # of symbols from the Configure::CHECK_MODES. def run_check(modes = [:guess], env = ENV) val = nil modes.each { |mode| case mode when :default val = @default when :env val = env[@key] when :interact val = @interact_block[self] if @interact_block when :guess val = @guess_block[self] if @guess_block else raise "unknown configure mode" end break unless val.nil? } val.nil? or @value = val @react_block && @react_block[@value] @value end end # class ConfigureCheck end # module Rant::Plugin