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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +56 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +52 -0
- data/README.md +208 -0
- data/data/README.md +117 -0
- data/data/r10_corpus_scan.json +4082 -0
- data/data/r10_manual_classification.json +5342 -0
- data/data/r10_verification_m4a.json +1531 -0
- data/data/r10_verification_m4b.json +1818 -0
- data/data/r10_verification_m4c.json +1489 -0
- data/data/r10_verification_m4d.json +318 -0
- data/data/r10_verification_rbs.json +192 -0
- data/data/verified_gems.json +397 -0
- data/exe/rmake +16 -0
- data/exe/rubycc +9 -0
- data/exe/rubycc-ar +114 -0
- data/exe/rubycc-doctor +14 -0
- data/exe/rubycc-pkgconf +8 -0
- data/include/float.h +87 -0
- data/include/iso646.h +18 -0
- data/include/libc/alloca.h +18 -0
- data/include/libc/arpa/inet.h +61 -0
- data/include/libc/assert.h +43 -0
- data/include/libc/dirent.h +72 -0
- data/include/libc/dlfcn.h +58 -0
- data/include/libc/features.h +205 -0
- data/include/libc/glibc/aarch64/ctype.h +108 -0
- data/include/libc/glibc/aarch64/endian.h +54 -0
- data/include/libc/glibc/aarch64/errno.h +154 -0
- data/include/libc/glibc/aarch64/fcntl.h +144 -0
- data/include/libc/glibc/aarch64/inttypes.h +182 -0
- data/include/libc/glibc/aarch64/limits.h +63 -0
- data/include/libc/glibc/aarch64/pthread.h +131 -0
- data/include/libc/glibc/aarch64/setjmp.h +84 -0
- data/include/libc/glibc/aarch64/stdint.h +174 -0
- data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
- data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
- data/include/libc/glibc/aarch64/sys/select.h +75 -0
- data/include/libc/glibc/aarch64/sys/stat.h +136 -0
- data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
- data/include/libc/glibc/aarch64/sys/time.h +74 -0
- data/include/libc/glibc/aarch64/sys/types.h +127 -0
- data/include/libc/glibc/aarch64/time.h +113 -0
- data/include/libc/glibc/x86_64/ctype.h +108 -0
- data/include/libc/glibc/x86_64/endian.h +54 -0
- data/include/libc/glibc/x86_64/errno.h +154 -0
- data/include/libc/glibc/x86_64/fcntl.h +134 -0
- data/include/libc/glibc/x86_64/inttypes.h +182 -0
- data/include/libc/glibc/x86_64/limits.h +61 -0
- data/include/libc/glibc/x86_64/pthread.h +111 -0
- data/include/libc/glibc/x86_64/setjmp.h +82 -0
- data/include/libc/glibc/x86_64/stdint.h +163 -0
- data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
- data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
- data/include/libc/glibc/x86_64/sys/select.h +75 -0
- data/include/libc/glibc/x86_64/sys/stat.h +130 -0
- data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
- data/include/libc/glibc/x86_64/sys/time.h +74 -0
- data/include/libc/glibc/x86_64/sys/types.h +123 -0
- data/include/libc/glibc/x86_64/time.h +113 -0
- data/include/libc/grp.h +50 -0
- data/include/libc/langinfo.h +126 -0
- data/include/libc/link.h +18 -0
- data/include/libc/locale.h +83 -0
- data/include/libc/math.h +191 -0
- data/include/libc/netinet/in.h +126 -0
- data/include/libc/netinet/tcp.h +44 -0
- data/include/libc/poll.h +46 -0
- data/include/libc/pwd.h +58 -0
- data/include/libc/regex.h +53 -0
- data/include/libc/sched.h +35 -0
- data/include/libc/signal.h +204 -0
- data/include/libc/stdio.h +157 -0
- data/include/libc/stdlib.h +92 -0
- data/include/libc/string.h +74 -0
- data/include/libc/strings.h +25 -0
- data/include/libc/sys/cdefs.h +126 -0
- data/include/libc/sys/inotify.h +111 -0
- data/include/libc/sys/ioctl.h +36 -0
- data/include/libc/sys/mman.h +65 -0
- data/include/libc/sys/param.h +41 -0
- data/include/libc/sys/resource.h +109 -0
- data/include/libc/sys/socket.h +167 -0
- data/include/libc/sys/statfs.h +82 -0
- data/include/libc/sys/timerfd.h +55 -0
- data/include/libc/sys/uio.h +40 -0
- data/include/libc/sys/un.h +25 -0
- data/include/libc/sys/utsname.h +35 -0
- data/include/libc/sys/wait.h +135 -0
- data/include/libc/termios.h +179 -0
- data/include/libc/unistd.h +194 -0
- data/include/stdalign.h +16 -0
- data/include/stdarg.h +31 -0
- data/include/stdatomic.h +158 -0
- data/include/stdbool.h +15 -0
- data/include/stdckdint.h +28 -0
- data/include/stddef.h +60 -0
- data/include/stdnoreturn.h +18 -0
- data/include/x86intrin.h +16 -0
- data/lib/rubycc/backend/aarch64.rb +1724 -0
- data/lib/rubycc/backend/x86_64.rb +1369 -0
- data/lib/rubycc/compile_error.rb +36 -0
- data/lib/rubycc/compiler.rb +305 -0
- data/lib/rubycc/doctor/builder.rb +151 -0
- data/lib/rubycc/doctor/cli.rb +190 -0
- data/lib/rubycc/doctor/fetcher.rb +84 -0
- data/lib/rubycc/doctor/gemfile.rb +115 -0
- data/lib/rubycc/doctor/verified_gems.rb +114 -0
- data/lib/rubycc/doctor.rb +16 -0
- data/lib/rubycc/driver.rb +463 -0
- data/lib/rubycc/front/ast.rb +528 -0
- data/lib/rubycc/front/constant_evaluator.rb +631 -0
- data/lib/rubycc/front/initializer_resolver.rb +592 -0
- data/lib/rubycc/front/lexeme_reader.rb +460 -0
- data/lib/rubycc/front/lexer.rb +232 -0
- data/lib/rubycc/front/parser.rb +4122 -0
- data/lib/rubycc/front/token.rb +49 -0
- data/lib/rubycc/ir/call_convention.rb +486 -0
- data/lib/rubycc/ir/generator.rb +6036 -0
- data/lib/rubycc/ir/ir.rb +417 -0
- data/lib/rubycc/link/compat_runtime.rb +89 -0
- data/lib/rubycc/link/errors.rb +13 -0
- data/lib/rubycc/link/executable_linker.rb +405 -0
- data/lib/rubycc/link/library_resolver.rb +437 -0
- data/lib/rubycc/link/partial_linker.rb +546 -0
- data/lib/rubycc/link/shared_linker.rb +1732 -0
- data/lib/rubycc/mkmf_shim.rb +83 -0
- data/lib/rubycc/objfile/ar_archive.rb +437 -0
- data/lib/rubycc/objfile/elf_reader.rb +646 -0
- data/lib/rubycc/objfile/elf_writer.rb +891 -0
- data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
- data/lib/rubycc/pkgconf/cli.rb +139 -0
- data/lib/rubycc/pkgconf/errors.rb +39 -0
- data/lib/rubycc/pkgconf/model.rb +28 -0
- data/lib/rubycc/pkgconf/parser.rb +109 -0
- data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
- data/lib/rubycc/pkgconf/resolver.rb +70 -0
- data/lib/rubycc/pkgconf/search_path.rb +38 -0
- data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
- data/lib/rubycc/preprocess/constant_expression.rb +219 -0
- data/lib/rubycc/preprocess/glibc_version.rb +124 -0
- data/lib/rubycc/preprocess/pp_token.rb +62 -0
- data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
- data/lib/rubycc/preprocess/scanner.rb +290 -0
- data/lib/rubycc/preprocess/token_converter.rb +157 -0
- data/lib/rubycc/rmake/cli.rb +143 -0
- data/lib/rubycc/rmake/errors.rb +71 -0
- data/lib/rubycc/rmake/executor.rb +818 -0
- data/lib/rubycc/rmake/expander.rb +251 -0
- data/lib/rubycc/rmake/makefile.rb +352 -0
- data/lib/rubycc/rmake/model.rb +137 -0
- data/lib/rubycc/rmake/parser.rb +226 -0
- data/lib/rubycc/rmake/rmake.rb +15 -0
- data/lib/rubycc/type.rb +1236 -0
- data/lib/rubycc/version.rb +5 -0
- data/lib/rubycc.rb +36 -0
- data/lib/rubygems_plugin.rb +102 -0
- 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
|