Class: Rubycc::Rmake::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycc/rmake/executor.rb

Overview

Runs an execution Plan (from Makefile#plan) without a shell. The minimal target environment has no /bin/sh (DESIGN R5), so each recipe line is interpreted here directly: split into words (honouring quotes), joined by the connectors make's recipes use (&&, ||, ;), with >/>>/2> redirections and VAR=value / cd prefixes applied to the command they front. The vocabulary is fixed by the mkmf corpus (test/fixtures/mkmf): only the constructs those Makefiles actually emit are accepted, and anything else raises UnsupportedRecipeError instead of being run through a shell that does not exist.

The utilities the recipes invoke (rm, mkdir, install, echo, ...) are reimplemented on top of FileUtils — the "no external tool" implementation is authoritative, so /usr/bin/install and a bare install behave identically and no process is spawned for them. Everything the runner does not recognise as a builtin (the compiler and linker, $(CC)/$(LDSHARED)) is exec'd directly with an argv array; the runner never builds a shell command string.

B3 adds two things on top of that. First, in-process tool substitution: when a set of program names is passed as tools, a command whose argv is one of them (the first word of $(CC)/$(LDSHARED)) is not exec'd but run by rubycc's own Driver — inside a forked child, so a compiler crash cannot take rmake down and the Driver's per-invocation state stays isolated. Second, a -j scheduler that forks independent stale steps up to jobs at a time, honouring the plan's dependency edges and buffering each worker's output to flush it whole when the step finishes (make -O's un-interleaved output).

Defined Under Namespace

Classes: LineState, Redirection, SimpleCommand

Constant Summary collapse

BUILTINS =

The utilities reimplemented in-process, keyed by the command's basename so that /usr/bin/mkdir and mkdir resolve to the same builtin. : is make's $(NULLCMD); exit is how mkmf's TOUCH = exit > stamps a timestamp file (the > creates it, exit succeeds).

%w[cd rm mkdir rmdir cp install echo touch true : exit].freeze

Instance Method Summary collapse

Constructor Details

#initialize(dir:, out: $stdout, err: $stderr, dry_run: false, env: ENV, tools: [], jobs: 1) ⇒ Executor

Returns a new instance of Executor.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rubycc/rmake/executor.rb', line 62

def initialize(dir:, out: $stdout, err: $stderr, dry_run: false, env: ENV,
               tools: [], jobs: 1)
  @dir = File.expand_path(dir)
  @out = out
  @err = err
  @dry_run = dry_run
  @env = env
  @tools = Array(tools)
  @jobs = [jobs.to_i, 1].max
  # In sequential mode a substituted tool is fork-isolated for its own sake;
  # a parallel worker is already a forked step child, so it runs the Driver
  # in-process rather than forking a second time.
  @isolate_tool = true
end

Instance Method Details

#execute(plan) ⇒ Object

Run every step of plan. With jobs == 1 (or under -n) this is a straight sequential walk: prerequisites already precede their dependents in the plan, so the order is a valid build order. With jobs > 1 the steps are dispatched by the parallel scheduler instead. Returns the plan; raises CommandFailedError / UnsupportedRecipeError at the first command that fails (and is not --prefixed) or cannot be interpreted.



83
84
85
86
87
88
89
90
91
92
# File 'lib/rubycc/rmake/executor.rb', line 83

def execute(plan)
  if @jobs > 1 && !@dry_run
    execute_parallel(plan)
  else
    plan.steps.each do |step|
      step.commands.each { |command| run_line(step.target, command) }
    end
  end
  plan
end