4.3.2.4 Custom Builder Operation

In order for a builder to perform a build operation, the builder class must implement a the Builder#run() method. Generally, the run() method will use the source file(s) to produce the target file. Here is an example of a trivial builder:

class Rscons::Builders::Custom < Rscons::Builder
  def run(options)
    File.open(@target, "w") do |fh|
      fh.write("Target file created.")
    end
    true
  end
end
4.3.2.4.1 Return Value

If the build operation has completed and failed, the run method should return false. In this case, generally the command executed or the builder itself would be expected to output something to $stderr indicating the reason for the build failure. If the build operation has completed successfully, the run method should return true. If the build operation is not yet complete and is waiting on other operations, the run method should return the return value from the Builder#wait_for method. See Custom Builder Parallelization.

4.3.2.4.2 Printing Build Status

A builder should print a status line when it produces a build target. The Builder#print_run_message method can be used to print the builder status line. This method supports a limited markup syntax to identify and color code the build target and/or source(s). Here is our Custom builder example extended to print its status:

class Rscons::Builders::Custom < Rscons::Builder
  def run(options)
    print_run_message("Creating <target>#{@target}<reset> from Custom builder")
    File.open(@target, "w") do |fh|
      fh.write("Target file created.")
    end
    true
  end
end
4.3.2.4.3 Custom Builder Cache Usage - Only Rebuild When Necessary

Whenever possible, a builder should keep track of information necessary to know whether the target file(s) need to be rebuilt. The Rscons::Cache object is the mechanism by which to keep track of this information. The Cache object provides two methods: #up_to_date? and #register_build which can be used to check if a built file is still up-to-date, and to register build information for a subsequent check. Here is a Custom builder which combines its source files similar to what the cat command would do:

class Rscons::Builders::Custom < Rscons::Builder
  def run(options)
    unless @cache.up_to_date?(@target, nil, @sources, @env)
      print_run_message("Combining <source>#{Util.short_format_paths(@sources)}<reset> => <target>#{@target}<reset>")
      File.open(@target, "wb") do |fh|
        @sources.each do |source|
          fh.write(File.read(source, mode: "rb"))
        end
      end
      @cache.register_build(@target, nil, @sources, @env)
    end
    true
  end
end

This builder would rebuild the target file and print its run message if the target file or any of the source file(s) were changed, but otherwise would be silent and not re-combine the source files.

Note that generally the same arguments should be passed to @cache.register_build and @cache.up_to_date?.

4.3.2.4.4 Custom Builder Parallelization

The Rscons scheduler can parallelize builders to take advantage of multiple processor cores. Taking advantage of this ability to parallelize requires the builder author to author the builder in a particular way. The #run() method of each builder is called from Rscons in the main program thread. However, the builder may execute a subcommand, spawn a thread, or register other builders to execute as a part of doing its job. In any of these cases, the builder's run method should make use of Builder#wait_for to "sleep" until one of the items being waited for has completed.

4.3.2.4.4.1 Using a Ruby Thread to Parallelize a Build Operation

Here is an example of using a Ruby thread to parallelize a build operation:

class MyBuilder < Rscons::Builder
  def run(options)
    if @thread
      true
    else
      print_run_message("#{name} #{target}", nil)
      @thread = Thread.new do
        sleep 2
        FileUtils.touch(@target)
      end
      wait_for(@thread)
    end
  end
end

build do
  Environment.new do |env|
    env.add_builder(MyBuilder)
    env.MyBuilder("foo")
  end
end

It is up to the author of the thread logic to only perform actions that are thread-safe. It is not safe to call other Rscons methods, for example, registering other builders or using the Cache, from a thread other than the one that calls the #run() method.

4.3.2.4.4.2 Executing a Subcommand from a Custom Builder

It is a very common case that a builder will execute a subcommand which produces the build target. This is how most of the built-in Rscons builders execute. A low-level way to handle this is for the builder to construct an instance of the Rscons::Command class and then wait_for the Command object. However, this is a common enough case that Rscons provides a few convenience methods to handle this:

The register_command helper method can be used to create a Command object and wait for it to complete. The standard_command helper does the same thing as register_command but additionally checks the @cache for the target being up to date. The finalize_command helper can be used in conjunction with either of the previous helper methods.

The built-in Rscons builders Command and Disassemble show examples of how to use the standard_command and finalize_command helper methods.

Example (built-in Command builder):

module Rscons
  module Builders
    # A builder to execute an arbitrary command that will produce the given
    # target based on the given sources.
    #
    # Example:
    #   env.Command("docs.html", "docs.md",
    #               CMD => %w[pandoc -fmarkdown -thtml -o${_TARGET} ${_SOURCES}])
    class Command < Builder

      # Run the builder to produce a build target.
      def run(options)
        if @command
          finalize_command
        else
          @vars["_TARGET"] = @target
          @vars["_SOURCES"] = @sources
          command = @env.build_command("${CMD}", @vars)
          cmd_desc = @vars["CMD_DESC"] || "Command"
          options = {}
          if @vars["CMD_STDOUT"]
            options[:stdout] = @env.expand_varref("${CMD_STDOUT}", @vars)
          end
          standard_command("#{cmd_desc} <target>#{@target}<reset>", command, options)
        end
      end

    end
  end
end

Example (built-in Disassemble builder):

module Rscons
  module Builders
    # The Disassemble builder produces a disassembly listing of a source file.
    class Disassemble < Builder

      # Run the builder to produce a build target.
      def run(options)
        if @command
          finalize_command
        else
          @vars["_SOURCES"] = @sources
          command = @env.build_command("${DISASM_CMD}", @vars)
          standard_command("Disassembling <source>#{Util.short_format_paths(@sources)}<reset> => <target>#{target}<reset>", command, stdout: @target)
        end
      end

    end
  end
end