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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +26 -14
  4. data/data/verified_gems.json +85 -63
  5. data/exe/rubycc-ar +11 -3
  6. data/include/libc/sys/cdefs.h +12 -0
  7. data/lib/rubycc/backend/aarch64.rb +705 -117
  8. data/lib/rubycc/backend/slot_residency.rb +169 -0
  9. data/lib/rubycc/backend/x86_64.rb +924 -137
  10. data/lib/rubycc/command_line.rb +339 -0
  11. data/lib/rubycc/compile_error.rb +6 -3
  12. data/lib/rubycc/compiler.rb +17 -2
  13. data/lib/rubycc/diagnostics.rb +105 -0
  14. data/lib/rubycc/doctor/gemfile.rb +12 -3
  15. data/lib/rubycc/doctor/verified_gems.rb +5 -1
  16. data/lib/rubycc/driver.rb +66 -10
  17. data/lib/rubycc/front/ast.rb +18 -7
  18. data/lib/rubycc/front/constant_evaluator.rb +12 -0
  19. data/lib/rubycc/front/lexeme_reader.rb +3 -1
  20. data/lib/rubycc/front/parser.rb +51 -18
  21. data/lib/rubycc/ir/analysis.rb +82 -0
  22. data/lib/rubycc/ir/call_convention.rb +74 -7
  23. data/lib/rubycc/ir/generator.rb +319 -9
  24. data/lib/rubycc/ir/ir.rb +39 -1
  25. data/lib/rubycc/ir/promotion.rb +255 -0
  26. data/lib/rubycc/ir/simplify.rb +570 -0
  27. data/lib/rubycc/link/library_resolver.rb +17 -5
  28. data/lib/rubycc/link/partial_linker.rb +8 -1
  29. data/lib/rubycc/link/shared_linker.rb +2 -2
  30. data/lib/rubycc/mkmf_shim.rb +178 -12
  31. data/lib/rubycc/objfile/ar_archive.rb +13 -2
  32. data/lib/rubycc/objfile/elf_reader.rb +13 -2
  33. data/lib/rubycc/pkgconf/parser.rb +4 -0
  34. data/lib/rubycc/pkgconf/resolver.rb +3 -1
  35. data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
  36. data/lib/rubycc/preprocess/preprocessor.rb +161 -37
  37. data/lib/rubycc/preprocess/scanner.rb +69 -14
  38. data/lib/rubycc/preprocess/token_converter.rb +11 -1
  39. data/lib/rubycc/rmake/cli.rb +32 -4
  40. data/lib/rubycc/rmake/executor.rb +157 -226
  41. data/lib/rubycc/rmake/makefile.rb +35 -13
  42. data/lib/rubycc/rmake/parser.rb +8 -2
  43. data/lib/rubycc/rmake/rmake.rb +1 -0
  44. data/lib/rubycc/rmake/tool_command.rb +69 -0
  45. data/lib/rubycc/shell.rb +510 -0
  46. data/lib/rubycc/type.rb +23 -7
  47. data/lib/rubycc/version.rb +1 -1
  48. data/lib/rubycc.rb +12 -0
  49. data/lib/rubygems_plugin.rb +31 -3
  50. 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
- # a set of program names is passed as +tools+, a command whose argv[0] is one
27
- # of them (the first word of `$(CC)`/`$(LDSHARED)`) is not exec'd but run by
28
- # rubycc's own Driver inside a forked child, so a compiler crash cannot take
29
- # rmake down and the Driver's per-invocation state stays isolated. Second, a
30
- # `-j` scheduler that forks independent stale steps up to +jobs+ at a time,
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
- BUILTINS = %w[cd rm mkdir rmdir cp install echo touch true : exit].freeze
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, and the reason string of the most recent failure (used
42
- # to enrich CommandFailedError). Each recipe line starts from the Makefile's
43
- # base directory afresh, matching make running every line in its own shell.
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
- # One redirection parsed off a command: which stream (:stdout/:stderr),
54
- # whether it truncates or appends, and the target path (relative to the
55
- # command's cwd).
56
- Redirection = Struct.new(:stream, :mode, :path)
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
- @dir = File.expand_path(dir)
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
- @tools = Array(tools)
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 as an and-or list: simple commands joined by
115
- # `&&` (run next only after success), `||` (run next only after failure)
116
- # and `;` (always run next). A single left-to-right status carries the
117
- # result, exactly as an sh and-or list evaluates. Returns the final success.
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
- commands = parse_line(target, text)
120
- success = true
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
- [buf, j]
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
- # Turn the token stream into [[connector, SimpleCommand], ...]. A leading
266
- # run of `VAR=value` words become that command's environment; a redirect
267
- # marker consumes the following word as its target path.
268
- def parse_line(target, text)
269
- tokens = tokenize(target, text)
270
- commands = []
271
- connector = :first
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 tool?(argv[0])
323
- run_tool(argv, cmd, state)
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
- # Whether argv[0] names one of the substituted tool programs (matched by the
330
- # word as written and by its basename, so both `gcc` and `/usr/bin/gcc`
331
- # resolve). Empty when substitution is off, which keeps the default path
332
- # (exec every unknown command) untouched.
333
- def tool?(arg0)
334
- return false if @tools.empty?
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.include?(arg0) || @tools.include?(File.basename(arg0))
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 its program word. The compile line and the
341
- # `-shared` link line map through the same Driver entry point (it selects
342
- # its mode from the flags), so the two need no special-casing here. Sequential
343
- # runs fork for crash isolation; a parallel worker is already isolated and
344
- # runs the Driver in-process.
345
- def run_tool(argv, cmd, state)
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
- pid = Process.spawn(env_overrides(cmd.assignments), *argv, options)
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 ||= @default_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 whose argv[0] is this Makefile's compiler/linker program
72
- # (the first word of `$(CC)` / `$(LDSHARED)`, normally "gcc") are run by
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 ? tool_programs : [], jobs: jobs).execute(computed)
91
+ tools: tools ? tool_prefixes : [], jobs: jobs).execute(computed)
85
92
  computed
86
93
  end
87
94
 
88
- # The set of program names a `-`tools run substitutes for rubycc: the first
89
- # word of `$(CC)` and of `$(LDSHARED)` (the compile and shared-link drivers
90
- # mkmf emits). `$(LDSHARED)` is normally `$(CC) -shared`, so its flag words
91
- # already sit inside the expanded recipe and only the leading program name
92
- # is matched here; for the mkmf corpus both reduce to "gcc".
93
- def tool_programs
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| value.split(/\s+/).reject(&:empty?).first }
96
- .compact.uniq
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(@dir, name)
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)
@@ -47,8 +47,11 @@ module Rubycc
47
47
  @current_rule = nil
48
48
  @expander = Expander.new(@variables)
49
49
  @overrides = overrides || {}
50
- (defaults || {}).each { |name, value| @variables[name] = Variable.new(:simple, value.to_s) }
51
- @overrides.each { |name, value| @variables[name] = Variable.new(:simple, value.to_s) }
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)
@@ -10,6 +10,7 @@ require_relative "errors"
10
10
  require_relative "model"
11
11
  require_relative "expander"
12
12
  require_relative "parser"
13
+ require_relative "tool_command"
13
14
  require_relative "makefile"
14
15
  require_relative "executor"
15
16
  require_relative "cli"