rubycc 1.0.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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +52 -0
  5. data/README.md +208 -0
  6. data/data/README.md +117 -0
  7. data/data/r10_corpus_scan.json +4082 -0
  8. data/data/r10_manual_classification.json +5342 -0
  9. data/data/r10_verification_m4a.json +1531 -0
  10. data/data/r10_verification_m4b.json +1818 -0
  11. data/data/r10_verification_m4c.json +1489 -0
  12. data/data/r10_verification_m4d.json +318 -0
  13. data/data/r10_verification_rbs.json +192 -0
  14. data/data/verified_gems.json +397 -0
  15. data/exe/rmake +16 -0
  16. data/exe/rubycc +9 -0
  17. data/exe/rubycc-ar +114 -0
  18. data/exe/rubycc-doctor +14 -0
  19. data/exe/rubycc-pkgconf +8 -0
  20. data/include/float.h +87 -0
  21. data/include/iso646.h +18 -0
  22. data/include/libc/alloca.h +18 -0
  23. data/include/libc/arpa/inet.h +61 -0
  24. data/include/libc/assert.h +43 -0
  25. data/include/libc/dirent.h +72 -0
  26. data/include/libc/dlfcn.h +58 -0
  27. data/include/libc/features.h +205 -0
  28. data/include/libc/glibc/aarch64/ctype.h +108 -0
  29. data/include/libc/glibc/aarch64/endian.h +54 -0
  30. data/include/libc/glibc/aarch64/errno.h +154 -0
  31. data/include/libc/glibc/aarch64/fcntl.h +144 -0
  32. data/include/libc/glibc/aarch64/inttypes.h +182 -0
  33. data/include/libc/glibc/aarch64/limits.h +63 -0
  34. data/include/libc/glibc/aarch64/pthread.h +131 -0
  35. data/include/libc/glibc/aarch64/setjmp.h +84 -0
  36. data/include/libc/glibc/aarch64/stdint.h +174 -0
  37. data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
  38. data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
  39. data/include/libc/glibc/aarch64/sys/select.h +75 -0
  40. data/include/libc/glibc/aarch64/sys/stat.h +136 -0
  41. data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
  42. data/include/libc/glibc/aarch64/sys/time.h +74 -0
  43. data/include/libc/glibc/aarch64/sys/types.h +127 -0
  44. data/include/libc/glibc/aarch64/time.h +113 -0
  45. data/include/libc/glibc/x86_64/ctype.h +108 -0
  46. data/include/libc/glibc/x86_64/endian.h +54 -0
  47. data/include/libc/glibc/x86_64/errno.h +154 -0
  48. data/include/libc/glibc/x86_64/fcntl.h +134 -0
  49. data/include/libc/glibc/x86_64/inttypes.h +182 -0
  50. data/include/libc/glibc/x86_64/limits.h +61 -0
  51. data/include/libc/glibc/x86_64/pthread.h +111 -0
  52. data/include/libc/glibc/x86_64/setjmp.h +82 -0
  53. data/include/libc/glibc/x86_64/stdint.h +163 -0
  54. data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
  55. data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
  56. data/include/libc/glibc/x86_64/sys/select.h +75 -0
  57. data/include/libc/glibc/x86_64/sys/stat.h +130 -0
  58. data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
  59. data/include/libc/glibc/x86_64/sys/time.h +74 -0
  60. data/include/libc/glibc/x86_64/sys/types.h +123 -0
  61. data/include/libc/glibc/x86_64/time.h +113 -0
  62. data/include/libc/grp.h +50 -0
  63. data/include/libc/langinfo.h +126 -0
  64. data/include/libc/link.h +18 -0
  65. data/include/libc/locale.h +83 -0
  66. data/include/libc/math.h +191 -0
  67. data/include/libc/netinet/in.h +126 -0
  68. data/include/libc/netinet/tcp.h +44 -0
  69. data/include/libc/poll.h +46 -0
  70. data/include/libc/pwd.h +58 -0
  71. data/include/libc/regex.h +53 -0
  72. data/include/libc/sched.h +35 -0
  73. data/include/libc/signal.h +204 -0
  74. data/include/libc/stdio.h +157 -0
  75. data/include/libc/stdlib.h +92 -0
  76. data/include/libc/string.h +74 -0
  77. data/include/libc/strings.h +25 -0
  78. data/include/libc/sys/cdefs.h +126 -0
  79. data/include/libc/sys/inotify.h +111 -0
  80. data/include/libc/sys/ioctl.h +36 -0
  81. data/include/libc/sys/mman.h +65 -0
  82. data/include/libc/sys/param.h +41 -0
  83. data/include/libc/sys/resource.h +109 -0
  84. data/include/libc/sys/socket.h +167 -0
  85. data/include/libc/sys/statfs.h +82 -0
  86. data/include/libc/sys/timerfd.h +55 -0
  87. data/include/libc/sys/uio.h +40 -0
  88. data/include/libc/sys/un.h +25 -0
  89. data/include/libc/sys/utsname.h +35 -0
  90. data/include/libc/sys/wait.h +135 -0
  91. data/include/libc/termios.h +179 -0
  92. data/include/libc/unistd.h +194 -0
  93. data/include/stdalign.h +16 -0
  94. data/include/stdarg.h +31 -0
  95. data/include/stdatomic.h +158 -0
  96. data/include/stdbool.h +15 -0
  97. data/include/stdckdint.h +28 -0
  98. data/include/stddef.h +60 -0
  99. data/include/stdnoreturn.h +18 -0
  100. data/include/x86intrin.h +16 -0
  101. data/lib/rubycc/backend/aarch64.rb +1724 -0
  102. data/lib/rubycc/backend/x86_64.rb +1369 -0
  103. data/lib/rubycc/compile_error.rb +36 -0
  104. data/lib/rubycc/compiler.rb +305 -0
  105. data/lib/rubycc/doctor/builder.rb +151 -0
  106. data/lib/rubycc/doctor/cli.rb +190 -0
  107. data/lib/rubycc/doctor/fetcher.rb +84 -0
  108. data/lib/rubycc/doctor/gemfile.rb +115 -0
  109. data/lib/rubycc/doctor/verified_gems.rb +114 -0
  110. data/lib/rubycc/doctor.rb +16 -0
  111. data/lib/rubycc/driver.rb +463 -0
  112. data/lib/rubycc/front/ast.rb +528 -0
  113. data/lib/rubycc/front/constant_evaluator.rb +631 -0
  114. data/lib/rubycc/front/initializer_resolver.rb +592 -0
  115. data/lib/rubycc/front/lexeme_reader.rb +460 -0
  116. data/lib/rubycc/front/lexer.rb +232 -0
  117. data/lib/rubycc/front/parser.rb +4122 -0
  118. data/lib/rubycc/front/token.rb +49 -0
  119. data/lib/rubycc/ir/call_convention.rb +486 -0
  120. data/lib/rubycc/ir/generator.rb +6036 -0
  121. data/lib/rubycc/ir/ir.rb +417 -0
  122. data/lib/rubycc/link/compat_runtime.rb +89 -0
  123. data/lib/rubycc/link/errors.rb +13 -0
  124. data/lib/rubycc/link/executable_linker.rb +405 -0
  125. data/lib/rubycc/link/library_resolver.rb +437 -0
  126. data/lib/rubycc/link/partial_linker.rb +546 -0
  127. data/lib/rubycc/link/shared_linker.rb +1732 -0
  128. data/lib/rubycc/mkmf_shim.rb +83 -0
  129. data/lib/rubycc/objfile/ar_archive.rb +437 -0
  130. data/lib/rubycc/objfile/elf_reader.rb +646 -0
  131. data/lib/rubycc/objfile/elf_writer.rb +891 -0
  132. data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
  133. data/lib/rubycc/pkgconf/cli.rb +139 -0
  134. data/lib/rubycc/pkgconf/errors.rb +39 -0
  135. data/lib/rubycc/pkgconf/model.rb +28 -0
  136. data/lib/rubycc/pkgconf/parser.rb +109 -0
  137. data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
  138. data/lib/rubycc/pkgconf/resolver.rb +70 -0
  139. data/lib/rubycc/pkgconf/search_path.rb +38 -0
  140. data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
  141. data/lib/rubycc/preprocess/constant_expression.rb +219 -0
  142. data/lib/rubycc/preprocess/glibc_version.rb +124 -0
  143. data/lib/rubycc/preprocess/pp_token.rb +62 -0
  144. data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
  145. data/lib/rubycc/preprocess/scanner.rb +290 -0
  146. data/lib/rubycc/preprocess/token_converter.rb +157 -0
  147. data/lib/rubycc/rmake/cli.rb +143 -0
  148. data/lib/rubycc/rmake/errors.rb +71 -0
  149. data/lib/rubycc/rmake/executor.rb +818 -0
  150. data/lib/rubycc/rmake/expander.rb +251 -0
  151. data/lib/rubycc/rmake/makefile.rb +352 -0
  152. data/lib/rubycc/rmake/model.rb +137 -0
  153. data/lib/rubycc/rmake/parser.rb +226 -0
  154. data/lib/rubycc/rmake/rmake.rb +15 -0
  155. data/lib/rubycc/type.rb +1236 -0
  156. data/lib/rubycc/version.rb +5 -0
  157. data/lib/rubycc.rb +36 -0
  158. data/lib/rubygems_plugin.rb +102 -0
  159. metadata +219 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+
