lib/fig/environment.rb in fig-0.1.62 vs lib/fig/environment.rb in fig-0.1.64

- old
+ new

@@ -8,325 +8,429 @@ require 'fig/statement/include' require 'fig/statement/path' require 'fig/statement/set' require 'fig/userinputerror' -module Fig - # Manages the program's metadata, including packages and environment - # variables, and sets things up for running commands (from "command" - # statements in configuration files). - class Environment - DEFAULT_VERSION_NAME = 'current' +module Fig; end - def initialize(repository, variables_override, retriever) - @repository = repository - @variables = variables_override || OperatingSystem.get_environment_variables - @retrieve_vars = {} - @packages = {} - @retriever = retriever - end +# Manages the program's metadata, including packages and environment +# variables, and sets things up for running commands (from "command" +# statements in definition files or from the command-line). +class Fig::Environment + # Note: when reading this code, understand that the word "retrieve" is a + # noun and not a verb, e.g. "retrieve path" means the value of a retrieve + # statement and not the action of retrieving a path. - # Returns the value of an envirionment variable - def [](name) - return @variables[name] - end + def initialize(repository, variables_override, working_directory_maintainer) + @repository = repository + @variables = + variables_override || Fig::OperatingSystem.get_environment_variables() + @retrieves = {} + @packages = {} + @working_directory_maintainer = working_directory_maintainer + end - def variables - return @variables.clone + # Returns the value of an envirionment variable + def [](name) + return @variables[name] + end + + def variables + return @variables.clone + end + + # Indicates that the values from a particular environment variable path + # should be copied to a local directory. + def add_retrieve(retrieve_statement) + name = retrieve_statement.var + if @retrieves.has_key?(name) + Fig::Logging.warn \ + %q<About to overwrite "#{name}" retrieve path of "#{@retrieves[name].path}" with "#{retrieve_statement.path}".> end - # Indicates that the values from a particular envrionment variable path - def add_retrieve(name, path) - @retrieve_vars[name] = path + @retrieves[name] = retrieve_statement + retrieve_statement.added_to_environment(true) - return + return + end + + def register_package(package) + name = package.name + + if get_package(name) + raise_repository_error( + name.nil? \ + ? %Q<There is already a package with the name "#{name}".> \ + : %q<There is already an unnamed package.>, + nil, + package + ) end - def register_package(package) - name = package.name + @packages[name] = package - if get_package(name) - Logging.fatal %Q<There is already a package with the name "#{name}".> - raise RepositoryError.new - end + return + end - @packages[name] = package + def get_package(name) + return @packages[name] + end + def apply_config(package, config_name, backtrace) + if package.applied_config_names.member?(config_name) return end + new_backtrace = backtrace || + Fig::Backtrace.new( + nil, + Fig::PackageDescriptor.new(package.name, package.version, config_name) + ) - def get_package(name) - return @packages[name] + config = package[config_name] + config.statements.each do + |statement| + apply_config_statement(package, statement, new_backtrace) end + package.add_applied_config_name(config_name) - def packages - return @packages.values + return + end + + def execute_shell(command) + @variables.with_environment do + yield command.map{|arg| expand_command_line_argument(arg, nil, nil)} end - def apply_config(package, config_name, backtrace) - if package.applied_config_names.member?(config_name) - return - end - new_backtrace = backtrace + return + end - config = package[config_name] - config.statements.each { |stmt| apply_config_statement(package, stmt, new_backtrace) } - package.add_applied_config_name(config_name) + def execute_config(base_package, descriptor, args, &block) + config_name = + descriptor.config || find_config_name_in_package(descriptor.name) - return + name = descriptor.name || base_package.name + package = lookup_package( + name, + descriptor.version, + Fig::Backtrace.new( + nil, + Fig::PackageDescriptor.new(name, descriptor.version, config_name) + ) + ) + + command_statement = package[config_name].command_statement + if command_statement + execute_command(command_statement, args, package, nil, &block) + else + raise Fig::UserInputError.new( + %Q<The "#{package.to_s}" package with the "#{config_name}" configuration does not contain a command.> + ) end - def execute_shell(command) - @variables.with_environment do - yield command.map{|arg| expand_command_line_argument(arg)} - end + return + end - return + # In order for this to work correctly, any Overrides need to be processed + # before any other kind of Statement. The Configuration class guarantees + # that those come first in its set of Statements. + def apply_config_statement(base_package, statement, backtrace) + case statement + when Fig::Statement::Path + prepend_variable(base_package, statement.name, statement.value, backtrace) + when Fig::Statement::Set + set_variable(base_package, statement.name, statement.value, backtrace) + when Fig::Statement::Include + include_config(base_package, statement.descriptor, backtrace) + when Fig::Statement::Override + backtrace.add_override(statement) + when Fig::Statement::Command + # Skip - has no effect on environment. + else + raise "Unexpected statement in a config block: #{statement.unparse('')}" end - def execute_command(command, args, package) - @variables.with_environment do - argument = - expand_command_line_argument( - "#{command.command} #{args.join(' ')}" - ) + return + end - yield expand_path(argument, package).split(' ') - end + def include_config(base_package, descriptor, backtrace) + resolved_descriptor = nil - return + # Check to see if this include has been overridden. + if backtrace + override = backtrace.get_override( + descriptor.name || base_package.name + ) + if override + resolved_descriptor = + Fig::PackageDescriptor.new( + descriptor.name, override, descriptor.config + ) + end end + resolved_descriptor ||= descriptor - def find_config_name_in_package(name) - package = get_package(name) - if not package - return Package::DEFAULT_CONFIG - end + new_backtrace = Fig::Backtrace.new(backtrace, resolved_descriptor) + package = lookup_package( + resolved_descriptor.name || base_package.name, + resolved_descriptor.version, + new_backtrace + ) + apply_config( + package, + resolved_descriptor.config || Fig::Package::DEFAULT_CONFIG, + new_backtrace + ) - return package.primary_config_name || Package::DEFAULT_CONFIG + return + end + + def check_unused_retrieves() + @retrieves.keys().sort().each do + |name| + + statement = @retrieves[name] + if statement.loaded_but_not_referenced? + Fig::Logging.warn \ + %Q<The #{name} variable was never referenced or didn't need expansion, so "#{statement.unparse('')}"#{statement.position_string} was ignored.> + end end + end - def execute_config(base_package, descriptor, args, &block) - config_name = - descriptor.config || find_config_name_in_package(descriptor.name) + private - name = descriptor.name || base_package.name - package = lookup_package( - name, - descriptor.version, - Backtrace.new( - nil, - PackageDescriptor.new(name, descriptor.version, config_name) - ) + def set_variable(base_package, name, value, backtrace) + expanded_value = + expand_variable_as_path_and_process_retrieves( + name, value, base_package, backtrace ) + @variables[name] = expanded_value - command = package[config_name].command - if command - execute_command(command, args, package, &block) - else - raise UserInputError.new(%Q<The "#{package.to_s}" package with the "#{config_name}" configuration does not contain a command.>) - end + if Fig::Logging.debug? + expanded_message = + expanded_value == value ? '' \ + : %Q< (expanded from "#{value}")> - return + Fig::Logging.debug( + %Q<Set #{name} to "#{expanded_value}"#{expanded_message}.> + ) end - def apply_config_statement(base_package, statement, backtrace) - case statement - when Statement::Path - prepend_variable(base_package, statement.name, statement.value) - when Statement::Set - set_variable(base_package, statement.name, statement.value) - when Statement::Include - include_config( - base_package, statement.descriptor, statement.overrides, backtrace - ) - when Statement::Command - # ignore - else - fail "Unexpected statement: #{statement}" - end + return + end - return + def prepend_variable(base_package, name, value, backtrace) + expanded_value = + expand_variable_as_path_and_process_retrieves( + name, value, base_package, backtrace + ) + @variables.prepend_variable(name, expanded_value) + + if Fig::Logging.debug? + expanded_message = + expanded_value == value ? '' \ + : %Q< ("#{value}" expanded to "#{expanded_value}")> + + Fig::Logging.debug( + %Q<Prepending to #{name} resulted in "#{@variables[name]}"#{expanded_message}.> + ) end - def include_config(base_package, descriptor, overrides, backtrace) - resolved_descriptor = nil + return + end - # Check to see if this include has been overridden. - if backtrace - override = backtrace.get_override( - descriptor.name || base_package.name + def lookup_package(name, version, backtrace) + package = get_package(name) + if package.nil? + if not version + raise_repository_error( + "No version specified for #{name}.", backtrace, package ) - if override - resolved_descriptor = - PackageDescriptor.new( - descriptor.name, override, descriptor.config - ) - end end - resolved_descriptor ||= descriptor - new_backtrace = Backtrace.new(backtrace, resolved_descriptor) - overrides.each do |override| - new_backtrace.add_override(override.package_name, override.version) - end - package = lookup_package( - resolved_descriptor.name || base_package.name, - resolved_descriptor.version, - new_backtrace + package = @repository.get_package( + Fig::PackageDescriptor.new(name, version, nil) ) - apply_config( - package, - resolved_descriptor.config || Package::DEFAULT_CONFIG, - new_backtrace - ) + package.backtrace = backtrace + @packages[name] = package + elsif version && version != package.version + raise_repository_error("Version mismatch: #{name}", backtrace, package) + end - return + return package + end + + def find_config_name_in_package(name) + package = get_package(name) + if not package + return Fig::Package::DEFAULT_CONFIG end - private + return package.primary_config_name || Fig::Package::DEFAULT_CONFIG + end - def set_variable(base_package, name, value) - @variables[name] = expand_and_retrieve_variable_value(base_package, name, value) + def execute_command(command_statement, args, package, backtrace) + @variables.with_environment do + argument = + expand_command_line_argument( + "#{command_statement.command} #{args.join(' ')}", backtrace, package + ) - return + yield expand_at_signs_in_path(argument, package, backtrace).split(' ') end - def prepend_variable(base_package, name, value) - value = expand_and_retrieve_variable_value(base_package, name, value) - @variables.prepend_variable(name, value) + return + end - return - end + def expand_variable_as_path_and_process_retrieves( + variable_name, variable_value, base_package, backtrace + ) + return variable_value unless base_package && base_package.name - def lookup_package(name, version, backtrace) - package = get_package(name) - if package.nil? - if not version - Logging.fatal "No version specified for #{name}." - raise RepositoryError.new - end + variable_value = + expand_at_signs_in_path(variable_value, base_package, backtrace) - package = @repository.get_package( - PackageDescriptor.new(name, version, nil) - ) - package.backtrace = backtrace - @packages[name] = package - elsif version && version != package.version - string_handle = StringIO.new - backtrace.dump(string_handle) if backtrace - package.backtrace.dump(string_handle) if package.backtrace - stacktrace = string_handle.string - Logging.fatal \ - "Version mismatch: #{name}" \ - + ( stacktrace.empty? ? '' : "\n#{stacktrace}" ) - raise RepositoryError.new - end + return variable_value if not @retrieves.member?(variable_name) - return package - end + return retrieve_files( + variable_name, variable_value, base_package, backtrace + ) + end - # Replace @ symbol with the package's directory, "[package]" with the - # package name. - def expand_and_retrieve_variable_value(base_package, name, value) - return value unless base_package && base_package.name + def retrieve_files(variable_name, variable_value, base_package, backtrace) + check_source_existence( + variable_name, variable_value, base_package, backtrace + ) - file = expand_path(value, base_package) + destination_path = + derive_retrieve_destination(variable_name, variable_value, base_package) - if @retrieve_vars.member?(name) - # A '//' in the source file's path tells us to preserve path - # information after the '//' when doing a retrieve. - if file.split('//').size > 1 - preserved_path = file.split('//').last - target = File.join( - translate_retrieve_variables(base_package, name), - preserved_path - ) - else - target = File.join( - translate_retrieve_variables(base_package, name) - ) - if not File.directory?(file) - target = File.join(target, File.basename(file)) - end - end - @retriever.with_package_version( - base_package.name, base_package.version - ) do - @retriever.retrieve(file, target) - end - file = target - end + @working_directory_maintainer.switch_to_package_version( + base_package.name, base_package.version + ) + @working_directory_maintainer.retrieve(variable_value, destination_path) - return file - end + return destination_path + end - def expand_path(path, base_package) - expanded_path = expand_at_sign_package_references(path, base_package) - check_for_bad_escape(expanded_path, path) + def check_source_existence( + variable_name, variable_value, base_package, backtrace + ) + return if File.exists?(variable_value) || File.symlink?(variable_value) - return expanded_path.gsub(%r< \\ ([\\@]) >x, '\1') - end + raise_repository_error( + %Q<In #{base_package}, the #{variable_name} variable points to a path that does not exist ("#{variable_value}", after expansion).>, + backtrace, + base_package + ) + end - def expand_at_sign_package_references(arg, base_package) - return arg.gsub( - %r< - (?: ^ | \G) # Zero-width anchor. - ( [^\\@]* (?:\\{2})*) # An even number of leading backslashes - \@ # The package indicator - >x - ) do |match| - backslashes = $1 || '' - backslashes + base_package.directory - end + def derive_retrieve_destination(variable_name, variable_value, base_package) + retrieve_path = + get_retrieve_path_with_substitution(variable_name, base_package) - return + # A '//' in the variable value tells us to preserve path + # information after the '//' when doing a retrieve. + if variable_value.include? '//' + preserved_path = variable_value.split('//').last + + return File.join(retrieve_path, preserved_path) end - def expand_command_line_argument(arg) - package_substituted = expand_named_package_references(arg) - check_for_bad_escape(package_substituted, arg) - - return package_substituted.gsub(%r< \\ ([\\@]) >x, '\1') + if File.directory?(variable_value) + return retrieve_path end - def expand_named_package_references(arg) - return arg.gsub( - # TODO: Refactor package name regex into PackageDescriptor constant. - %r< - (?: ^ | \G) # Zero-width anchor. - ( [^\\@]* (?:\\{2})*) # An even number of leading backslashes - \@ # The package indicator - ( [a-zA-Z0-9_.-]+ ) # Package name - >x - ) do |match| - backslashes = $1 || '' - package = get_package($2) - if package.nil? - raise RepositoryError.new("Package not found: #{$1}") - end - backslashes + package.directory - end + return File.join(retrieve_path, File.basename(variable_value)) + end + + def expand_at_signs_in_path(path, base_package, backtrace) + expanded_path = + replace_at_signs_with_package_references(path, base_package) + check_for_bad_escape(expanded_path, path, base_package, backtrace) + + return collapse_backslashes_for_escaped_at_signs(expanded_path) + end + + def replace_at_signs_with_package_references(arg, base_package) + return arg.gsub( + %r< + (?: ^ | \G) # Zero-width anchor. + ( [^\\@]* (?:\\{2})*) # An even number of leading backslashes + \@ # The package indicator + >x + ) do |match| + backslashes = $1 || '' + backslashes + base_package.directory end + end - # The value is expected to have had any @ substitution already done, but - # collapsing of escapes not done yet. - def check_for_bad_escape(substituted, original) - if substituted =~ %r< - (?: ^ | [^\\]) # Start of line or non backslash - (?: \\{2})* # Even number of backslashes (including zero) - ( \\ [^\\@] ) # A bad escape + def expand_command_line_argument(arg, backtrace, package) + package_substituted = expand_named_package_references(arg, backtrace) + check_for_bad_escape(package_substituted, arg, package, backtrace) + + return collapse_backslashes_for_escaped_at_signs(package_substituted) + end + + def expand_named_package_references(arg, backtrace) + return arg.gsub( + # TODO: Refactor package name regex into PackageDescriptor constant. + %r< + (?: ^ | \G) # Zero-width anchor. + ( [^\\@]* (?:\\{2})*) # An even number of leading backslashes + \@ # The package indicator + ( [a-zA-Z0-9_.-]+ ) # Package name >x - raise RepositoryError.new( - %Q<Unknown escape "#{$1}" in "#{original}"> + ) do |match| + backslashes = $1 || '' + package_name = $2 + package = get_package(package_name) + if package.nil? + raise_repository_error( + %Q<Command-line referenced the "#{package_name}" package, which has not been referenced by any other package, so there's nothing to substitute with.>, + backtrace, + nil ) end - - return + backslashes + package.directory end + end - def translate_retrieve_variables(base_package, name) - return \ - @retrieve_vars[name].gsub(/ \[package\] /x, base_package.name) + # The value is expected to have had any @ substitution already done, but + # collapsing of escapes not done yet. + def check_for_bad_escape(substituted, original, package, backtrace) + if substituted =~ %r< + (?: ^ | [^\\]) # Start of line or non backslash + (?: \\{2})* # Even number of backslashes (including zero) + ( \\ [^\\@] ) # A bad escape + >x + raise_repository_error( + %Q<Unknown escape "#{$1}" in "#{original}">, backtrace, package + ) end + + return + end + + # After @ substitution, we need to get rid of the backslashes in front of + # any escaped @ signs. + def collapse_backslashes_for_escaped_at_signs(string) + return string.gsub(%r< \\ ([\\@]) >x, '\1') + end + + def get_retrieve_path_with_substitution(name, base_package) + retrieve_statement = @retrieves[name] + retrieve_statement.referenced(true) + + return retrieve_statement.path.gsub(/ \[package\] /x, base_package.name) + end + + def raise_repository_error(message, backtrace, package) + string_handle = StringIO.new + backtrace.dump(string_handle) if backtrace + package.backtrace.dump(string_handle) if package && package.backtrace + stacktrace = string_handle.string + + raise Fig::RepositoryError.new( + message + ( stacktrace.empty? ? '' : "\n#{stacktrace}" ) + ) end end