require 'debug' module Comfpile class ArtefactExecSkipError < StandardError end class Artefact attr_reader :core, :engine attr_reader :exit_state attr_reader :stage, :target # @return [Array] List of all artefacts # included in this artefact. # Included artefacts are also required, but will additionally # update this Artefact's age. attr_reader :included_artefacts # @return [Array] List of all artefacts # required to build this artefact. # Required artefacts are needed to build this artefact, but # do not modify the artefact's age. attr_reader :required_artefacts # @return [Array] List of all artefacts # potentially referenced by this artefact. # Referenced artefacts are those that are potentially used # by this artefact through e.g. function calls, but are # not directly needed to build this artefact. Example being # how sourcecode of a library is eventually needed for e.g. # linking steps, but the objects can be built separately. attr_reader :referenced_artefacts # ARTEFACT STATES # # The following states are known to the system: # - blocked: The Artefact is blocked and waiting on other artefacts # - waiting: The Artefact is idle and ready to be queued # - running: The Artefact is currently being run # Note that this is not a state, but is determined by the # @running flag # - succeeded: it has finished its work without issue # - skipped: it didn't run/won't run because of failed dependencies # - failed: it has failed due to a requirement not being met # # Meta-States exist: # - in_progress/completed: Anything but/Only succeeded, skipped, failed def initialize(core, engine, stage, target) @core = core @engine = engine @stage = stage @target = target @age = Time.at(0) @parent_artefact = nil @required_artefacts = [] @included_artefacts = [] @referenced_artefacts = [] @log_current_level = :debug @log_current_line = "Not started..." @steps = [] @step_additions = nil @steps_done_ctr = 0 @waitlist = [] @parameters = {} @exit_state = nil @running = false end def [](key) v = @parameters[key] return v unless v.nil? if @parent_artefact return @parent_artefact[key] end end def []=(key, value) @parameters[key] = value end # # Log an event for this artefact. # Saves the given argument as log line, # optionally prints it to a logger. Use this to # save state progress messages as well as # error messages. # # @param [String] text Text message to log # @param [Symbol] state Level to log at. Known levels # are: # - :debug # - :info # - :warning # - :error # def log(text, level = :debug) @log_current_line = text @log_current_level = level puts "> #{@state} #{@target}: #{text}" nil end private def add_step_data(data) @step_additions ||= [] @step_additions << data end private def process_additional_step_data unless @step_additions.nil? @steps.insert(@steps_done_ctr, @step_additions) @steps.flatten! @step_additions = nil end end def add_step(&block) add_step_data({ type: :block, executed: false, block: block }) end def parent_artefact(stage, target) @parent_artefact = require_artefact(stage, target) end def include_artefact(stage, target, **opts) artefact = require_artefact(stage, target, **opts) @included_artefacts << artefact artefact end def require_artefact(stage, target, **opts) artefact = reference_artefact(stage, target, required: true, **opts) @required_artefacts << artefact artefact end def reference_artefact(stage, target, required: false) artefact = craft_artefact(stage, target) wait_on(artefact) if required @referenced_artefacts << artefact artefact end # # Wait on a specific artefact to complete # # @param [Comfpile::Artefact, nil] artefact The artefact to wait on # @param [Boolean] required Whether or not this artefact is required. # When set to true (default), a failed artefact will skip this artefact. # When set to false, this artefact will ignore the failure. # # @return [Boolean] true when we have to wait on this artefact or # it failed, false if we're all good def wait_on(artefact, required: true) if(artefact.nil?) fail! "Missing artefact dependency for #{stage} #{target}!" if required true elsif artefact.in_progress? @waitlist << { artefact: artefact, required: true } true elsif(required and not artefact.succeeded?) skip! "Failed artefact dependency: #{artefact}" true end false end # Find or create a new artefact # # @param [Symbol] stage The type of item to create. # can either be a stage for file processing (e.g. :parsed, # :sourcefile, :x86_debug_compiled), or an action (:clean) # @param [String] target Target file. Usually expressed # as path relative to Comfpile's resource locations. # # @return [nil, Artefact] Returns nil if no engine was # found that can craft this, else returns the # created or looked-up artefact. # def craft_artefact(stage, target) @core.craft_artefact(stage, target) end def waitlist_empty? return true if completed? loop do return true if @waitlist.empty? item = @waitlist[-1] return false if item[:artefact].in_progress? if not item[:required] @waitlist.pop elsif item[:artefact].succeeded? @waitlist.pop else skip! "Failed artefact dependency: #{item[:artefact]}" return true end end end def done_with_steps? @steps_done_ctr >= @steps.length end def state return :blocked unless waitlist_empty? return @exit_state unless @exit_state.nil? return :running if @running return :waiting end def completed? not @exit_state.nil? end def succeeded? @exit_state == :succeeded end def in_progress? not completed? end def waiting? self.state == :waiting end private def mark_state_change(state, reason, abort: false) return unless @exit_state.nil? puts "#{@stage} #{target}: Reached state #{state}: #{reason}" @exit_state = state @reason = reason abort_step! if abort end def skip!(reason, **opts) mark_state_change(:skipped, reason, **opts) end def fail!(reason, **opts) mark_state_change(:failed, reason, **opts) end def succeed!(reason, **opts) mark_state_change(:succeeded, reason, **opts) end def abort_step! raise ArtefactExecSkipError end def execute_step return unless waiting? @running = true process_additional_step_data next_step = @steps[@steps_done_ctr] succeed! "All done", abort: true if next_step.nil? case next_step[:type] when :block instance_exec &next_step[:block] else fail! "Unknown artefact step taken!", abort: true end @steps_done_ctr += 1 succeed! "All done", abort: true if waitlist_empty? and done_with_steps? ensure @running = false end def inspect "#" end def to_s inspect end end end