5
+ module Rubycc
6
+ module Rmake
7
+ # Turns raw make text with `$(...)` / `${...}` / `$x` references into its
8
+ # expanded string. It implements exactly the reference forms the mkmf corpus
9
+ # uses (test/fixtures/mkmf): plain variable references, nested references,
10
+ # the `$(var:from=to)` substitution reference, and the automatic variables
11
+ # ($@, $<, $^, $*, and their D/F directory/file variants). Undefined
12
+ # variables expand to the empty string, matching make.
13
+ #
14
+ # Recursive (`=`) variables are stored unexpanded and expanded on every
15
+ # reference; simple (`:=`) variables are stored already-expanded. That split
16
+ # lives in the Variable value object, so the expander only has to re-expand
17
+ # whatever text a recursive variable holds — which is where an `A=$(B)` /
18
+ # `B=$(A)` cycle would otherwise recurse forever, so every variable
19
+ # dereference spends one unit of the depth budget below.
20
+ class Expander
21
+ # DoS fail-safe: the maximum number of nested variable dereferences during
22
+ # a single expansion. A reference cycle between recursive variables would
23
+ # recurse without bound; this cap converts that into an ExpansionError long
24
+ # before the Ruby stack is exhausted. Legitimate mkmf variable chains
25
+ # (e.g. rubyarchhdrdir -> rubyhdrdir -> includedir -> prefix) are well
26
+ # under a dozen deep, so the limit is generous.
27
+ MAX_EXPANSION_DEPTH = 200
28
+
29
+ # DoS fail-safe, part two: the depth cap above only counts how deep the
30
+ # call stack goes, so it says nothing about *fan-out*. A chain like
31
+ # `A1 = $(A0)$(A0)` / `A2 = $(A1)$(A1)` / ... stays only n levels deep
32
+ # while doing 2^n work, so a 30-line Makefile can wedge the parser (`:=`
33
+ # assignments expand at parse time, before any target is built). The two
34
+ # budgets below bound the total work of one top-level expansion, in the
35
+ # same spirit as the preprocessor's EXPANSION_TOKEN_LIMIT.
36
+ #
37
+ # Both are needed, because each misses the explosion the other catches:
38
+ #
39
+ # * MAX_EXPANSION_REFERENCES counts variable dereferences. It is the only
40
+ # one that sees a fan-out whose leaves expand to nothing (`A0 =` empty
41
+ # still costs 2^n dereferences while producing zero characters), and it
42
+ # is also what catches a cycle that grows the call count rather than the
43
+ # text.
44
+ # * MAX_EXPANSION_OUTPUT counts characters produced. It is the only one
45
+ # that sees a blow-up driven by text size rather than reference count
46
+ # (`BIG = <megabytes>` referenced a handful of times per level exhausts
47
+ # memory long before the reference count is reached).
48
+ #
49
+ # Both limits are orders of magnitude above what the mkmf corpus needs:
50
+ # measured over the rmake test suite (test/fixtures/mkmf included), the
51
+ # heaviest single expansion resolves 32 references and yields ~4 KB.
52
+ #
53
+ # The reference cap is deliberately kept small rather than merely "safe":
54
+ # a budget is spent per top-level call, so the time it takes to reach the
55
+ # cap is itself the attacker's resource — a Makefile with many `:=`
56
+ # assignments multiplies it by the number of assignments. 100_000 still
57
+ # leaves ~3000x headroom over the measured 32 while tripping in well under
58
+ # a second.
59
+ MAX_EXPANSION_REFERENCES = 100_000
60
+ MAX_EXPANSION_OUTPUT = 4_194_304 # 4 MiB of characters
61
+
62
+ # Base characters of the automatic variables rmake recognises. Each may be
63
+ # written bare ($@) or parenthesised ($(@)), and the parenthesised form may
64
+ # carry a D/F modifier ($(@D) = directory part, $(@F) = file part).
65
+ AUTOMATIC_CHARS = %w[@ < ^ ? * + %].freeze
66
+
67
+ # variables:: Hash{String => Variable}
68
+ def initialize(variables)
69
+ @variables = variables
70
+ @references = 0
71
+ @output_chars = 0
72
+ end
73
+
74
+ # Expand +text+. +autos+ is a Hash{String => String} keyed by automatic
75
+ # variable base character (e.g. {"@" => "foo.o", "<" => "foo.c"}); pass nil
76
+ # outside a recipe, where automatic variables expand to "".
77
+ def expand(text, autos = nil, depth = 0)
78
+ # depth 0 is the entry point from outside, so one external call gets one
79
+ # budget; the recursive calls below all pass depth + 1 and therefore
80
+ # share it.
81
+ if depth.zero?
82
+ @references = 0
83
+ @output_chars = 0
84
+ end
85
+ guard_depth(depth)
86
+ out = +""
87
+ i = 0
88
+ n = text.length
89
+ while i < n
90
+ c = text[i]
91
+ if c == "$"
92
+ consumed, value = expand_reference(text, i, autos, depth)
93
+ @output_chars += value.length
94
+ guard_output
95
+ out << value
96
+ i += consumed
97
+ else
98
+ @output_chars += 1
99
+ guard_output
100
+ out << c
101
+ i += 1
102
+ end
103
+ end
104
+ out
105
+ end
106
+
107
+ private
108
+
109
+ def guard_depth(depth)
110
+ return if depth <= MAX_EXPANSION_DEPTH
111
+
112
+ raise ExpansionError,
113
+ "variable expansion exceeded #{MAX_EXPANSION_DEPTH} levels " \
114
+ "(likely a reference cycle between recursive variables)"
115
+ end
116
+
117
+ def guard_references
118
+ return if @references <= MAX_EXPANSION_REFERENCES
119
+
120
+ raise ExpansionError,
121
+ "variable expansion resolved more than #{MAX_EXPANSION_REFERENCES} references " \
122
+ "(likely a reference cycle or an exponentially expanding variable chain)"
123
+ end
124
+
125
+ def guard_output
126
+ return if @output_chars <= MAX_EXPANSION_OUTPUT
127
+
128
+ raise ExpansionError,
129
+ "variable expansion produced more than #{MAX_EXPANSION_OUTPUT} characters " \
130
+ "(likely a reference cycle or an exponentially expanding variable chain)"
131
+ end
132
+
133
+ # Parse one reference starting at the `$` at +i+. Returns [chars_consumed,
134
+ # expanded_value].
135
+ def expand_reference(text, i, autos, depth)
136
+ nxt = text[i + 1]
137
+ case nxt
138
+ when nil
139
+ # Trailing bare `$` — emit it literally.
140
+ [1, "$"]
141
+ when "$"
142
+ [2, "$"]
143
+ when "(", "{"
144
+ close = (nxt == "(" ? ")" : "}")
145
+ inner, len = read_balanced(text, i + 2, nxt, close)
146
+ [2 + len, expand_parenthesised(inner, autos, depth)]
147
+ else
148
+ # Single-character reference: $@, $<, $x ...
149
+ [2, resolve_name(nxt, autos, depth)]
150
+ end
151
+ end
152
+
153
+ # Read up to the matching close bracket, honouring nesting so that
154
+ # `$(a$(b))` is read whole. Returns [inner_without_brackets,
155
+ # chars_consumed_including_close]. An unterminated reference is treated as
156
+ # extending to end of text (make is similarly lenient).
157
+ def read_balanced(text, start, open, close)
158
+ depth = 1
159
+ i = start
160
+ n = text.length
161
+ while i < n
162
+ c = text[i]
163
+ if c == open
164
+ depth += 1
165
+ elsif c == close
166
+ depth -= 1
167
+ return [text[start...i], (i - start) + 1] if depth.zero?
168
+ end
169
+ i += 1
170
+ end
171
+ [text[start..], n - start]
172
+ end
173
+
174
+ def expand_parenthesised(inner, autos, depth)
175
+ # Substitution reference: $(var:from=to). Split on the first ':' and then
176
+ # the remainder on the first '=' so a replacement text may itself contain
177
+ # '=' or spaces (e.g. `$(ECHO1:0=@ echo)`).
178
+ if (colon = inner.index(":")) && inner.index("=", colon)
179
+ name = inner[0...colon]
180
+ from, to = inner[(colon + 1)..].split("=", 2)
181
+ value = resolve_name(expand(name, autos, depth + 1), autos, depth)
182
+ return substitute_suffix(value, expand(from, autos, depth + 1), expand(to.to_s, autos, depth + 1))
183
+ end
184
+
185
+ name = expand(inner, autos, depth + 1)
186
+ resolve_name(name, autos, depth)
187
+ end
188
+
189
+ # Resolve a fully-expanded variable/automatic name to its value. This is
190
+ # the one place every dereference passes through — the `$x` branch of
191
+ # #expand_reference and both branches of #expand_parenthesised call it —
192
+ # so the reference budget is charged here and nowhere else.
193
+ def resolve_name(name, autos, depth)
194
+ @references += 1
195
+ guard_references
196
+ auto = automatic_value(name, autos)
197
+ return auto unless auto.nil?
198
+
199
+ var = @variables[name]
200
+ return "" if var.nil?
201
+
202
+ var.simple? ? var.value : expand(var.value, autos, depth + 1)
203
+ end
204
+
205
+ # Returns the automatic-variable value for +name+, or nil when +name+ is
206
+ # not an automatic variable. Outside a recipe (+autos+ nil) the automatic
207
+ # variables are defined but empty, matching make.
208
+ def automatic_value(name, autos)
209
+ base = name
210
+ modifier = nil
211
+ if name.length == 2 && (name[1] == "D" || name[1] == "F") && AUTOMATIC_CHARS.include?(name[0])
212
+ base = name[0]
213
+ modifier = name[1]
214
+ end
215
+ return nil unless AUTOMATIC_CHARS.include?(base) && name.length <= 2
216
+
217
+ raw = autos ? autos.fetch(base, "") : ""
218
+ case modifier
219
+ when "D" then directory_parts(raw)
220
+ when "F" then file_parts(raw)
221
+ else raw
222
+ end
223
+ end
224
+
225
+ # make's $(var:from=to) replaces +from+ at the end of each whitespace-
226
+ # separated word with +to+ (equivalent to $(patsubst %from,%to,var)).
227
+ def substitute_suffix(value, from, to)
228
+ value.split(/\s+/).reject(&:empty?).map do |word|
229
+ word.end_with?(from) ? word[0, word.length - from.length] + to : word
230
+ end.join(" ")
231
+ end
232
+
233
+ # $(@D): directory part of each word, "." when a word has no directory,
234
+ # trailing slash stripped.
235
+ def directory_parts(value)
236
+ value.split(/\s+/).reject(&:empty?).map do |word|
237
+ slash = word.rindex("/")
238
+ slash.nil? ? "." : (slash.zero? ? "/" : word[0...slash])
239
+ end.join(" ")
240
+ end
241
+
242
+ # $(@F): file (basename) part of each word.
243
+ def file_parts(value)
244
+ value.split(/\s+/).reject(&:empty?).map do |word|
245
+ slash = word.rindex("/")
246
+ slash.nil? ? word : word[(slash + 1)..]
247
+ end.join(" ")
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "model"
5
+ require_relative "expander"
6
+ require_relative "parser"
7
+ require_relative "executor"
8
+
9
+ module Rubycc
10
+ module Rmake
11
+ # The public entry point: parse Makefile text, then answer "what would make
12
+ # do?" for a goal as an execution Plan. It resolves the raw rules the Parser
13
+ # produced into a working graph — a per-target index of explicit
14
+ # prerequisites and recipes, the `.c.o:` suffix rules, the `.PHONY`/
15
+ # `.SUFFIXES` special targets and VPATH — and walks that graph in
16
+ # dependency order, comparing timestamps to decide which targets are stale.
17
+ # It only plans: recipes are expanded and their `@`/`-`/`+` prefixes
18
+ # interpreted, but nothing is run (the runner is B2).
19
+ class Makefile
20
+ # A target that names its own inference: `.c.o` builds `.o` from `.c`. Two
21
+ # dots and nothing else, which is how it stays distinct from the one-dot
22
+ # special targets `.PHONY` / `.SUFFIXES`.
23
+ SUFFIX_TARGET = /\A(\.[^.\s]+)(\.[^.\s]+)\z/
24
+
25
+ attr_reader :variables, :dir, :default_goal, :phony, :suffixes
26
+
27
+ # +overrides+ are command-line variable definitions (make's `VAR=value`
28
+ # operands) that take precedence over the Makefile's own assignments;
29
+ # +defaults+ are POSIX's built-in variables (currently just `MAKE`, seeded
30
+ # by CLI). Both are threaded to the Parser, which seeds them — protecting
31
+ # +overrides+ from any Makefile assignment, but leaving +defaults+ as
32
+ # ordinary variables a Makefile assignment can replace.
33
+ def self.parse(text, dir: ".", overrides: {}, defaults: {})
34
+ parsed = Parser.parse(text, overrides: overrides, defaults: defaults)
35
+ new(parsed.variables, parsed.rules, dir: dir)
36
+ end
37
+
38
+ def initialize(variables, rules, dir: ".")
39
+ @variables = variables
40
+ @dir = dir
41
+ @expander = Expander.new(@variables)
42
+ @explicit = {} # target name => {prereqs:, recipe:, double_colon:}
43
+ @suffix_rules = [] # SuffixRule
44
+ @phony = []
45
+ @suffixes = []
46
+ @default_goal = nil
47
+ index_rules(rules)
48
+ end
49
+
50
+ # Compute the execution plan for +goal+ (the default goal when omitted),
51
+ # judging staleness against the filesystem as of +now+ is implicit in the
52
+ # file mtimes read from #dir. Returns a Plan of steps in the order make
53
+ # would run them.
54
+ def plan(goal = nil, now: Time.now)
55
+ @now = now
56
+ goal ||= @default_goal
57
+ raise RmakeError, "no target specified and the Makefile has no default goal" if goal.nil?
58
+
59
+ @steps = []
60
+ @state = {}
61
+ build(goal)
62
+ Plan.new(@steps)
63
+ end
64
+
65
+ # Plan +goal+ and then run it through the shell-less Executor (B2/B3). This
66
+ # is the "make it" entry point that pairs with #plan (the "what would make
67
+ # do" entry point). +dry_run+ prints the recipe lines without running them
68
+ # (make -n) and matches #plan's #command_lines.
69
+ #
70
+ # +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
73
+ # rubycc's own Driver in a forked child instead of being exec'd, so no
74
+ # external compiler is needed. It defaults off, leaving the B2 behaviour
75
+ # (every command exec'd) exactly as it was. +jobs+ is the maximum number of
76
+ # independent steps to build concurrently (`-j`; default 1 = sequential).
77
+ #
78
+ # Returns the Plan that was executed; raises CommandFailedError /
79
+ # UnsupportedRecipeError on the first failing or uninterpretable recipe line.
80
+ def run(goal = nil, out: $stdout, err: $stderr, dry_run: false, env: ENV, now: Time.now,
81
+ tools: nil, jobs: 1)
82
+ computed = plan(goal, now: now)
83
+ Executor.new(dir: @dir, out: out, err: err, dry_run: dry_run, env: env,
84
+ tools: tools ? tool_programs : [], jobs: jobs).execute(computed)
85
+ computed
86
+ end
87
+
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
94
+ [variable_value("CC"), variable_value("LDSHARED")]
95
+ .map { |value| value.split(/\s+/).reject(&:empty?).first }
96
+ .compact.uniq
97
+ end
98
+
99
+ # The fully-expanded value of a variable (empty string when undefined).
100
+ def variable_value(name)
101
+ @expander.expand("$(#{name})")
102
+ end
103
+
104
+ private
105
+
106
+ # --- turning raw rules into the working graph ------------------------
107
+
108
+ def index_rules(rules)
109
+ rules.each do |rule|
110
+ targets = expand_words(rule.targets)
111
+ prereqs = expand_words(rule.prerequisites)
112
+ targets.each { |t| classify_target(t, prereqs, rule) }
113
+ end
114
+ end
115
+
116
+ def classify_target(target, prereqs, rule)
117
+ case target
118
+ when ".PHONY"
119
+ @phony.concat(prereqs)
120
+ when ".SUFFIXES"
121
+ prereqs.empty? ? @suffixes.clear : @suffixes.concat(prereqs)
122
+ else
123
+ if (m = SUFFIX_TARGET.match(target)) && prereqs.empty? && !rule.recipe.empty?
124
+ @suffix_rules << SuffixRule.new(from_suffix: m[1], to_suffix: m[2], recipe: rule.recipe)
125
+ else
126
+ record_explicit(target, prereqs, rule)
127
+ end
128
+ end
129
+ end
130
+
131
+ def record_explicit(target, prereqs, rule)
132
+ entry = (@explicit[target] ||= { prereqs: [], recipe: nil, double_colon: rule.double_colon? })
133
+ entry[:prereqs].concat(prereqs)
134
+ # The last rule to carry a recipe wins (make's behaviour for `:` rules);
135
+ # prerequisite-only rules just contribute dependencies.
136
+ entry[:recipe] = rule.recipe unless rule.recipe.empty?
137
+ @default_goal ||= target
138
+ end
139
+
140
+ # --- dependency walk + staleness ------------------------------------
141
+
142
+ # Returns [stale?, mtime_or_nil, exposed_steps]. +stale?+ propagates upward:
143
+ # a prerequisite that will be rebuilt makes its dependents stale even if
144
+ # their own file is newer, matching make's dry-run reasoning.
145
+ #
146
+ # +exposed_steps+ is the set of step targets a parent must wait for before
147
+ # this target counts as ready — the dependency edges the `-j` scheduler
148
+ # walks. A target that is itself a step exposes just itself (its own
149
+ # prerequisite steps become that step's #prereqs and are hidden behind it);
150
+ # a target that is not a step forwards its prerequisites' exposed steps, so
151
+ # a phony aggregate like `all` exposes the real steps beneath it.
152
+ def build(target)
153
+ return @state[target] if @state.key?(target)
154
+
155
+ # Tentative entry breaks any dependency cycle without looping forever.
156
+ @state[target] = [false, file_mtime(target), []]
157
+ node = resolve(target)
158
+
159
+ rebuilt = false
160
+ newest = nil
161
+ dep_steps = []
162
+ node[:prereqs].each do |p|
163
+ stale, mtime, exposed = build(p)
164
+ rebuilt ||= stale
165
+ newest = newer(newest, mtime)
166
+ dep_steps.concat(exposed)
167
+ end
168
+
169
+ own = node[:mtime]
170
+ stale = node[:phony] || own.nil? || rebuilt || (newest && own && newest > own)
171
+
172
+ if stale && node[:recipe]
173
+ step = build_step(target, node)
174
+ step.prereqs = dep_steps.uniq
175
+ @steps << step
176
+ exposed = [target]
177
+ else
178
+ exposed = dep_steps.uniq
179
+ end
180
+
181
+ @state[target] = [stale, own, exposed]
182
+ end
183
+
184
+ # Assemble everything needed to judge and, if stale, build +target+: its
185
+ # prerequisites, the recipe (explicit or inferred), the automatic-variable
186
+ # source/stem for a suffix rule, its mtime and whether it is phony.
187
+ def resolve(target)
188
+ entry = @explicit[target]
189
+ prereqs = entry ? entry[:prereqs].dup : []
190
+ recipe = entry && entry[:recipe]
191
+ source = nil
192
+ stem = nil
193
+
194
+ if recipe.nil?
195
+ inferred = infer_suffix_rule(target)
196
+ if inferred
197
+ rule, source, stem = inferred
198
+ recipe = rule.recipe
199
+ prereqs += [source]
200
+ end
201
+ end
202
+
203
+ {
204
+ target: target,
205
+ prereqs: prereqs,
206
+ recipe: recipe,
207
+ source: source,
208
+ stem: stem,
209
+ mtime: file_mtime(target),
210
+ phony: @phony.include?(target)
211
+ }
212
+ end
213
+
214
+ # Find an inference rule for +target+: its suffix must be a known
215
+ # `.SUFFIXES` entry and produced by some `.X.Y:` rule whose same-stem
216
+ # source file exists (searched along VPATH). Returns [rule, source_name,
217
+ # stem] or nil. Candidate source suffixes are tried in `.SUFFIXES` order,
218
+ # as make does.
219
+ def infer_suffix_rule(target)
220
+ to_suffix = suffix_of(target)
221
+ return nil unless to_suffix && @suffixes.include?(to_suffix)
222
+
223
+ stem = target[0...(target.length - to_suffix.length)]
224
+ @suffixes.each do |from|
225
+ rule = @suffix_rules.find { |r| r.from_suffix == from && r.to_suffix == to_suffix }
226
+ next unless rule
227
+
228
+ resolved = resolve_prerequisite("#{stem}#{from}")
229
+ return [rule, resolved, stem] if resolved
230
+ end
231
+ nil
232
+ end
233
+
234
+ def suffix_of(name)
235
+ base = File.basename(name)
236
+ dot = base.rindex(".")
237
+ return nil if dot.nil? || dot.zero?
238
+
239
+ base[dot..]
240
+ end
241
+
242
+ # --- recipe expansion ------------------------------------------------
243
+
244
+ def build_step(target, node)
245
+ autos = automatic_variables(target, node)
246
+ commands = node[:recipe].map do |raw|
247
+ expanded = @expander.expand(raw, autos)
248
+ text, silent, ignore, force = split_prefixes(expanded)
249
+ Command.new(text: text, silent: silent, ignore_error: ignore, force: force)
250
+ end
251
+ Step.new(target: target, commands: commands)
252
+ end
253
+
254
+ def automatic_variables(target, node)
255
+ first = node[:source] || node[:prereqs].first || ""
256
+ all = node[:prereqs].join(" ")
257
+ {
258
+ "@" => target,
259
+ "<" => first,
260
+ "^" => all,
261
+ "+" => all,
262
+ "?" => all,
263
+ "*" => node[:stem] || strip_known_suffix(target)
264
+ }
265
+ end
266
+
267
+ def strip_known_suffix(target)
268
+ suffix = suffix_of(target)
269
+ suffix && @suffixes.include?(suffix) ? target[0...(target.length - suffix.length)] : target
270
+ end
271
+
272
+ # Interpret the leading recipe-line prefixes make recognises: `@` (silent),
273
+ # `-` (ignore errors) and `+` (run even under -n). They may appear in any
274
+ # order and be separated by whitespace; the remaining text is the command
275
+ # as make would print it under -n.
276
+ def split_prefixes(line)
277
+ silent = ignore = force = false
278
+ i = 0
279
+ loop do
280
+ i += 1 while i < line.length && whitespace?(line[i])
281
+ case line[i]
282
+ when "@" then silent = true
283
+ when "-" then ignore = true
284
+ when "+" then force = true
285
+ else break
286
+ end
287
+ i += 1
288
+ end
289
+ [line[i..] || "", silent, ignore, force]
290
+ end
291
+
292
+ def whitespace?(char)
293
+ char == " " || char == "\t"
294
+ end
295
+
296
+ # --- filesystem / VPATH ---------------------------------------------
297
+
298
+ def file_mtime(name)
299
+ located = locate(name)
300
+ located ? File.mtime(located[1]) : nil
301
+ end
302
+
303
+ # The name a prerequisite resolves to after VPATH search (what $< would
304
+ # hold), or nil when no such file exists anywhere on the path.
305
+ def resolve_prerequisite(name)
306
+ located = locate(name)
307
+ located && located[0]
308
+ end
309
+
310
+ # Search for +name+ as a plain path first, then along VPATH. Returns
311
+ # [name_to_use, full_path] or nil. A "." VPATH entry (mkmf's srcdir) leaves
312
+ # the name unadorned, so $< stays `parser.c` rather than `./parser.c`.
313
+ def locate(name)
314
+ direct = full_path(name)
315
+ return [name, direct] if File.exist?(direct)
316
+ return nil if absolute?(name)
317
+
318
+ vpath_dirs.each do |d|
319
+ candidate = (d == ".") ? name : File.join(d, name)
320
+ full = full_path(candidate)
321
+ return [candidate, full] if File.exist?(full)
322
+ end
323
+ nil
324
+ end
325
+
326
+ def vpath_dirs
327
+ @vpath_dirs ||= variable_value("VPATH").split(/[:\s]+/).reject(&:empty?)
328
+ end
329
+
330
+ def full_path(name)
331
+ absolute?(name) ? name : File.join(@dir, name)
332
+ end
333
+
334
+ def absolute?(name)
335
+ name.start_with?("/")
336
+ end
337
+
338
+ # --- helpers ---------------------------------------------------------
339
+
340
+ def expand_words(raw_words)
341
+ raw_words.flat_map { |w| @expander.expand(w).split(/[ \t]+/) }.reject(&:empty?)
342
+ end
343
+
344
+ def newer(a, b)
345
+ return b if a.nil?
346
+ return a if b.nil?
347
+
348
+ a > b ? a : b
349
+ end
350
+ end
351
+ end
352
+ end