rubycc 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +26 -14
- data/data/verified_gems.json +85 -63
- data/exe/rubycc-ar +11 -3
- data/include/libc/sys/cdefs.h +12 -0
- data/lib/rubycc/backend/aarch64.rb +705 -117
- data/lib/rubycc/backend/slot_residency.rb +169 -0
- data/lib/rubycc/backend/x86_64.rb +924 -137
- data/lib/rubycc/command_line.rb +339 -0
- data/lib/rubycc/compile_error.rb +6 -3
- data/lib/rubycc/compiler.rb +17 -2
- data/lib/rubycc/diagnostics.rb +105 -0
- data/lib/rubycc/doctor/gemfile.rb +12 -3
- data/lib/rubycc/doctor/verified_gems.rb +5 -1
- data/lib/rubycc/driver.rb +66 -10
- data/lib/rubycc/front/ast.rb +18 -7
- data/lib/rubycc/front/constant_evaluator.rb +12 -0
- data/lib/rubycc/front/lexeme_reader.rb +3 -1
- data/lib/rubycc/front/parser.rb +51 -18
- data/lib/rubycc/ir/analysis.rb +82 -0
- data/lib/rubycc/ir/call_convention.rb +74 -7
- data/lib/rubycc/ir/generator.rb +319 -9
- data/lib/rubycc/ir/ir.rb +39 -1
- data/lib/rubycc/ir/promotion.rb +255 -0
- data/lib/rubycc/ir/simplify.rb +570 -0
- data/lib/rubycc/link/library_resolver.rb +17 -5
- data/lib/rubycc/link/partial_linker.rb +8 -1
- data/lib/rubycc/link/shared_linker.rb +2 -2
- data/lib/rubycc/mkmf_shim.rb +178 -12
- data/lib/rubycc/objfile/ar_archive.rb +13 -2
- data/lib/rubycc/objfile/elf_reader.rb +13 -2
- data/lib/rubycc/pkgconf/parser.rb +4 -0
- data/lib/rubycc/pkgconf/resolver.rb +3 -1
- data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
- data/lib/rubycc/preprocess/preprocessor.rb +161 -37
- data/lib/rubycc/preprocess/scanner.rb +69 -14
- data/lib/rubycc/preprocess/token_converter.rb +11 -1
- data/lib/rubycc/rmake/cli.rb +32 -4
- data/lib/rubycc/rmake/executor.rb +157 -226
- data/lib/rubycc/rmake/makefile.rb +35 -13
- data/lib/rubycc/rmake/parser.rb +8 -2
- data/lib/rubycc/rmake/rmake.rb +1 -0
- data/lib/rubycc/rmake/tool_command.rb +69 -0
- data/lib/rubycc/shell.rb +510 -0
- data/lib/rubycc/type.rb +23 -7
- data/lib/rubycc/version.rb +1 -1
- data/lib/rubycc.rb +12 -0
- data/lib/rubygems_plugin.rb +31 -3
- metadata +14 -3
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "errors"
|
|
5
|
+
require_relative "tool_command"
|
|
6
|
+
require_relative "../command_line"
|
|
7
|
+
require_relative "../shell"
|
|
5
8
|
|
|
6
9
|
module Rubycc
|
|
7
10
|
module Rmake
|
|
@@ -23,50 +26,61 @@ module Rubycc
|
|
|
23
26
|
# directly with an argv array; the runner never builds a shell command string.
|
|
24
27
|
#
|
|
25
28
|
# B3 adds two things on top of that. First, in-process tool substitution: when
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
29
|
+
# +tools+ carries the argv prefixes of `$(CC)`/`$(LDSHARED)` (which may be two
|
|
30
|
+
# words or more — the mkmf shim writes `<ruby> <path>/exe/rubycc`), a command
|
|
31
|
+
# whose argv begins with one of them is not exec'd but run by rubycc's own
|
|
32
|
+
# Driver — inside a forked child, so a compiler crash cannot take rmake down
|
|
33
|
+
# and the Driver's per-invocation state stays isolated. Second, a `-j`
|
|
34
|
+
# scheduler that forks independent stale steps up to +jobs+ at a time,
|
|
31
35
|
# honouring the plan's dependency edges and buffering each worker's output to
|
|
32
36
|
# flush it whole when the step finishes (make -O's un-interleaved output).
|
|
33
37
|
class Executor
|
|
34
38
|
# The utilities reimplemented in-process, keyed by the command's basename so
|
|
35
39
|
# that `/usr/bin/mkdir` and `mkdir` resolve to the same builtin. `:` is
|
|
36
40
|
# make's $(NULLCMD); `exit` is how mkmf's `TOUCH = exit >` stamps a
|
|
37
|
-
# timestamp file (the `>` creates it, `exit` succeeds).
|
|
38
|
-
|
|
41
|
+
# timestamp file (the `>` creates it, `exit` succeeds). `test` and its
|
|
42
|
+
# other spelling `[` (escaped here only because %w[] counts brackets) are
|
|
43
|
+
# builtins for the same reason as the rest: coreutils is not in the
|
|
44
|
+
# minimal target environment, so falling back to /usr/bin/test would make
|
|
45
|
+
# a recipe's result depend on what happens to be installed.
|
|
46
|
+
BUILTINS = %w[cd rm mkdir rmdir cp install echo touch true : exit test \[].freeze
|
|
39
47
|
|
|
40
48
|
# Per-recipe-line mutable state: the working directory a `cd` in the same
|
|
41
|
-
# line has moved to,
|
|
42
|
-
#
|
|
43
|
-
#
|
|
49
|
+
# line has moved to, the shell variables assignments in the same line have
|
|
50
|
+
# set, and the reason string of the most recent failure (used to enrich
|
|
51
|
+
# CommandFailedError). Each recipe line starts from the Makefile's base
|
|
52
|
+
# directory with no variables, matching make running every line in its own
|
|
53
|
+
# shell — a `list=` in one line must not be visible in the next.
|
|
44
54
|
class LineState
|
|
45
55
|
attr_accessor :cwd, :failure_reason
|
|
56
|
+
attr_reader :variables
|
|
46
57
|
|
|
47
58
|
def initialize(cwd)
|
|
48
59
|
@cwd = cwd
|
|
60
|
+
@variables = {}
|
|
49
61
|
@failure_reason = nil
|
|
50
62
|
end
|
|
51
63
|
end
|
|
52
64
|
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
Redirection =
|
|
57
|
-
|
|
58
|
-
# One simple command: leading `VAR=value` assignments, the argv words and
|
|
59
|
-
# the redirections that apply to it.
|
|
60
|
-
SimpleCommand = Struct.new(:assignments, :argv, :redirections)
|
|
65
|
+
# The parsed shapes come from the shared splitter (Rubycc::CommandLine),
|
|
66
|
+
# which rmake and the mkmf shim both use; they are named here too so the
|
|
67
|
+
# runner reads as it did when it owned the parser.
|
|
68
|
+
Redirection = CommandLine::Redirection
|
|
69
|
+
SimpleCommand = CommandLine::SimpleCommand
|
|
61
70
|
|
|
62
71
|
def initialize(dir:, out: $stdout, err: $stderr, dry_run: false, env: ENV,
|
|
63
72
|
tools: [], jobs: 1)
|
|
64
|
-
|
|
73
|
+
# Kept as bytes: everything joined onto it later — a recipe's file names,
|
|
74
|
+
# a `cd` operand — comes from the Makefile, which Parser hands over as
|
|
75
|
+
# bytes.
|
|
76
|
+
@dir = File.expand_path(dir).b
|
|
65
77
|
@out = out
|
|
66
78
|
@err = err
|
|
67
79
|
@dry_run = dry_run
|
|
68
80
|
@env = env
|
|
69
|
-
|
|
81
|
+
# Each tool is an argv prefix (the words that name the program). A bare
|
|
82
|
+
# string is accepted as the one-word prefix it describes.
|
|
83
|
+
@tools = Array(tools).map { |tool| Array(tool) }
|
|
70
84
|
@jobs = [jobs.to_i, 1].max
|
|
71
85
|
# In sequential mode a substituted tool is fork-isolated for its own sake;
|
|
72
86
|
# a parallel worker is already a forked step child, so it runs the Driver
|
|
@@ -111,201 +125,32 @@ module Rubycc
|
|
|
111
125
|
@out.puts(command.text) if @dry_run || !command.silent?
|
|
112
126
|
end
|
|
113
127
|
|
|
114
|
-
# Interpret a recipe line
|
|
115
|
-
#
|
|
116
|
-
# and
|
|
117
|
-
#
|
|
128
|
+
# Interpret a recipe line with Rubycc::Shell — the connectors (`&&`, `||`,
|
|
129
|
+
# `;`), the compound commands (`for`, `if`, `{ }`) and the shell variables
|
|
130
|
+
# — and run each simple command it hands back here. The split is on
|
|
131
|
+
# purpose: Shell knows shell syntax and nothing about make, this runner
|
|
132
|
+
# knows the builtins, the substituted tools and the redirections and
|
|
133
|
+
# nothing about syntax. The variables live in +state+, so they last for
|
|
134
|
+
# this line only, and expansion falls back to the environment the spawned
|
|
135
|
+
# commands will themselves see, so `$PATH` means one thing per recipe.
|
|
118
136
|
def run_and_or_list(target, text, state)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
commands.each do |connector, cmd|
|
|
122
|
-
run = case connector
|
|
123
|
-
when :first, :semi then true
|
|
124
|
-
when :and then success
|
|
125
|
-
when :or then !success
|
|
126
|
-
end
|
|
127
|
-
success = run_simple(target, cmd, state, text) if run
|
|
128
|
-
end
|
|
129
|
-
success
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# --- lexing ----------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
# Characters after which a backslash inside double quotes keeps its
|
|
135
|
-
# special meaning (POSIX quote removal, XCU 2.2 / 2.2.3): only these five
|
|
136
|
-
# make the backslash consume — and remove itself along with — the next
|
|
137
|
-
# character; before a newline both vanish (line continuation). Before any
|
|
138
|
-
# other character the backslash inside double quotes is left in the word
|
|
139
|
-
# verbatim: `"a\b"` stays `a\b`, while `"a\"b"` becomes `a"b`. Verified
|
|
140
|
-
# against /bin/sh (dash).
|
|
141
|
-
DQUOTE_BACKSLASH_SPECIAL = ["$", "`", '"', "\\", "\n"].freeze
|
|
142
|
-
|
|
143
|
-
# Split a recipe line into tokens: :word (quote-stripped), the connectors
|
|
144
|
-
# :and/:or/:semi and :redirect markers. Single quotes protect everything
|
|
145
|
-
# verbatim — backslash has no special meaning inside them. Double quotes
|
|
146
|
-
# strip a backslash only before `$`/`` ` ``/`"`/`\`/newline
|
|
147
|
-
# (DQUOTE_BACKSLASH_SPECIAL); elsewhere the backslash stays in the word.
|
|
148
|
-
# Outside any quote, a backslash preserves the literal value of the
|
|
149
|
-
# following character and disappears itself, except before a newline
|
|
150
|
-
# where both vanish (line continuation) — this is POSIX quote removal,
|
|
151
|
-
# verified against /bin/sh. mkmf relies on the outside-quotes rule: it
|
|
152
|
-
# writes `-DSYSCONFDIR=\"...\"` expecting the shell to unescape the
|
|
153
|
-
# backslash-quotes into a literal `"` in the word. Genuinely unhandled
|
|
154
|
-
# shell syntax (pipe, background, substitution, subshell) stops the run.
|
|
155
|
-
def tokenize(target, text)
|
|
156
|
-
tokens = []
|
|
157
|
-
word = nil
|
|
158
|
-
i = 0
|
|
159
|
-
n = text.length
|
|
160
|
-
while i < n
|
|
161
|
-
c = text[i]
|
|
162
|
-
case c
|
|
163
|
-
when "'"
|
|
164
|
-
close = text.index(c, i + 1)
|
|
165
|
-
unsupported!("unterminated quote", target, text) if close.nil?
|
|
166
|
-
word = (word || +"") + text[(i + 1)...close]
|
|
167
|
-
i = close + 1
|
|
168
|
-
when '"'
|
|
169
|
-
segment, i = scan_double_quoted(target, text, i)
|
|
170
|
-
word = (word || +"") + segment
|
|
171
|
-
when "\\"
|
|
172
|
-
nxt = text[i + 1]
|
|
173
|
-
if nxt.nil?
|
|
174
|
-
# A lone trailing backslash with nothing to escape is kept
|
|
175
|
-
# literally (verified against /bin/sh).
|
|
176
|
-
word = (word || +"") + c
|
|
177
|
-
i += 1
|
|
178
|
-
elsif nxt == "\n"
|
|
179
|
-
i += 2 # line continuation: backslash and newline both vanish
|
|
180
|
-
else
|
|
181
|
-
word = (word || +"") + nxt
|
|
182
|
-
i += 2
|
|
183
|
-
end
|
|
184
|
-
when " ", "\t"
|
|
185
|
-
tokens << [:word, word] if word
|
|
186
|
-
word = nil
|
|
187
|
-
i += 1
|
|
188
|
-
when "&"
|
|
189
|
-
tokens << [:word, word] if word
|
|
190
|
-
word = nil
|
|
191
|
-
unsupported!("background '&'", target, text) unless text[i + 1] == "&"
|
|
192
|
-
tokens << [:and]
|
|
193
|
-
i += 2
|
|
194
|
-
when "|"
|
|
195
|
-
tokens << [:word, word] if word
|
|
196
|
-
word = nil
|
|
197
|
-
unsupported!("pipe '|'", target, text) unless text[i + 1] == "|"
|
|
198
|
-
tokens << [:or]
|
|
199
|
-
i += 2
|
|
200
|
-
when ";"
|
|
201
|
-
tokens << [:word, word] if word
|
|
202
|
-
word = nil
|
|
203
|
-
tokens << [:semi]
|
|
204
|
-
i += 1
|
|
205
|
-
when ">"
|
|
206
|
-
tokens << [:word, word] if word
|
|
207
|
-
word = nil
|
|
208
|
-
if text[i + 1] == ">"
|
|
209
|
-
tokens << [:redirect, :stdout, :append]
|
|
210
|
-
i += 2
|
|
211
|
-
else
|
|
212
|
-
tokens << [:redirect, :stdout, :truncate]
|
|
213
|
-
i += 1
|
|
214
|
-
end
|
|
215
|
-
when "<", "`", "(", ")"
|
|
216
|
-
unsupported!("shell metacharacter '#{c}'", target, text)
|
|
217
|
-
else
|
|
218
|
-
if word.nil? && (c == "1" || c == "2") && text[i + 1] == ">"
|
|
219
|
-
stream = c == "2" ? :stderr : :stdout
|
|
220
|
-
if text[i + 2] == ">"
|
|
221
|
-
tokens << [:redirect, stream, :append]
|
|
222
|
-
i += 3
|
|
223
|
-
else
|
|
224
|
-
tokens << [:redirect, stream, :truncate]
|
|
225
|
-
i += 2
|
|
226
|
-
end
|
|
227
|
-
else
|
|
228
|
-
word = (word || +"") + c
|
|
229
|
-
i += 1
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
|
-
tokens << [:word, word] if word
|
|
234
|
-
tokens
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Scan a double-quoted segment starting at +i+ (text[i] == '"'). Applies
|
|
238
|
-
# the backslash-removal rule that is special to double quotes (see
|
|
239
|
-
# DQUOTE_BACKSLASH_SPECIAL) and returns [content, index_after_closing_quote].
|
|
240
|
-
def scan_double_quoted(target, text, i)
|
|
241
|
-
n = text.length
|
|
242
|
-
j = i + 1
|
|
243
|
-
buf = +""
|
|
244
|
-
loop do
|
|
245
|
-
unsupported!("unterminated quote", target, text) if j >= n
|
|
246
|
-
|
|
247
|
-
c = text[j]
|
|
248
|
-
if c == '"'
|
|
249
|
-
j += 1
|
|
250
|
-
break
|
|
251
|
-
elsif c == "\\" && j + 1 < n && DQUOTE_BACKSLASH_SPECIAL.include?(text[j + 1])
|
|
252
|
-
nxt = text[j + 1]
|
|
253
|
-
buf << nxt unless nxt == "\n" # backslash-newline vanishes entirely
|
|
254
|
-
j += 2
|
|
255
|
-
else
|
|
256
|
-
buf << c
|
|
257
|
-
j += 1
|
|
258
|
-
end
|
|
137
|
+
shell = Shell.new(variables: state.variables, environment: @env) do |cmd|
|
|
138
|
+
run_simple(target, cmd, state, text)
|
|
259
139
|
end
|
|
260
|
-
|
|
140
|
+
shell.run(text)
|
|
141
|
+
rescue CommandLine::UnsupportedSyntaxError => e
|
|
142
|
+
unsupported!(e.construct, target, text)
|
|
261
143
|
end
|
|
262
144
|
|
|
263
145
|
# --- parsing ---------------------------------------------------------
|
|
264
146
|
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
268
|
-
def
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
assignments = []
|
|
273
|
-
argv = []
|
|
274
|
-
redirections = []
|
|
275
|
-
i = 0
|
|
276
|
-
|
|
277
|
-
flush = lambda do
|
|
278
|
-
unless assignments.empty? && argv.empty? && redirections.empty?
|
|
279
|
-
commands << [connector, SimpleCommand.new(assignments, argv, redirections)]
|
|
280
|
-
end
|
|
281
|
-
assignments = []
|
|
282
|
-
argv = []
|
|
283
|
-
redirections = []
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
while i < tokens.length
|
|
287
|
-
tok = tokens[i]
|
|
288
|
-
case tok[0]
|
|
289
|
-
when :word
|
|
290
|
-
w = tok[1]
|
|
291
|
-
if argv.empty? && w =~ /\A[A-Za-z_][A-Za-z0-9_]*=/
|
|
292
|
-
assignments << w
|
|
293
|
-
else
|
|
294
|
-
argv << w
|
|
295
|
-
end
|
|
296
|
-
when :and, :or, :semi
|
|
297
|
-
flush.call
|
|
298
|
-
connector = tok[0]
|
|
299
|
-
when :redirect
|
|
300
|
-
nxt = tokens[i + 1]
|
|
301
|
-
unsupported!("redirection without a target", target, text) if nxt.nil? || nxt[0] != :word
|
|
302
|
-
redirections << Redirection.new(tok[1], tok[2], nxt[1])
|
|
303
|
-
i += 1
|
|
304
|
-
end
|
|
305
|
-
i += 1
|
|
306
|
-
end
|
|
307
|
-
flush.call
|
|
308
|
-
commands
|
|
147
|
+
# Split a recipe line into words and connectors with the shared splitter.
|
|
148
|
+
# Kept as a method of the runner because a failure has to name the target
|
|
149
|
+
# whose recipe was at fault, which only the runner knows.
|
|
150
|
+
def tokenize(target, text)
|
|
151
|
+
CommandLine.tokenize(text)
|
|
152
|
+
rescue CommandLine::UnsupportedSyntaxError => e
|
|
153
|
+
unsupported!(e.construct, target, text)
|
|
309
154
|
end
|
|
310
155
|
|
|
311
156
|
# --- running a simple command ---------------------------------------
|
|
@@ -319,31 +164,36 @@ module Rubycc
|
|
|
319
164
|
name = File.basename(argv[0])
|
|
320
165
|
if BUILTINS.include?(name)
|
|
321
166
|
run_builtin(target, name, argv, cmd, state, text)
|
|
322
|
-
elsif
|
|
323
|
-
run_tool(
|
|
167
|
+
elsif (driver_argv = tool_arguments(argv))
|
|
168
|
+
run_tool(driver_argv, cmd, state)
|
|
324
169
|
else
|
|
325
170
|
run_external(target, argv, cmd, state)
|
|
326
171
|
end
|
|
327
172
|
end
|
|
328
173
|
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
|
|
334
|
-
|
|
174
|
+
# The arguments to hand rubycc's Driver when +argv+ runs one of the
|
|
175
|
+
# substituted tools, or nil when it does not (in which case the command is
|
|
176
|
+
# run as an ordinary external process). A tool is matched by its whole argv
|
|
177
|
+
# prefix — the words `$(CC)`/`$(LDSHARED)` expand to, which for the mkmf
|
|
178
|
+
# shim's `<ruby> <path>/exe/rubycc` is two of them — and exactly those words
|
|
179
|
+
# are dropped, so what reaches the Driver is what the recipe added. The
|
|
180
|
+
# longest matching prefix wins, since a longer one describes the command
|
|
181
|
+
# more completely. With substitution off (@tools empty) nothing matches,
|
|
182
|
+
# which keeps the default path (exec every unknown command) untouched.
|
|
183
|
+
def tool_arguments(argv)
|
|
184
|
+
return nil if @tools.empty?
|
|
335
185
|
|
|
336
|
-
@tools.
|
|
186
|
+
prefix = @tools.select { |candidate| ToolCommand.match?(argv, candidate) }.max_by(&:length)
|
|
187
|
+
prefix && argv.drop(prefix.length)
|
|
337
188
|
end
|
|
338
189
|
|
|
339
190
|
# Run a substituted compiler/linker command through rubycc's Driver, which
|
|
340
|
-
# takes the gcc-style argv minus
|
|
341
|
-
# `-shared` link line map through
|
|
342
|
-
# its mode from the flags), so the
|
|
343
|
-
# runs fork for crash isolation;
|
|
344
|
-
# runs the Driver in-process.
|
|
345
|
-
def run_tool(
|
|
346
|
-
driver_argv = argv.drop(1)
|
|
191
|
+
# takes the gcc-style argv minus the words that named the program (see
|
|
192
|
+
# #tool_arguments). The compile line and the `-shared` link line map through
|
|
193
|
+
# the same Driver entry point (it selects its mode from the flags), so the
|
|
194
|
+
# two need no special-casing here. Sequential runs fork for crash isolation;
|
|
195
|
+
# a parallel worker is already isolated and runs the Driver in-process.
|
|
196
|
+
def run_tool(driver_argv, cmd, state)
|
|
347
197
|
ok, reason = @isolate_tool ? fork_driver(driver_argv, state.cwd, cmd) \
|
|
348
198
|
: inline_driver(driver_argv, state.cwd, cmd)
|
|
349
199
|
state.failure_reason = reason unless ok
|
|
@@ -400,7 +250,20 @@ module Rubycc
|
|
|
400
250
|
options[:out] = out_io if out_io
|
|
401
251
|
options[:err] = err_io if err_io
|
|
402
252
|
begin
|
|
403
|
-
|
|
253
|
+
# `[argv[0], argv[0]]` (program plus argv0) rather than a bare
|
|
254
|
+
# `argv[0]` is load-bearing. Given a lone command string, Process
|
|
255
|
+
# .spawn decides for itself whether to involve /bin/sh, and it
|
|
256
|
+
# routes a shell reserved word there: measured on Ruby 3.4.5,
|
|
257
|
+
# "for", "do", "done", "if", "then", "elif", "else", "fi", "case",
|
|
258
|
+
# "esac", "while", "until", "in", "time", "!", "{", "}", "[[" and
|
|
259
|
+
# "]]" all reach the shell, while an ordinary name execs directly
|
|
260
|
+
# and raises ENOENT. A recipe fragment that happens to be one such
|
|
261
|
+
# word would then behave one way where a shell is installed and
|
|
262
|
+
# another where it is not -- the outcome DESIGN R5 and this module
|
|
263
|
+
# exist to rule out (see the file banner). The array form takes the
|
|
264
|
+
# decision away: it always execs. Commands of two words or more
|
|
265
|
+
# already did.
|
|
266
|
+
pid = Process.spawn(env_overrides(cmd.assignments), [argv[0], argv[0]], *argv[1..], options)
|
|
404
267
|
_, status = Process.waitpid2(pid)
|
|
405
268
|
state.failure_reason = "exited with status #{status.exitstatus}" unless status.success?
|
|
406
269
|
status.success?
|
|
@@ -609,6 +472,7 @@ module Rubycc
|
|
|
609
472
|
when "install" then builtin_install(target, argv, state, text)
|
|
610
473
|
when "echo" then builtin_echo(argv, out)
|
|
611
474
|
when "touch" then builtin_touch(argv, state)
|
|
475
|
+
when "test", "[" then builtin_test(target, argv, state, text)
|
|
612
476
|
when "true", ":", "exit" then true
|
|
613
477
|
else
|
|
614
478
|
# BUILTINS listed it but no branch handles it — a programming error.
|
|
@@ -785,6 +649,73 @@ module Rubycc
|
|
|
785
649
|
true
|
|
786
650
|
end
|
|
787
651
|
|
|
652
|
+
# `test EXPR` / `[ EXPR ]` — the conditional Automake's install rules ask
|
|
653
|
+
# every file about (`test -f $p`, `test -z "$list2"`). It is a builtin
|
|
654
|
+
# rather than /usr/bin/test for the reason the whole runner exists: the
|
|
655
|
+
# minimal target environment has no coreutils, and a recipe must not
|
|
656
|
+
# succeed only where they happen to be installed. Only the primaries those
|
|
657
|
+
# recipes use are implemented; anything else is refused rather than
|
|
658
|
+
# guessed, since a wrong answer here silently takes the wrong branch.
|
|
659
|
+
# A false condition is an ordinary status, not an error, so it records no
|
|
660
|
+
# failure reason.
|
|
661
|
+
def builtin_test(target, argv, state, text)
|
|
662
|
+
args = argv.drop(1)
|
|
663
|
+
if File.basename(argv[0]) == "["
|
|
664
|
+
unsupported!("`[` without a closing `]`", target, text) unless args.last == "]"
|
|
665
|
+
|
|
666
|
+
args = args[0...-1]
|
|
667
|
+
end
|
|
668
|
+
test_expression(target, args, state, text)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# Evaluate a test expression by its argument count, which is how POSIX
|
|
672
|
+
# defines it (XCU test): no arguments is false, one argument is true when
|
|
673
|
+
# the string is not empty, two are a unary primary, three a binary one,
|
|
674
|
+
# and a leading `!` negates whichever of those follows.
|
|
675
|
+
def test_expression(target, args, state, text)
|
|
676
|
+
return !test_expression(target, args.drop(1), state, text) if args.first == "!"
|
|
677
|
+
|
|
678
|
+
case args.length
|
|
679
|
+
when 0 then false
|
|
680
|
+
when 1 then !args[0].empty?
|
|
681
|
+
when 2 then test_unary(target, args[0], args[1], state, text)
|
|
682
|
+
when 3 then test_binary(target, args, text)
|
|
683
|
+
else unsupported!("test expression #{args.inspect}", target, text)
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# The file and string primaries. `-r` asks the kernel rather than reading
|
|
688
|
+
# the mode bits, so it answers for the user rmake is actually running as.
|
|
689
|
+
def test_unary(target, op, operand, state, text)
|
|
690
|
+
case op
|
|
691
|
+
when "-n" then !operand.empty?
|
|
692
|
+
when "-z" then operand.empty?
|
|
693
|
+
else
|
|
694
|
+
# The rest ask about a file, resolved against this line's cwd like
|
|
695
|
+
# every other operand a builtin takes.
|
|
696
|
+
path = absolute(operand, state.cwd)
|
|
697
|
+
case op
|
|
698
|
+
when "-f" then File.file?(path)
|
|
699
|
+
when "-d" then File.directory?(path)
|
|
700
|
+
when "-e" then File.exist?(path)
|
|
701
|
+
when "-r" then File.readable?(path)
|
|
702
|
+
when "-s" then File.exist?(path) && File.size(path).positive?
|
|
703
|
+
else unsupported!("test primary '#{op}'", target, text)
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# String comparison. The arithmetic and file-age primaries (`-eq`, `-nt`,
|
|
709
|
+
# ...) are refused: no recipe rmake has to run uses one.
|
|
710
|
+
def test_binary(target, args, text)
|
|
711
|
+
left, op, right = args
|
|
712
|
+
case op
|
|
713
|
+
when "=" then left == right
|
|
714
|
+
when "!=" then left != right
|
|
715
|
+
else unsupported!("test operator '#{op}'", target, text)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
788
719
|
# `touch FILE...` — create each file or update its timestamp.
|
|
789
720
|
def builtin_touch(argv, state)
|
|
790
721
|
argv.drop(1).reject { |a| a.start_with?("-") }.each do |f|
|
|
@@ -5,6 +5,7 @@ require_relative "model"
|
|
|
5
5
|
require_relative "expander"
|
|
6
6
|
require_relative "parser"
|
|
7
7
|
require_relative "executor"
|
|
8
|
+
require_relative "tool_command"
|
|
8
9
|
|
|
9
10
|
module Rubycc
|
|
10
11
|
module Rmake
|
|
@@ -51,9 +52,15 @@ module Rubycc
|
|
|
51
52
|
# judging staleness against the filesystem as of +now+ is implicit in the
|
|
52
53
|
# file mtimes read from #dir. Returns a Plan of steps in the order make
|
|
53
54
|
# would run them.
|
|
55
|
+
#
|
|
56
|
+
# +goal+ comes from the process (a command-line target), while the names it
|
|
57
|
+
# has to match — the rule targets, .PHONY, the default goal — are words of
|
|
58
|
+
# the Makefile, which Parser hands over as bytes. A goal spelled in another
|
|
59
|
+
# encoding matches no explicit rule at all once it holds a non-ASCII byte,
|
|
60
|
+
# so it is re-tagged here rather than silently planning nothing.
|
|
54
61
|
def plan(goal = nil, now: Time.now)
|
|
55
62
|
@now = now
|
|
56
|
-
goal
|
|
63
|
+
goal = goal ? goal.b : @default_goal
|
|
57
64
|
raise RmakeError, "no target specified and the Makefile has no default goal" if goal.nil?
|
|
58
65
|
|
|
59
66
|
@steps = []
|
|
@@ -68,8 +75,8 @@ module Rubycc
|
|
|
68
75
|
# (make -n) and matches #plan's #command_lines.
|
|
69
76
|
#
|
|
70
77
|
# +tools+ turns on in-process tool substitution (B3): when truthy the
|
|
71
|
-
# recipe commands
|
|
72
|
-
#
|
|
78
|
+
# recipe commands that run this Makefile's compiler/linker — the ones whose
|
|
79
|
+
# argv begins with the words of `$(CC)` / `$(LDSHARED)` — are run by
|
|
73
80
|
# rubycc's own Driver in a forked child instead of being exec'd, so no
|
|
74
81
|
# external compiler is needed. It defaults off, leaving the B2 behaviour
|
|
75
82
|
# (every command exec'd) exactly as it was. +jobs+ is the maximum number of
|
|
@@ -81,19 +88,28 @@ module Rubycc
|
|
|
81
88
|
tools: nil, jobs: 1)
|
|
82
89
|
computed = plan(goal, now: now)
|
|
83
90
|
Executor.new(dir: @dir, out: out, err: err, dry_run: dry_run, env: env,
|
|
84
|
-
tools: tools ?
|
|
91
|
+
tools: tools ? tool_prefixes : [], jobs: jobs).execute(computed)
|
|
85
92
|
computed
|
|
86
93
|
end
|
|
87
94
|
|
|
88
|
-
# The
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
95
|
+
# The commands a `-`tools run substitutes for rubycc, each as the argv
|
|
96
|
+
# prefix that names the program: the words of `$(CC)` and of `$(LDSHARED)`
|
|
97
|
+
# (the compile and shared-link drivers mkmf emits), with trailing option
|
|
98
|
+
# words trimmed off. `LDSHARED` is normally `$(CC) -shared`, so both
|
|
99
|
+
# collapse to the same prefix and the `-shared` stays in the recipe where
|
|
100
|
+
# the Driver reads it; a plain gcc Makefile gives ["gcc"].
|
|
101
|
+
#
|
|
102
|
+
# A prefix rather than a program name because the words that name the
|
|
103
|
+
# program are not always one: the mkmf shim writes `CC = <ruby>
|
|
104
|
+
# <path>/exe/rubycc`, launching the executable through the running
|
|
105
|
+
# interpreter rather than through its `#!/usr/bin/env ruby` line. Matching
|
|
106
|
+
# the whole prefix is what keeps `ruby -Ilib <script>` or `jruby <script>`
|
|
107
|
+
# from handing the script path to the Driver as a compiler argument
|
|
108
|
+
# (ToolCommand).
|
|
109
|
+
def tool_prefixes
|
|
94
110
|
[variable_value("CC"), variable_value("LDSHARED")]
|
|
95
|
-
.map { |value|
|
|
96
|
-
.
|
|
111
|
+
.map { |value| ToolCommand.prefix(value) }
|
|
112
|
+
.reject(&:empty?).uniq
|
|
97
113
|
end
|
|
98
114
|
|
|
99
115
|
# The fully-expanded value of a variable (empty string when undefined).
|
|
@@ -327,8 +343,14 @@ module Rubycc
|
|
|
327
343
|
@vpath_dirs ||= variable_value("VPATH").split(/[:\s]+/).reject(&:empty?)
|
|
328
344
|
end
|
|
329
345
|
|
|
346
|
+
# +name+ is a word of the Makefile (bytes) while #dir came from the process
|
|
347
|
+
# (ARGV or Dir.pwd), so the join is done in bytes.
|
|
330
348
|
def full_path(name)
|
|
331
|
-
absolute?(name) ? name : File.join(
|
|
349
|
+
absolute?(name) ? name : File.join(dir_bytes, name)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def dir_bytes
|
|
353
|
+
@dir_bytes ||= @dir.b
|
|
332
354
|
end
|
|
333
355
|
|
|
334
356
|
def absolute?(name)
|
data/lib/rubycc/rmake/parser.rb
CHANGED
|
@@ -47,8 +47,11 @@ module Rubycc
|
|
|
47
47
|
@current_rule = nil
|
|
48
48
|
@expander = Expander.new(@variables)
|
|
49
49
|
@overrides = overrides || {}
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
# Seeded values arrive from the process (a `VAR=value` operand, the
|
|
51
|
+
# built-in MAKE) and are expanded into the Makefile's own byte text, so
|
|
52
|
+
# they are taken as bytes here.
|
|
53
|
+
(defaults || {}).each { |name, value| @variables[name] = Variable.new(:simple, value.to_s.b) }
|
|
54
|
+
@overrides.each { |name, value| @variables[name] = Variable.new(:simple, value.to_s.b) }
|
|
52
55
|
end
|
|
53
56
|
|
|
54
57
|
attr_reader :variables, :rules
|
|
@@ -57,7 +60,10 @@ module Rubycc
|
|
|
57
60
|
new(overrides: overrides, defaults: defaults).tap { |p| p.run(text) }
|
|
58
61
|
end
|
|
59
62
|
|
|
63
|
+
# Bytes in (lib/rubycc.rb): the Makefile grammar is spelled in ASCII, and
|
|
64
|
+
# comments and recipe text are carried through.
|
|
60
65
|
def run(text)
|
|
66
|
+
text = text.b unless text.encoding == Encoding::BINARY
|
|
61
67
|
logical_lines(text).each do |content, kind, line_no|
|
|
62
68
|
if kind == :recipe
|
|
63
69
|
handle_recipe(content, line_no)
|
data/lib/rubycc/rmake/rmake.rb
CHANGED