require_relative '../artefact.rb' require_relative '../artefact_engine.rb' module Comfpile class ParserArtefact < Comfpile::Artefact attr_reader :parsed_parameters attr_reader :parsed_dependencies def initialize(*args, search_regexes: nil, **opts) super(*args, **opts) @search_regexes = search_regexes @parsed_parameters = {} @parsed_dependencies = {} parent_artefact :sourcefile, @target require_artefact :config_file, File.dirname(@target) add_step do sourcefile_parse_step sourcefile_dependency_step end end # Attempts to find the set of values for a given # parameter key. # # @param [String, Array] keys Key, or array of keys, to look for # # @return [Array] Array of values found for this key # def find_parsed_parameters(keys) if keys.is_a? Array output = [] keys.each do |key| output += @parsed_parameters[key] || [] end output else @parsed_parameters[keys] || [] end end def find_parsed_parameter(key) find_parsed_parameters(key).last end # Try to find a relative or absolute source file # artefact. # Used to resolve the include, require and reference # statements and find relative and/or absolute source # files with matching name. Similar to how C++'s # include statement it will check relative, then # globally. # # @param [String, Array] name Path of the sourcefile to look for # @param [Boolean] optional If true, will also check for absolute # source file, and will discard the file if it can't be found. # Useful for e.g. C/C++ header files that may not be in the search # path of comfpile. # # @return [String] Resolved path of the sourcefile. Relative # if a relative file is found, else absolute. # def resolve_sourcefile(name, optional: false) if(name.is_a? Array) name.map do |n| resolve_sourcefile(n, optional: optional) end.compact else own_dir = File.dirname(@target) relative_file = File.join(own_dir, name).gsub(/^\.\//, '') if not craft_artefact(:sourcefile, relative_file).nil? relative_file elsif not craft_artefact(:sourcefile, name).nil? name elsif not optional relative_file else nil end end end def sourcefile_parse_step log "Parsing file #{@target}..." param_count = 0 File.readlines(@parent_artefact.file).each do |line| @search_regexes.each do |r| next if r[:regex].nil? match = r[:regex].match line next if match.nil? match = match.named_captures key = match['key'] || r[:key] value = match['value'] || r[:value] next if key.nil? log "Found k/v pair: '#{key}' '#{value}'", :debug param_count += 1 @parsed_parameters[key] ||= [] @parsed_parameters[key] << value end end log "Parsing completed, found #{param_count} parameters!" end def sourcefile_dependency_step ['include', 'require', 'reference'].map do |key| key_artefacts = resolve_sourcefile(find_parsed_parameters(key), optional: key == 'include') @parsed_dependencies[key] = key_artefacts.map { |key| craft_artefact(:parsed, key) } end log "Generated dependencies. #{@parsed_dependencies.map { |k, v| "#{k}: #{v.size}" }.join(', ')}." end end class DependencyAnalysisArtefact < Artefact attr_reader :dependencies attr_reader :mtime def initialize(*args, traverse: nil, **opts) super(*args, **opts) parent_artefact :parsed, @target @dependencies = [] @dependency_processed_artefacts = {} @dependency_waiting_on = [ @parent_artefact ] @traverse = traverse || ['include', 'require', 'reference'] add_step do iterative_dependency_wait_step end add_step do @mtime = @dependencies.map(&:mtime).max end end def iterative_dependency_wait_step dependency_unfurl = @dependency_waiting_on @dependency_waiting_on = [] loop do break if dependency_unfurl.empty? artefact = dependency_unfurl.pop next if @dependency_processed_artefacts[artefact.target] @dependencies << artefact @dependency_processed_artefacts[artefact.target] = true artefact.parsed_dependencies.each do |key, dep_list| next unless @traverse.nil? or @traverse.include? key dep_list.each do |dep_artefact| if wait_on dep_artefact @dependency_waiting_on << dep_artefact else dependency_unfurl << dep_artefact end end end end retry_step! unless @dependency_waiting_on.empty? end end class ParserEngine < ArtefactEngine # # Initialize a new ParserEngine # # This engine has the task of generating single-file parser artefacts, # which check for comfpile parameters that will be extracted from the sourcefile. # Each file will recursively craft other files it requires and add them, either as # fully included, required, or referenced file. # # @param [] core # @param [] **options # def initialize(core, **options) super(core, **options) @input_file_regex = options[:file_regex] @search_regexes = options[:search_regexes] raise ArgumentError, "Missing input file regex!" unless @input_file_regex.is_a? Regexp raise ArgumentError, "Missing regex parsing array!" unless @search_regexes.is_a? Array end def generate_parser_artefact(stage, target) match = @input_file_regex.match target return nil if match.nil? ParserArtefact.new(@core, self, stage, target, search_regexes: @search_regexes) end def generate_dependency_analysis_artefact(stage, target, **opts) match = @input_file_regex.match target return nil if match.nil? DependencyAnalysisArtefact.new(@core, self, stage, target, **opts) end def craft(stage, target) case stage when :parsed generate_parser_artefact(stage, target) when /^dependency_analysis(?:_(.+))?$/ traverse = $1&.split('_') generate_dependency_analysis_artefact(stage, target, traverse: traverse) else nil end end end end