rubycc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +52 -0
  5. data/README.md +208 -0
  6. data/data/README.md +117 -0
  7. data/data/r10_corpus_scan.json +4082 -0
  8. data/data/r10_manual_classification.json +5342 -0
  9. data/data/r10_verification_m4a.json +1531 -0
  10. data/data/r10_verification_m4b.json +1818 -0
  11. data/data/r10_verification_m4c.json +1489 -0
  12. data/data/r10_verification_m4d.json +318 -0
  13. data/data/r10_verification_rbs.json +192 -0
  14. data/data/verified_gems.json +397 -0
  15. data/exe/rmake +16 -0
  16. data/exe/rubycc +9 -0
  17. data/exe/rubycc-ar +114 -0
  18. data/exe/rubycc-doctor +14 -0
  19. data/exe/rubycc-pkgconf +8 -0
  20. data/include/float.h +87 -0
  21. data/include/iso646.h +18 -0
  22. data/include/libc/alloca.h +18 -0
  23. data/include/libc/arpa/inet.h +61 -0
  24. data/include/libc/assert.h +43 -0
  25. data/include/libc/dirent.h +72 -0
  26. data/include/libc/dlfcn.h +58 -0
  27. data/include/libc/features.h +205 -0
  28. data/include/libc/glibc/aarch64/ctype.h +108 -0
  29. data/include/libc/glibc/aarch64/endian.h +54 -0
  30. data/include/libc/glibc/aarch64/errno.h +154 -0
  31. data/include/libc/glibc/aarch64/fcntl.h +144 -0
  32. data/include/libc/glibc/aarch64/inttypes.h +182 -0
  33. data/include/libc/glibc/aarch64/limits.h +63 -0
  34. data/include/libc/glibc/aarch64/pthread.h +131 -0
  35. data/include/libc/glibc/aarch64/setjmp.h +84 -0
  36. data/include/libc/glibc/aarch64/stdint.h +174 -0
  37. data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
  38. data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
  39. data/include/libc/glibc/aarch64/sys/select.h +75 -0
  40. data/include/libc/glibc/aarch64/sys/stat.h +136 -0
  41. data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
  42. data/include/libc/glibc/aarch64/sys/time.h +74 -0
  43. data/include/libc/glibc/aarch64/sys/types.h +127 -0
  44. data/include/libc/glibc/aarch64/time.h +113 -0
  45. data/include/libc/glibc/x86_64/ctype.h +108 -0
  46. data/include/libc/glibc/x86_64/endian.h +54 -0
  47. data/include/libc/glibc/x86_64/errno.h +154 -0
  48. data/include/libc/glibc/x86_64/fcntl.h +134 -0
  49. data/include/libc/glibc/x86_64/inttypes.h +182 -0
  50. data/include/libc/glibc/x86_64/limits.h +61 -0
  51. data/include/libc/glibc/x86_64/pthread.h +111 -0
  52. data/include/libc/glibc/x86_64/setjmp.h +82 -0
  53. data/include/libc/glibc/x86_64/stdint.h +163 -0
  54. data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
  55. data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
  56. data/include/libc/glibc/x86_64/sys/select.h +75 -0
  57. data/include/libc/glibc/x86_64/sys/stat.h +130 -0
  58. data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
  59. data/include/libc/glibc/x86_64/sys/time.h +74 -0
  60. data/include/libc/glibc/x86_64/sys/types.h +123 -0
  61. data/include/libc/glibc/x86_64/time.h +113 -0
  62. data/include/libc/grp.h +50 -0
  63. data/include/libc/langinfo.h +126 -0
  64. data/include/libc/link.h +18 -0
  65. data/include/libc/locale.h +83 -0
  66. data/include/libc/math.h +191 -0
  67. data/include/libc/netinet/in.h +126 -0
  68. data/include/libc/netinet/tcp.h +44 -0
  69. data/include/libc/poll.h +46 -0
  70. data/include/libc/pwd.h +58 -0
  71. data/include/libc/regex.h +53 -0
  72. data/include/libc/sched.h +35 -0
  73. data/include/libc/signal.h +204 -0
  74. data/include/libc/stdio.h +157 -0
  75. data/include/libc/stdlib.h +92 -0
  76. data/include/libc/string.h +74 -0
  77. data/include/libc/strings.h +25 -0
  78. data/include/libc/sys/cdefs.h +126 -0
  79. data/include/libc/sys/inotify.h +111 -0
  80. data/include/libc/sys/ioctl.h +36 -0
  81. data/include/libc/sys/mman.h +65 -0
  82. data/include/libc/sys/param.h +41 -0
  83. data/include/libc/sys/resource.h +109 -0
  84. data/include/libc/sys/socket.h +167 -0
  85. data/include/libc/sys/statfs.h +82 -0
  86. data/include/libc/sys/timerfd.h +55 -0
  87. data/include/libc/sys/uio.h +40 -0
  88. data/include/libc/sys/un.h +25 -0
  89. data/include/libc/sys/utsname.h +35 -0
  90. data/include/libc/sys/wait.h +135 -0
  91. data/include/libc/termios.h +179 -0
  92. data/include/libc/unistd.h +194 -0
  93. data/include/stdalign.h +16 -0
  94. data/include/stdarg.h +31 -0
  95. data/include/stdatomic.h +158 -0
  96. data/include/stdbool.h +15 -0
  97. data/include/stdckdint.h +28 -0
  98. data/include/stddef.h +60 -0
  99. data/include/stdnoreturn.h +18 -0
  100. data/include/x86intrin.h +16 -0
  101. data/lib/rubycc/backend/aarch64.rb +1724 -0
  102. data/lib/rubycc/backend/x86_64.rb +1369 -0
  103. data/lib/rubycc/compile_error.rb +36 -0
  104. data/lib/rubycc/compiler.rb +305 -0
  105. data/lib/rubycc/doctor/builder.rb +151 -0
  106. data/lib/rubycc/doctor/cli.rb +190 -0
  107. data/lib/rubycc/doctor/fetcher.rb +84 -0
  108. data/lib/rubycc/doctor/gemfile.rb +115 -0
  109. data/lib/rubycc/doctor/verified_gems.rb +114 -0
  110. data/lib/rubycc/doctor.rb +16 -0
  111. data/lib/rubycc/driver.rb +463 -0
  112. data/lib/rubycc/front/ast.rb +528 -0
  113. data/lib/rubycc/front/constant_evaluator.rb +631 -0
  114. data/lib/rubycc/front/initializer_resolver.rb +592 -0
  115. data/lib/rubycc/front/lexeme_reader.rb +460 -0
  116. data/lib/rubycc/front/lexer.rb +232 -0
  117. data/lib/rubycc/front/parser.rb +4122 -0
  118. data/lib/rubycc/front/token.rb +49 -0
  119. data/lib/rubycc/ir/call_convention.rb +486 -0
  120. data/lib/rubycc/ir/generator.rb +6036 -0
  121. data/lib/rubycc/ir/ir.rb +417 -0
  122. data/lib/rubycc/link/compat_runtime.rb +89 -0
  123. data/lib/rubycc/link/errors.rb +13 -0
  124. data/lib/rubycc/link/executable_linker.rb +405 -0
  125. data/lib/rubycc/link/library_resolver.rb +437 -0
  126. data/lib/rubycc/link/partial_linker.rb +546 -0
  127. data/lib/rubycc/link/shared_linker.rb +1732 -0
  128. data/lib/rubycc/mkmf_shim.rb +83 -0
  129. data/lib/rubycc/objfile/ar_archive.rb +437 -0
  130. data/lib/rubycc/objfile/elf_reader.rb +646 -0
  131. data/lib/rubycc/objfile/elf_writer.rb +891 -0
  132. data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
  133. data/lib/rubycc/pkgconf/cli.rb +139 -0
  134. data/lib/rubycc/pkgconf/errors.rb +39 -0
  135. data/lib/rubycc/pkgconf/model.rb +28 -0
  136. data/lib/rubycc/pkgconf/parser.rb +109 -0
  137. data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
  138. data/lib/rubycc/pkgconf/resolver.rb +70 -0
  139. data/lib/rubycc/pkgconf/search_path.rb +38 -0
  140. data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
  141. data/lib/rubycc/preprocess/constant_expression.rb +219 -0
  142. data/lib/rubycc/preprocess/glibc_version.rb +124 -0
  143. data/lib/rubycc/preprocess/pp_token.rb +62 -0
  144. data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
  145. data/lib/rubycc/preprocess/scanner.rb +290 -0
  146. data/lib/rubycc/preprocess/token_converter.rb +157 -0
  147. data/lib/rubycc/rmake/cli.rb +143 -0
  148. data/lib/rubycc/rmake/errors.rb +71 -0
  149. data/lib/rubycc/rmake/executor.rb +818 -0
  150. data/lib/rubycc/rmake/expander.rb +251 -0
  151. data/lib/rubycc/rmake/makefile.rb +352 -0
  152. data/lib/rubycc/rmake/model.rb +137 -0
  153. data/lib/rubycc/rmake/parser.rb +226 -0
  154. data/lib/rubycc/rmake/rmake.rb +15 -0
  155. data/lib/rubycc/type.rb +1236 -0
  156. data/lib/rubycc/version.rb +5 -0
  157. data/lib/rubycc.rb +36 -0
  158. data/lib/rubygems_plugin.rb +102 -0
  159. metadata +219 -0
@@ -0,0 +1,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