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,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Rubycc
|
|
7
|
+
module Link
|
|
8
|
+
# Turns the command line's library requests (`-l` names and `-L` search
|
|
9
|
+
# directories) into the two concrete input sets the final link consumes: the
|
|
10
|
+
# shared objects whose exports satisfy imports (SharedLinker's `needed:`) and
|
|
11
|
+
# the relocatable inputs pulled in for their definitions (SharedLinker's
|
|
12
|
+
# `inputs`, where archives are taken lazily). It is the piece that stands
|
|
13
|
+
# between a driver's flags and the linker core: nothing here reads a compiler
|
|
14
|
+
# flag string — the caller has already split `-lz` into the name `"z"` and
|
|
15
|
+
# `-L/opt/lib` into a directory — so this class only does the resolution the
|
|
16
|
+
# ELF/GNU conventions prescribe.
|
|
17
|
+
#
|
|
18
|
+
# Search follows the classic rule: each `-L` directory in command-line order,
|
|
19
|
+
# then the platform's default library directories (only those that exist), and
|
|
20
|
+
# within a single directory an unversioned `lib<name>.so`, a versioned
|
|
21
|
+
# `lib<name>.so.<version>`, and then `lib<name>.a` are preferred in that order.
|
|
22
|
+
# The first directory holding either form settles the request — the `.so`/`.a`
|
|
23
|
+
# preference does not reach across directories, so an earlier directory's `.a`
|
|
24
|
+
# wins over a later directory's `.so`, matching the traditional linker. A
|
|
25
|
+
# `:filename` request (`-l:libfoo.so.1`) looks that exact name up instead of
|
|
26
|
+
# composing `lib…so`/`lib…a`.
|
|
27
|
+
#
|
|
28
|
+
# A resolved file is dispatched by what it actually is, read from its leading
|
|
29
|
+
# bytes: an ELF shared object (ET_DYN) joins `needed`, an ELF relocatable or an
|
|
30
|
+
# `ar` archive joins `inputs`, and anything else is treated as a GNU-ld text
|
|
31
|
+
# linker script (glibc ships `libc.so` as `GROUP ( libc.so.6 … )`) whose
|
|
32
|
+
# GROUP/INPUT file lists are expanded and each named file resolved in turn.
|
|
33
|
+
#
|
|
34
|
+
# The transitive closure of shared dependencies is deliberately *not* followed:
|
|
35
|
+
# a `.so` that itself needs other `.so`s records those as its own DT_NEEDED and
|
|
36
|
+
# the runtime loader pulls them at load time; a static link only needs the
|
|
37
|
+
# directly requested libraries. Deduplication is by real path so the same
|
|
38
|
+
# library requested twice (or reached once directly and once through a script)
|
|
39
|
+
# contributes a single entry, keeping the output deterministic (N4).
|
|
40
|
+
class LibraryResolver
|
|
41
|
+
# ELF and `ar` magics, and the e_type value that marks a shared object; a
|
|
42
|
+
# resolved file matching neither magic is taken to be a text linker script.
|
|
43
|
+
ELFMAG = "\x7FELF".b
|
|
44
|
+
AR_MAGIC = "!<arch>\n".b
|
|
45
|
+
ET_DYN = 3
|
|
46
|
+
|
|
47
|
+
# The target's default library directories, consulted after every `-L`
|
|
48
|
+
# directory. Debian's multiarch directory is target-specific; using the
|
|
49
|
+
# x86_64 spelling unconditionally made a native aarch64 build unable to
|
|
50
|
+
# resolve even libc's ordinary `-lm`/`-lpthread` dependencies.
|
|
51
|
+
TARGET_SYSTEM_DIRS = {
|
|
52
|
+
"x86_64" => [
|
|
53
|
+
"/usr/lib/x86_64-linux-gnu",
|
|
54
|
+
"/usr/lib",
|
|
55
|
+
"/lib/x86_64-linux-gnu",
|
|
56
|
+
"/lib",
|
|
57
|
+
"/usr/local/lib"
|
|
58
|
+
].freeze,
|
|
59
|
+
"aarch64" => [
|
|
60
|
+
"/usr/lib/aarch64-linux-gnu",
|
|
61
|
+
"/usr/lib",
|
|
62
|
+
"/lib/aarch64-linux-gnu",
|
|
63
|
+
"/lib",
|
|
64
|
+
"/usr/aarch64-linux-gnu/lib",
|
|
65
|
+
"/usr/local/lib"
|
|
66
|
+
].freeze
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
# musl folds the libraries that glibc normally exposes as separate DSOs into
|
|
70
|
+
# libc. Runtime images therefore have no libm.so, libpthread.so, or libdl.so
|
|
71
|
+
# (and often no unversioned libc.so either), even though extconf-generated
|
|
72
|
+
# link flags still request them. Only these known libc-provided names are
|
|
73
|
+
# eligible for the fallback; an absent arbitrary `-lfoo` must remain an
|
|
74
|
+
# error rather than silently depending on libc.
|
|
75
|
+
MUSL_LIBC_PROVIDED_LIBRARIES = %w[c m pthread rt dl resolv util crypt nsl].freeze
|
|
76
|
+
|
|
77
|
+
# Alpine/musl runtime names. The loader is also the libc implementation, so
|
|
78
|
+
# it is a valid last-resort target when a libc.musl-* compatibility symlink
|
|
79
|
+
# is not present in a stripped runtime.
|
|
80
|
+
MUSL_LIBC_FILE = /\Alibc\.musl(?:-[^.]*)?\.so\.\d[\w.-]*\z/
|
|
81
|
+
MUSL_LOADER_FILE = /\Ald-musl-[^.]+\.so\.\d[\w.-]*\z/
|
|
82
|
+
VERSIONED_SHARED_SUFFIX = /\A\d[\w.-]*\z/
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
# Return the conventional system library directories for +target+.
|
|
86
|
+
# A target triple is accepted because the driver normalizes one before
|
|
87
|
+
# passing it here. Unknown targets fall back to the host layout so that
|
|
88
|
+
# the resolver remains useful for diagnostics instead of inventing a
|
|
89
|
+
# multiarch path for an unsupported architecture.
|
|
90
|
+
def default_system_dirs(target: nil)
|
|
91
|
+
arch = normalize_target(target || RbConfig::CONFIG["host_cpu"])
|
|
92
|
+
TARGET_SYSTEM_DIRS.fetch(arch) do
|
|
93
|
+
TARGET_SYSTEM_DIRS.fetch(normalize_target(RbConfig::CONFIG["host_cpu"])) do
|
|
94
|
+
TARGET_SYSTEM_DIRS.fetch("x86_64")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def normalize_target(spec)
|
|
100
|
+
cpu = spec.to_s.split("-", 2).first.to_s
|
|
101
|
+
case cpu
|
|
102
|
+
when "amd64", "x64" then "x86_64"
|
|
103
|
+
when "arm64" then "aarch64"
|
|
104
|
+
else cpu
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Resolves `libraries` (an ordered array of `-l` argument strings, each the
|
|
109
|
+
# text after `-l`, e.g. `"z"` or `":libfoo.so.1"`) against `search_dirs`
|
|
110
|
+
# (the `-L` directories, in order) and returns a Resolution.
|
|
111
|
+
def resolve(libraries, search_dirs: [], target: nil)
|
|
112
|
+
new(search_dirs: search_dirs, target: target).resolve(libraries)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# High-level convenience for a driver: resolves `libraries` and links them
|
|
116
|
+
# together with the object `inputs` into a shared object. The resolved
|
|
117
|
+
# archives follow the objects so the lazy pull-in sees the objects'
|
|
118
|
+
# undefined symbols first; the resolved shared objects become the
|
|
119
|
+
# dependencies imports bind against.
|
|
120
|
+
def link(inputs, libraries: [], search_dirs: [], soname: nil, target: nil)
|
|
121
|
+
r = resolve(libraries, search_dirs: search_dirs, target: target)
|
|
122
|
+
SharedLinker.link(inputs + r.inputs, needed: r.needed, soname: soname)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Convenience: link and write the shared object to `path`.
|
|
126
|
+
def link_to(inputs, path, libraries: [], search_dirs: [], soname: nil, target: nil)
|
|
127
|
+
File.binwrite(path, link(inputs, libraries: libraries, search_dirs: search_dirs,
|
|
128
|
+
soname: soname, target: target))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Kept as a public host-layout constant for callers and tests that need to
|
|
133
|
+
# inspect the default search path without selecting a cross target.
|
|
134
|
+
DEFAULT_SYSTEM_DIRS = default_system_dirs.freeze
|
|
135
|
+
|
|
136
|
+
# The outcome of resolving a set of library requests: `needed` are the shared
|
|
137
|
+
# object paths to bind imports against, `inputs` the relocatable inputs
|
|
138
|
+
# (archives and objects) to feed the merge, both in first-seen order.
|
|
139
|
+
Resolution = Struct.new(:needed, :inputs, keyword_init: true)
|
|
140
|
+
|
|
141
|
+
def initialize(search_dirs: [], target: nil)
|
|
142
|
+
@dirs = (search_dirs + self.class.default_system_dirs(target: target)).select do |d|
|
|
143
|
+
File.directory?(d)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The ordered search path actually consulted (existing `-L` dirs followed by
|
|
148
|
+
# the existing default directories); exposed so a driver can report it.
|
|
149
|
+
attr_reader :dirs
|
|
150
|
+
|
|
151
|
+
def resolve(libraries)
|
|
152
|
+
@needed = []
|
|
153
|
+
@inputs = []
|
|
154
|
+
@seen = Set.new
|
|
155
|
+
libraries.each { |spec| resolve_spec(spec) }
|
|
156
|
+
Resolution.new(needed: @needed, inputs: @inputs)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# Resolves one `-l` request to a file and ingests it. `spec` is the text
|
|
162
|
+
# after `-l`: a leading colon selects an exact filename, otherwise the name
|
|
163
|
+
# is composed into `lib<name>.so` / `lib<name>.a`.
|
|
164
|
+
def resolve_spec(spec)
|
|
165
|
+
ingest(find_library(spec))
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Locates the file for a `-l` request. For a plain name each directory is
|
|
169
|
+
# tried in order, preferring an unversioned shared object, then a versioned
|
|
170
|
+
# shared object, then an archive within that directory; the first directory
|
|
171
|
+
# carrying any form wins. On musl, the small set of libraries folded into
|
|
172
|
+
# libc falls back to the detected musl libc image after the ordinary search.
|
|
173
|
+
# A `:filename` request looks the given name up verbatim. An unsatisfiable
|
|
174
|
+
# request is a hard error, named the way a driver reports a missing library.
|
|
175
|
+
def find_library(spec)
|
|
176
|
+
if spec.start_with?(":")
|
|
177
|
+
exact = spec[1..]
|
|
178
|
+
dir = @dirs.find { |d| File.file?(File.join(d, exact)) }
|
|
179
|
+
return File.join(dir, exact) if dir
|
|
180
|
+
else
|
|
181
|
+
path = find_plain_library(spec)
|
|
182
|
+
return path if path
|
|
183
|
+
|
|
184
|
+
if MUSL_LIBC_PROVIDED_LIBRARIES.include?(spec)
|
|
185
|
+
libc = find_musl_libc
|
|
186
|
+
return libc if libc
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
raise LinkError, "cannot find -l#{spec}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Searches the normal shared/archive forms for a plain `-l` request. The
|
|
193
|
+
# versioned form matters in runtime-only images, where package managers keep
|
|
194
|
+
# `libz.so.1` but omit the development symlink `libz.so`.
|
|
195
|
+
def find_plain_library(spec)
|
|
196
|
+
@dirs.each do |d|
|
|
197
|
+
shared = File.join(d, "lib#{spec}.so")
|
|
198
|
+
return shared if File.file?(shared)
|
|
199
|
+
|
|
200
|
+
versioned = find_versioned_shared(d, spec)
|
|
201
|
+
return versioned if versioned
|
|
202
|
+
|
|
203
|
+
archive = File.join(d, "lib#{spec}.a")
|
|
204
|
+
return archive if File.file?(archive)
|
|
205
|
+
end
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Finds a versioned shared object without manufacturing or requiring the
|
|
210
|
+
# unversioned development symlink. Symlinks carrying the ABI/SONAME name
|
|
211
|
+
# are preferred over their fully-versioned targets when both are present;
|
|
212
|
+
# otherwise the highest numeric version is selected deterministically.
|
|
213
|
+
def find_versioned_shared(dir, spec)
|
|
214
|
+
prefix = "lib#{spec}.so."
|
|
215
|
+
candidates = directory_entries(dir).filter_map do |name|
|
|
216
|
+
next unless name.start_with?(prefix)
|
|
217
|
+
suffix = name.delete_prefix(prefix)
|
|
218
|
+
next unless VERSIONED_SHARED_SUFFIX.match?(suffix)
|
|
219
|
+
|
|
220
|
+
path = File.join(dir, name)
|
|
221
|
+
path if File.file?(path)
|
|
222
|
+
end
|
|
223
|
+
choose_versioned(candidates)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Finds musl's combined libc image. Prefer the libc.musl-* name, which is
|
|
227
|
+
# the stable library spelling in Alpine runtime images, and use the loader
|
|
228
|
+
# name as a fallback because musl implements both entry points in one ELF.
|
|
229
|
+
# Searching the resolver's directories keeps this useful for a mounted
|
|
230
|
+
# runtime/sysroot rather than assuming only the host's /lib.
|
|
231
|
+
def find_musl_libc
|
|
232
|
+
@dirs.each do |dir|
|
|
233
|
+
candidate = choose_versioned(directory_entries(dir).filter_map do |name|
|
|
234
|
+
next unless MUSL_LIBC_FILE.match?(name)
|
|
235
|
+
|
|
236
|
+
path = File.join(dir, name)
|
|
237
|
+
path if File.file?(path)
|
|
238
|
+
end)
|
|
239
|
+
return candidate if candidate
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
@dirs.each do |dir|
|
|
243
|
+
candidate = choose_versioned(directory_entries(dir).filter_map do |name|
|
|
244
|
+
next unless MUSL_LOADER_FILE.match?(name)
|
|
245
|
+
|
|
246
|
+
path = File.join(dir, name)
|
|
247
|
+
path if File.file?(path)
|
|
248
|
+
end)
|
|
249
|
+
return candidate if candidate
|
|
250
|
+
end
|
|
251
|
+
nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def choose_versioned(candidates)
|
|
255
|
+
candidates.max_by do |path|
|
|
256
|
+
name = File.basename(path)
|
|
257
|
+
version = name.split(".so.", 2).last.to_s.split(".").map(&:to_i)
|
|
258
|
+
[File.symlink?(path) ? 1 : 0, version, name]
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def directory_entries(dir)
|
|
263
|
+
Dir.children(dir)
|
|
264
|
+
rescue SystemCallError
|
|
265
|
+
[]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Classifies a resolved file and routes it: a shared object to `needed`, a
|
|
269
|
+
# relocatable object or archive to `inputs`, a linker script to expansion.
|
|
270
|
+
# Files are deduplicated by real path so a library reached more than once
|
|
271
|
+
# (directly and through a script, say) is recorded a single time.
|
|
272
|
+
def ingest(path)
|
|
273
|
+
return unless @seen.add?(dedup_key(path))
|
|
274
|
+
|
|
275
|
+
case classify(path)
|
|
276
|
+
when :shared then @needed << path
|
|
277
|
+
when :object, :archive then @inputs << path
|
|
278
|
+
when :script then expand_script(path)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# A stable identity for deduplication: the canonical real path when it can be
|
|
283
|
+
# resolved, else the path as given (a broken symlink still keys on itself).
|
|
284
|
+
def dedup_key(path)
|
|
285
|
+
File.realpath(path)
|
|
286
|
+
rescue SystemCallError
|
|
287
|
+
path
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Reads a file's leading bytes and names its kind. The 18 bytes cover the ELF
|
|
291
|
+
# magic and e_type; a shorter file cannot be an ELF object. A file matching
|
|
292
|
+
# neither the ELF nor the `ar` magic is assumed to be a text linker script.
|
|
293
|
+
def classify(path)
|
|
294
|
+
head = File.binread(path, 18).to_s.b
|
|
295
|
+
if head.start_with?(ELFMAG)
|
|
296
|
+
head.byteslice(16, 2).to_s.unpack1("S<") == ET_DYN ? :shared : :object
|
|
297
|
+
elsif head.start_with?(AR_MAGIC)
|
|
298
|
+
:archive
|
|
299
|
+
else
|
|
300
|
+
:script
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Parses a GNU-ld text linker script for its GROUP/INPUT file lists and
|
|
305
|
+
# resolves each named entry, in order, through the same machinery — so a
|
|
306
|
+
# `-l` token inside the script searches the path recursively and an absolute
|
|
307
|
+
# path is ingested directly.
|
|
308
|
+
def expand_script(path)
|
|
309
|
+
LinkerScript.parse(File.read(path)).each { |token| resolve_token(token) }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Resolves one token from a linker script's file list. A `-l` token is a
|
|
313
|
+
# nested library request; any other token is a file path — used verbatim
|
|
314
|
+
# when it exists, otherwise looked up by name across the search directories.
|
|
315
|
+
def resolve_token(token)
|
|
316
|
+
return resolve_spec(token.sub(/\A-l/, "")) if token.start_with?("-l")
|
|
317
|
+
|
|
318
|
+
path =
|
|
319
|
+
if File.file?(token)
|
|
320
|
+
token
|
|
321
|
+
else
|
|
322
|
+
dir = @dirs.find { |d| File.file?(File.join(d, token)) }
|
|
323
|
+
dir && File.join(dir, token)
|
|
324
|
+
end
|
|
325
|
+
raise LinkError, "cannot find '#{token}' referenced by a linker script" unless path
|
|
326
|
+
|
|
327
|
+
ingest(path)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# A deliberately small reader for the sliver of GNU-ld linker-script syntax
|
|
332
|
+
# that a library search actually meets: the file-list commands GROUP and INPUT
|
|
333
|
+
# (with AS_NEEDED nested inside), and OUTPUT_FORMAT, which is skipped. glibc's
|
|
334
|
+
# `libc.so` is exactly such a script — `/* GNU ld script */ GROUP ( … )` — and
|
|
335
|
+
# this recovers the real files it points at.
|
|
336
|
+
#
|
|
337
|
+
# It is not a linker-script *language*: SECTIONS, PROVIDE, MEMORY, symbol
|
|
338
|
+
# assignments and the rest are not modelled. Only the recognized commands act;
|
|
339
|
+
# every other token outside a file list is ignored, so an unfamiliar directive
|
|
340
|
+
# passes by without derailing the scan rather than being half-interpreted.
|
|
341
|
+
# AS_NEEDED contents are treated as ordinary inputs here — the "only if used"
|
|
342
|
+
# trimming is the DT_NEEDED as-needed logic SharedLinker already applies.
|
|
343
|
+
class LinkerScript
|
|
344
|
+
# The commands that introduce a parenthesized list of input files. AS_NEEDED
|
|
345
|
+
# appears nested within GROUP/INPUT but is itself just another such list.
|
|
346
|
+
FILE_LIST_COMMANDS = %w[GROUP INPUT AS_NEEDED].freeze
|
|
347
|
+
|
|
348
|
+
class << self
|
|
349
|
+
# Parses `text` and returns the ordered file tokens its GROUP/INPUT lists
|
|
350
|
+
# name (absolute paths or `-l` requests), for the caller to resolve.
|
|
351
|
+
def parse(text)
|
|
352
|
+
new(text).parse
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def initialize(text)
|
|
357
|
+
@tokens = tokenize(strip_comments(text))
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def parse
|
|
361
|
+
files = []
|
|
362
|
+
i = 0
|
|
363
|
+
while i < @tokens.length
|
|
364
|
+
token = @tokens[i]
|
|
365
|
+
if FILE_LIST_COMMANDS.include?(token) && @tokens[i + 1] == "("
|
|
366
|
+
i = collect(i + 1, files)
|
|
367
|
+
elsif token == "OUTPUT_FORMAT" && @tokens[i + 1] == "("
|
|
368
|
+
i = skip_parens(i + 1)
|
|
369
|
+
else
|
|
370
|
+
i += 1
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
files
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
# Collects a parenthesized file list starting at `open` (the index of its
|
|
379
|
+
# `(`), appending each file token to `out` and recursing into nested
|
|
380
|
+
# GROUP/INPUT/AS_NEEDED lists so their files land in the same order. Commas,
|
|
381
|
+
# which GNU-ld allows between file names, are separators and dropped. Returns
|
|
382
|
+
# the index just past the matching `)`.
|
|
383
|
+
def collect(open, out)
|
|
384
|
+
i = open + 1
|
|
385
|
+
loop do
|
|
386
|
+
token = @tokens[i]
|
|
387
|
+
raise LinkError, "unterminated file list in linker script" if token.nil?
|
|
388
|
+
|
|
389
|
+
case token
|
|
390
|
+
when ")" then return i + 1
|
|
391
|
+
when "," then i += 1
|
|
392
|
+
when *FILE_LIST_COMMANDS
|
|
393
|
+
raise LinkError, "expected '(' after #{token} in linker script" unless @tokens[i + 1] == "("
|
|
394
|
+
|
|
395
|
+
i = collect(i + 1, out)
|
|
396
|
+
when "OUTPUT_FORMAT"
|
|
397
|
+
i = @tokens[i + 1] == "(" ? skip_parens(i + 1) : i + 1
|
|
398
|
+
else
|
|
399
|
+
out << token
|
|
400
|
+
i += 1
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Skips a balanced parenthesized group starting at `open` (its `(`), used to
|
|
406
|
+
# discard an OUTPUT_FORMAT's contents. Returns the index past the matching
|
|
407
|
+
# `)`.
|
|
408
|
+
def skip_parens(open)
|
|
409
|
+
depth = 0
|
|
410
|
+
i = open
|
|
411
|
+
loop do
|
|
412
|
+
token = @tokens[i]
|
|
413
|
+
raise LinkError, "unterminated group in linker script" if token.nil?
|
|
414
|
+
|
|
415
|
+
depth += 1 if token == "("
|
|
416
|
+
depth -= 1 if token == ")"
|
|
417
|
+
return i + 1 if depth.zero?
|
|
418
|
+
|
|
419
|
+
i += 1
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Removes `/* … */` comments (the only comment form the scripts use),
|
|
424
|
+
# replacing each with a space so it cannot glue two tokens together.
|
|
425
|
+
def strip_comments(text)
|
|
426
|
+
text.gsub(%r{/\*.*?\*/}m, " ")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Splits the script into words, first padding the structural punctuation
|
|
430
|
+
# (parentheses and commas) with spaces so `GROUP(a,b)` tokenizes the same as
|
|
431
|
+
# `GROUP ( a , b )`.
|
|
432
|
+
def tokenize(text)
|
|
433
|
+
text.gsub(/([(),])/, ' \1 ').split
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|