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,818 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Rubycc
|
|
7
|
+
module Rmake
|
|
8
|
+
# Runs an execution Plan (from Makefile#plan) without a shell. The minimal
|
|
9
|
+
# target environment has no /bin/sh (DESIGN R5), so each recipe line is
|
|
10
|
+
# interpreted here directly: split into words (honouring quotes), joined by
|
|
11
|
+
# the connectors make's recipes use (`&&`, `||`, `;`), with `>`/`>>`/`2>`
|
|
12
|
+
# redirections and `VAR=value` / `cd` prefixes applied to the command they
|
|
13
|
+
# front. The vocabulary is fixed by the mkmf corpus (test/fixtures/mkmf): only
|
|
14
|
+
# the constructs those Makefiles actually emit are accepted, and anything else
|
|
15
|
+
# raises UnsupportedRecipeError instead of being run through a shell that does
|
|
16
|
+
# not exist.
|
|
17
|
+
#
|
|
18
|
+
# The utilities the recipes invoke (rm, mkdir, install, echo, ...) are
|
|
19
|
+
# reimplemented on top of FileUtils — the "no external tool" implementation is
|
|
20
|
+
# authoritative, so `/usr/bin/install` and a bare `install` behave identically
|
|
21
|
+
# and no process is spawned for them. Everything the runner does not recognise
|
|
22
|
+
# as a builtin (the compiler and linker, `$(CC)`/`$(LDSHARED)`) is exec'd
|
|
23
|
+
# directly with an argv array; the runner never builds a shell command string.
|
|
24
|
+
#
|
|
25
|
+
# 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,
|
|
31
|
+
# honouring the plan's dependency edges and buffering each worker's output to
|
|
32
|
+
# flush it whole when the step finishes (make -O's un-interleaved output).
|
|
33
|
+
class Executor
|
|
34
|
+
# The utilities reimplemented in-process, keyed by the command's basename so
|
|
35
|
+
# that `/usr/bin/mkdir` and `mkdir` resolve to the same builtin. `:` is
|
|
36
|
+
# 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
|
|
39
|
+
|
|
40
|
+
# 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.
|
|
44
|
+
class LineState
|
|
45
|
+
attr_accessor :cwd, :failure_reason
|
|
46
|
+
|
|
47
|
+
def initialize(cwd)
|
|
48
|
+
@cwd = cwd
|
|
49
|
+
@failure_reason = nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
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)
|
|
61
|
+
|
|
62
|
+
def initialize(dir:, out: $stdout, err: $stderr, dry_run: false, env: ENV,
|
|
63
|
+
tools: [], jobs: 1)
|
|
64
|
+
@dir = File.expand_path(dir)
|
|
65
|
+
@out = out
|
|
66
|
+
@err = err
|
|
67
|
+
@dry_run = dry_run
|
|
68
|
+
@env = env
|
|
69
|
+
@tools = Array(tools)
|
|
70
|
+
@jobs = [jobs.to_i, 1].max
|
|
71
|
+
# In sequential mode a substituted tool is fork-isolated for its own sake;
|
|
72
|
+
# a parallel worker is already a forked step child, so it runs the Driver
|
|
73
|
+
# in-process rather than forking a second time.
|
|
74
|
+
@isolate_tool = true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Run every step of +plan+. With +jobs+ == 1 (or under -n) this is a
|
|
78
|
+
# straight sequential walk: prerequisites already precede their dependents
|
|
79
|
+
# in the plan, so the order is a valid build order. With +jobs+ > 1 the
|
|
80
|
+
# steps are dispatched by the parallel scheduler instead. Returns the plan;
|
|
81
|
+
# raises CommandFailedError / UnsupportedRecipeError at the first command
|
|
82
|
+
# that fails (and is not `-`-prefixed) or cannot be interpreted.
|
|
83
|
+
def execute(plan)
|
|
84
|
+
if @jobs > 1 && !@dry_run
|
|
85
|
+
execute_parallel(plan)
|
|
86
|
+
else
|
|
87
|
+
plan.steps.each do |step|
|
|
88
|
+
step.commands.each { |command| run_line(step.target, command) }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
plan
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# Execute one recipe line (a Command carrying its `@`/`-` attributes). make
|
|
97
|
+
# echoes the line before running it unless it is silent (`@`); under -n it
|
|
98
|
+
# echoes every line and runs nothing.
|
|
99
|
+
def run_line(target, command)
|
|
100
|
+
echo_command(command)
|
|
101
|
+
return if @dry_run
|
|
102
|
+
|
|
103
|
+
state = LineState.new(@dir)
|
|
104
|
+
ok = run_and_or_list(target, command.text, state)
|
|
105
|
+
return if ok || command.ignore_error?
|
|
106
|
+
|
|
107
|
+
raise CommandFailedError.new(target: target, command: command.text, reason: state.failure_reason)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def echo_command(command)
|
|
111
|
+
@out.puts(command.text) if @dry_run || !command.silent?
|
|
112
|
+
end
|
|
113
|
+
|
|
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.
|
|
118
|
+
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
|
|
259
|
+
end
|
|
260
|
+
[buf, j]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# --- parsing ---------------------------------------------------------
|
|
264
|
+
|
|
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
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# --- running a simple command ---------------------------------------
|
|
312
|
+
|
|
313
|
+
def run_simple(target, cmd, state, text)
|
|
314
|
+
argv = expand_globs(cmd.argv, state.cwd)
|
|
315
|
+
# A bare `VAR=value` with no command (or an empty command) is a no-op that
|
|
316
|
+
# succeeds, as it would in sh.
|
|
317
|
+
return true if argv.empty?
|
|
318
|
+
|
|
319
|
+
name = File.basename(argv[0])
|
|
320
|
+
if BUILTINS.include?(name)
|
|
321
|
+
run_builtin(target, name, argv, cmd, state, text)
|
|
322
|
+
elsif tool?(argv[0])
|
|
323
|
+
run_tool(argv, cmd, state)
|
|
324
|
+
else
|
|
325
|
+
run_external(target, argv, cmd, state)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
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?
|
|
335
|
+
|
|
336
|
+
@tools.include?(arg0) || @tools.include?(File.basename(arg0))
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# 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)
|
|
347
|
+
ok, reason = @isolate_tool ? fork_driver(driver_argv, state.cwd, cmd) \
|
|
348
|
+
: inline_driver(driver_argv, state.cwd, cmd)
|
|
349
|
+
state.failure_reason = reason unless ok
|
|
350
|
+
ok
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# rubycc's Driver, loaded on first use so rmake stays loadable on its own
|
|
354
|
+
# (the whole compiler/linker stack it drags in is not needed for a plain
|
|
355
|
+
# Makefile parse/plan). The umbrella `rubycc` is required rather than just
|
|
356
|
+
# `rubycc/driver` so every constant the link path reaches — the ELF reader's
|
|
357
|
+
# error classes the archive writer rescues among them — is defined; loading
|
|
358
|
+
# only the driver leaves some of those unresolved.
|
|
359
|
+
def driver_class
|
|
360
|
+
require "rubycc" unless defined?(Rubycc::Driver)
|
|
361
|
+
Rubycc::Driver
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Expand `*`/`?`/`[...]` globs against the command's cwd. A pattern that
|
|
365
|
+
# matches nothing is left verbatim (sh's default, and what lets `rm -f
|
|
366
|
+
# *.bak` be harmless when no backup files exist). Names come back relative
|
|
367
|
+
# to cwd, which is how the builtins resolve them.
|
|
368
|
+
def expand_globs(words, cwd)
|
|
369
|
+
words.flat_map do |w|
|
|
370
|
+
next [w] unless w =~ /[*?\[]/
|
|
371
|
+
|
|
372
|
+
matches = Dir.glob(w, base: cwd).sort
|
|
373
|
+
matches.empty? ? [w] : matches
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Open the command's redirections and yield the resulting stdout/stderr IO
|
|
378
|
+
# (nil when a stream is not redirected). Opening a `>` target creates and
|
|
379
|
+
# truncates it, which is also the whole effect of mkmf's `exit > stamp`.
|
|
380
|
+
def with_redirections(redirections, cwd)
|
|
381
|
+
opened = []
|
|
382
|
+
streams = { stdout: nil, stderr: nil }
|
|
383
|
+
redirections.each do |r|
|
|
384
|
+
path = r.path == "/dev/null" ? File::NULL : absolute(r.path, cwd)
|
|
385
|
+
io = File.open(path, r.mode == :append ? "a" : "w")
|
|
386
|
+
opened << io
|
|
387
|
+
streams[r.stream] = io
|
|
388
|
+
end
|
|
389
|
+
yield(streams[:stdout], streams[:stderr])
|
|
390
|
+
ensure
|
|
391
|
+
opened.each(&:close)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Run an unknown command (the compiler/linker) as a real process with an
|
|
395
|
+
# argv array — never a shell string — honouring the command's env, cwd and
|
|
396
|
+
# redirections. A missing executable is a normal build failure.
|
|
397
|
+
def run_external(target, argv, cmd, state)
|
|
398
|
+
with_redirections(cmd.redirections, state.cwd) do |out_io, err_io|
|
|
399
|
+
options = { chdir: state.cwd }
|
|
400
|
+
options[:out] = out_io if out_io
|
|
401
|
+
options[:err] = err_io if err_io
|
|
402
|
+
begin
|
|
403
|
+
pid = Process.spawn(env_overrides(cmd.assignments), *argv, options)
|
|
404
|
+
_, status = Process.waitpid2(pid)
|
|
405
|
+
state.failure_reason = "exited with status #{status.exitstatus}" unless status.success?
|
|
406
|
+
status.success?
|
|
407
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
408
|
+
state.failure_reason = "cannot execute #{argv[0]}: #{e.message}"
|
|
409
|
+
false
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def env_overrides(assignments)
|
|
415
|
+
assignments.each_with_object({}) do |a, h|
|
|
416
|
+
name, value = a.split("=", 2)
|
|
417
|
+
h[name] = value
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# --- in-process tool invocation (Driver) -----------------------------
|
|
422
|
+
|
|
423
|
+
# Run the Driver in a forked child (sequential mode). The child chdirs,
|
|
424
|
+
# applies the command's `VAR=value` prefixes, points the Driver's streams at
|
|
425
|
+
# the command's redirections or a capture pipe, and exits with the Driver's
|
|
426
|
+
# status; the parent drains the pipe, waits, and forwards whatever the child
|
|
427
|
+
# printed to its own output. `exit!` is used so the child never runs Ruby's
|
|
428
|
+
# at_exit hooks (Minitest's reporter among them). Returns [ok?, reason].
|
|
429
|
+
def fork_driver(driver_argv, cwd, cmd)
|
|
430
|
+
reader, writer = IO.pipe
|
|
431
|
+
pid = fork do
|
|
432
|
+
reader.close
|
|
433
|
+
status = 1
|
|
434
|
+
begin
|
|
435
|
+
Dir.chdir(cwd)
|
|
436
|
+
env_overrides(cmd.assignments).each { |k, v| ENV[k] = v }
|
|
437
|
+
out_io, err_io = redirection_ios(cmd.redirections, cwd)
|
|
438
|
+
status = driver_class.run(driver_argv, stdout: out_io || writer, stderr: err_io || writer)
|
|
439
|
+
[out_io, err_io].compact.each(&:close)
|
|
440
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
441
|
+
safe_puts(writer, "rmake: rubycc: #{e.class}: #{e.message}")
|
|
442
|
+
status = 1
|
|
443
|
+
end
|
|
444
|
+
writer.flush
|
|
445
|
+
exit!(status)
|
|
446
|
+
end
|
|
447
|
+
writer.close
|
|
448
|
+
output = reader.read
|
|
449
|
+
reader.close
|
|
450
|
+
_, status = Process.waitpid2(pid)
|
|
451
|
+
@out.write(output) unless output.empty?
|
|
452
|
+
status.success? ? [true, nil] : [false, tool_reason(status)]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Run the Driver in the current process (parallel mode: the caller is
|
|
456
|
+
# already an isolated step worker). chdir is block-scoped so a step's later
|
|
457
|
+
# commands are unaffected. Returns [ok?, reason].
|
|
458
|
+
def inline_driver(driver_argv, cwd, cmd)
|
|
459
|
+
out_io, err_io = redirection_ios(cmd.redirections, cwd)
|
|
460
|
+
status = 1
|
|
461
|
+
Dir.chdir(cwd) do
|
|
462
|
+
with_env(env_overrides(cmd.assignments)) do
|
|
463
|
+
status = driver_class.run(driver_argv, stdout: out_io || @out, stderr: err_io || @err)
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
[out_io, err_io].compact.each(&:close)
|
|
467
|
+
status.zero? ? [true, nil] : [false, "rubycc exited with status #{status}"]
|
|
468
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
469
|
+
[false, "#{e.class}: #{e.message}"]
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
def tool_reason(status)
|
|
473
|
+
if status.signaled?
|
|
474
|
+
"rubycc terminated by signal #{status.termsig}"
|
|
475
|
+
else
|
|
476
|
+
"rubycc exited with status #{status.exitstatus}"
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Temporarily overlay ENV with +overrides+ for the block, restoring it after
|
|
481
|
+
# (used so a tool line's `VAR=value` prefix does not leak into later work).
|
|
482
|
+
def with_env(overrides)
|
|
483
|
+
return yield if overrides.empty?
|
|
484
|
+
|
|
485
|
+
saved = overrides.keys.to_h { |k| [k, ENV[k]] }
|
|
486
|
+
overrides.each { |k, v| ENV[k] = v }
|
|
487
|
+
begin
|
|
488
|
+
yield
|
|
489
|
+
ensure
|
|
490
|
+
saved.each { |k, v| v.nil? ? ENV.delete(k) : ENV[k] = v }
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Open a command's redirections and return [stdout_io, stderr_io] (nil for a
|
|
495
|
+
# stream that is not redirected). The non-block sibling of #with_redirections,
|
|
496
|
+
# used where the Driver needs the IO objects handed to it directly.
|
|
497
|
+
def redirection_ios(redirections, cwd)
|
|
498
|
+
out_io = err_io = nil
|
|
499
|
+
redirections.each do |r|
|
|
500
|
+
path = r.path == "/dev/null" ? File::NULL : absolute(r.path, cwd)
|
|
501
|
+
io = File.open(path, r.mode == :append ? "a" : "w")
|
|
502
|
+
r.stream == :stderr ? (err_io = io) : (out_io = io)
|
|
503
|
+
end
|
|
504
|
+
[out_io, err_io]
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def safe_puts(io, message)
|
|
508
|
+
io.puts(message)
|
|
509
|
+
rescue StandardError
|
|
510
|
+
nil
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# --- parallel scheduling (-j) ----------------------------------------
|
|
514
|
+
|
|
515
|
+
# Build +plan+ with up to @jobs step workers running at once. Each ready
|
|
516
|
+
# step (all its prerequisite steps finished) is forked; the worker runs the
|
|
517
|
+
# whole step in-process with its output captured, so a step's lines stay
|
|
518
|
+
# together and a compiler crash is contained. On a worker failure no new
|
|
519
|
+
# step is launched, the ones already running are drained, and then the
|
|
520
|
+
# failure is raised — make's default "-k off" behaviour.
|
|
521
|
+
def execute_parallel(plan)
|
|
522
|
+
remaining = plan.steps.dup
|
|
523
|
+
done = {}
|
|
524
|
+
running = {}
|
|
525
|
+
failure = nil
|
|
526
|
+
|
|
527
|
+
loop do
|
|
528
|
+
while failure.nil? && running.size < @jobs && (step = next_ready(remaining, done, running))
|
|
529
|
+
remaining.delete(step)
|
|
530
|
+
launch_step(step, running)
|
|
531
|
+
end
|
|
532
|
+
break if running.empty?
|
|
533
|
+
|
|
534
|
+
pid, status = Process.wait2
|
|
535
|
+
finished = running.delete(pid)
|
|
536
|
+
next unless finished
|
|
537
|
+
|
|
538
|
+
@out.write(finished[:thread].value)
|
|
539
|
+
if status.success?
|
|
540
|
+
done[finished[:step].target] = true
|
|
541
|
+
else
|
|
542
|
+
failure ||= [finished[:step], status]
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
return unless failure
|
|
547
|
+
|
|
548
|
+
step, status = failure
|
|
549
|
+
raise CommandFailedError.new(target: step.target,
|
|
550
|
+
command: "recipe for #{step.target}",
|
|
551
|
+
reason: tool_reason(status))
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# The first remaining step all of whose prerequisite steps have finished and
|
|
555
|
+
# none of which is still running. nil when nothing is currently runnable
|
|
556
|
+
# (every remaining step waits on an in-flight one).
|
|
557
|
+
def next_ready(remaining, done, running)
|
|
558
|
+
pending = running.values.map { |r| r[:step].target }
|
|
559
|
+
remaining.find do |step|
|
|
560
|
+
step.prereqs.all? { |p| done[p] } && pending.none? { |t| step.prereqs.include?(t) }
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Fork a worker for +step+. The child funnels every stream (builtin echo,
|
|
565
|
+
# the Driver, any spawned helper) into a capture pipe by reopening its
|
|
566
|
+
# stdout/stderr, then runs the step's recipe lines with tool substitution
|
|
567
|
+
# inline (it is already isolated). The parent records the child's pid and a
|
|
568
|
+
# thread that drains the pipe so a large transcript cannot dead-lock the
|
|
569
|
+
# write.
|
|
570
|
+
def launch_step(step, running)
|
|
571
|
+
reader, writer = IO.pipe
|
|
572
|
+
pid = fork do
|
|
573
|
+
reader.close
|
|
574
|
+
run_step_worker(step, writer)
|
|
575
|
+
end
|
|
576
|
+
writer.close
|
|
577
|
+
running[pid] = { step: step, reader: reader, thread: Thread.new { reader.read } }
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def run_step_worker(step, writer)
|
|
581
|
+
$stdout.reopen(writer)
|
|
582
|
+
$stderr.reopen(writer)
|
|
583
|
+
@out = $stdout
|
|
584
|
+
@err = $stderr
|
|
585
|
+
@isolate_tool = false
|
|
586
|
+
step.commands.each { |command| run_line(step.target, command) }
|
|
587
|
+
writer.flush
|
|
588
|
+
exit!(0)
|
|
589
|
+
rescue RmakeError => e
|
|
590
|
+
safe_puts(writer, e.message)
|
|
591
|
+
exit!(1)
|
|
592
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
593
|
+
safe_puts(writer, "rmake: #{e.class}: #{e.message}")
|
|
594
|
+
exit!(1)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
# --- builtins --------------------------------------------------------
|
|
598
|
+
|
|
599
|
+
def run_builtin(target, name, argv, cmd, state, text)
|
|
600
|
+
with_redirections(cmd.redirections, state.cwd) do |out_io, err_io|
|
|
601
|
+
out = out_io || @out
|
|
602
|
+
err = err_io || @err
|
|
603
|
+
case name
|
|
604
|
+
when "cd" then builtin_cd(argv, state)
|
|
605
|
+
when "rm" then builtin_rm(argv, state)
|
|
606
|
+
when "mkdir" then builtin_mkdir(target, argv, state, text)
|
|
607
|
+
when "rmdir" then builtin_rmdir(argv, state)
|
|
608
|
+
when "cp" then builtin_cp(target, argv, state, text)
|
|
609
|
+
when "install" then builtin_install(target, argv, state, text)
|
|
610
|
+
when "echo" then builtin_echo(argv, out)
|
|
611
|
+
when "touch" then builtin_touch(argv, state)
|
|
612
|
+
when "true", ":", "exit" then true
|
|
613
|
+
else
|
|
614
|
+
# BUILTINS listed it but no branch handles it — a programming error.
|
|
615
|
+
unsupported!("builtin '#{name}'", target, text)
|
|
616
|
+
end
|
|
617
|
+
rescue RmakeError
|
|
618
|
+
# Unsupported-syntax and other rmake-level errors must propagate; only
|
|
619
|
+
# a utility's incidental I/O failure is turned into a soft failure.
|
|
620
|
+
raise
|
|
621
|
+
rescue StandardError => e
|
|
622
|
+
# Turn a utility's own I/O error into a recorded failure so the line
|
|
623
|
+
# reports it (and `-`/`||` can absorb it), rather than crashing rmake.
|
|
624
|
+
state.failure_reason = e.message
|
|
625
|
+
err.puts("rmake: #{name}: #{e.message}") if err
|
|
626
|
+
false
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# `cd DIR`: move the rest of this recipe line into DIR. A missing directory
|
|
631
|
+
# fails, so `cd x && cmd` skips cmd — the sh behaviour.
|
|
632
|
+
def builtin_cd(argv, state)
|
|
633
|
+
dir = argv[1]
|
|
634
|
+
return false if dir.nil?
|
|
635
|
+
|
|
636
|
+
target = absolute(dir, state.cwd)
|
|
637
|
+
return false unless File.directory?(target)
|
|
638
|
+
|
|
639
|
+
state.cwd = target
|
|
640
|
+
true
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# `rm [-f] [-r] FILE...`. `-f` swallows missing files; `-r`/`-R` recurses.
|
|
644
|
+
def builtin_rm(argv, state)
|
|
645
|
+
force = recursive = false
|
|
646
|
+
files = []
|
|
647
|
+
argv.drop(1).each do |a|
|
|
648
|
+
if a.start_with?("-") && a != "-"
|
|
649
|
+
force ||= a.include?("f")
|
|
650
|
+
recursive ||= a.include?("r") || a.include?("R")
|
|
651
|
+
else
|
|
652
|
+
files << a
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
ok = true
|
|
657
|
+
files.each do |f|
|
|
658
|
+
path = absolute(f, state.cwd)
|
|
659
|
+
if File.exist?(path) || File.symlink?(path)
|
|
660
|
+
recursive ? FileUtils.rm_rf(path) : FileUtils.rm(path)
|
|
661
|
+
elsif !force
|
|
662
|
+
state.failure_reason = "no such file: #{f}"
|
|
663
|
+
ok = false
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
ok
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# `mkdir [-p] DIR...`. Without -p a pre-existing directory is an error, as
|
|
670
|
+
# is a missing parent; the corpus always passes -p.
|
|
671
|
+
def builtin_mkdir(target, argv, state, text)
|
|
672
|
+
create_parents = false
|
|
673
|
+
dirs = []
|
|
674
|
+
argv.drop(1).each do |a|
|
|
675
|
+
if a == "-p" || a == "--parents"
|
|
676
|
+
create_parents = true
|
|
677
|
+
elsif a.start_with?("-")
|
|
678
|
+
unsupported!("mkdir option #{a}", target, text)
|
|
679
|
+
else
|
|
680
|
+
dirs << a
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
dirs.each do |d|
|
|
685
|
+
path = absolute(d, state.cwd)
|
|
686
|
+
create_parents ? FileUtils.mkdir_p(path) : FileUtils.mkdir(path)
|
|
687
|
+
end
|
|
688
|
+
true
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# `rmdir [--ignore-fail-on-non-empty] [-p] DIR...`. Removes empty
|
|
692
|
+
# directories; -p also removes now-empty ancestors. A non-empty directory
|
|
693
|
+
# fails unless the ignore flag is set. No operands is a no-op success.
|
|
694
|
+
def builtin_rmdir(argv, state)
|
|
695
|
+
ignore_nonempty = remove_parents = false
|
|
696
|
+
dirs = []
|
|
697
|
+
argv.drop(1).each do |a|
|
|
698
|
+
case a
|
|
699
|
+
when "--ignore-fail-on-non-empty" then ignore_nonempty = true
|
|
700
|
+
when "-p", "--parents" then remove_parents = true
|
|
701
|
+
else dirs << a unless a.start_with?("-")
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
ok = true
|
|
706
|
+
dirs.each do |d|
|
|
707
|
+
ok = false unless remove_one_dir(absolute(d, state.cwd), remove_parents, ignore_nonempty, state)
|
|
708
|
+
end
|
|
709
|
+
ok
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
def remove_one_dir(path, remove_parents, ignore_nonempty, state)
|
|
713
|
+
loop do
|
|
714
|
+
return true unless File.directory?(path)
|
|
715
|
+
|
|
716
|
+
unless (Dir.children(path).empty?)
|
|
717
|
+
return true if ignore_nonempty
|
|
718
|
+
|
|
719
|
+
state.failure_reason = "directory not empty: #{path}"
|
|
720
|
+
return false
|
|
721
|
+
end
|
|
722
|
+
Dir.rmdir(path)
|
|
723
|
+
break unless remove_parents
|
|
724
|
+
|
|
725
|
+
path = File.dirname(path)
|
|
726
|
+
end
|
|
727
|
+
true
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# `cp [-p] SRC... DEST`. When DEST is a directory each SRC is copied into
|
|
731
|
+
# it; otherwise a single SRC is copied to DEST.
|
|
732
|
+
def builtin_cp(target, argv, state, text)
|
|
733
|
+
_flags, operands = split_flags(argv.drop(1))
|
|
734
|
+
unsupported!("cp needs a source and destination", target, text) if operands.length < 2
|
|
735
|
+
|
|
736
|
+
dest = absolute(operands.pop, state.cwd)
|
|
737
|
+
operands.each do |src|
|
|
738
|
+
FileUtils.cp(absolute(src, state.cwd), dest)
|
|
739
|
+
end
|
|
740
|
+
true
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# `install [-c] [-m MODE] SRC... DEST` — mkmf's $(INSTALL_PROG)/
|
|
744
|
+
# $(INSTALL_DATA). `-c` (copy) is the default here; `-m` sets the octal
|
|
745
|
+
# mode. DEST is treated as a directory when it exists as one.
|
|
746
|
+
def builtin_install(target, argv, state, text)
|
|
747
|
+
mode = nil
|
|
748
|
+
operands = []
|
|
749
|
+
rest = argv.drop(1)
|
|
750
|
+
i = 0
|
|
751
|
+
while i < rest.length
|
|
752
|
+
a = rest[i]
|
|
753
|
+
if a == "-m"
|
|
754
|
+
mode = rest[i + 1]
|
|
755
|
+
i += 2
|
|
756
|
+
elsif a == "-c"
|
|
757
|
+
i += 1
|
|
758
|
+
elsif a.start_with?("-") && a != "-"
|
|
759
|
+
unsupported!("install option #{a}", target, text)
|
|
760
|
+
else
|
|
761
|
+
operands << a
|
|
762
|
+
i += 1
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
unsupported!("install needs a source and destination", target, text) if operands.length < 2
|
|
766
|
+
|
|
767
|
+
dest = absolute(operands.pop, state.cwd)
|
|
768
|
+
operands.each do |src|
|
|
769
|
+
spath = absolute(src, state.cwd)
|
|
770
|
+
dpath = File.directory?(dest) ? File.join(dest, File.basename(src)) : dest
|
|
771
|
+
FileUtils.cp(spath, dpath)
|
|
772
|
+
File.chmod(mode.to_i(8), dpath) if mode
|
|
773
|
+
end
|
|
774
|
+
true
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# `echo [-n] WORDS...` — join the arguments with single spaces. Writes to
|
|
778
|
+
# the redirected stream when one is present, else the runner's output.
|
|
779
|
+
def builtin_echo(argv, out)
|
|
780
|
+
args = argv.drop(1)
|
|
781
|
+
newline = true
|
|
782
|
+
newline = false if args.first == "-n" && (args = args.drop(1))
|
|
783
|
+
out.write(args.join(" "))
|
|
784
|
+
out.write("\n") if newline
|
|
785
|
+
true
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# `touch FILE...` — create each file or update its timestamp.
|
|
789
|
+
def builtin_touch(argv, state)
|
|
790
|
+
argv.drop(1).reject { |a| a.start_with?("-") }.each do |f|
|
|
791
|
+
FileUtils.touch(absolute(f, state.cwd))
|
|
792
|
+
end
|
|
793
|
+
true
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# --- helpers ---------------------------------------------------------
|
|
797
|
+
|
|
798
|
+
# Partition argv-tail words into option flags and positional operands
|
|
799
|
+
# (used by the utilities that take no option arguments of their own).
|
|
800
|
+
def split_flags(words)
|
|
801
|
+
flags = []
|
|
802
|
+
operands = []
|
|
803
|
+
words.each do |w|
|
|
804
|
+
(w.start_with?("-") && w != "-" ? flags : operands) << w
|
|
805
|
+
end
|
|
806
|
+
[flags, operands]
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
def absolute(path, cwd)
|
|
810
|
+
path.start_with?("/") ? path : File.expand_path(path, cwd)
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def unsupported!(construct, target, command)
|
|
814
|
+
raise UnsupportedRecipeError.new(construct, target: target, command: command)
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
|
818
|
+
end
|