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,376 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module ObjFile
5
+ # Writes a *general* ELF64 relocatable object (ET_REL) for Linux x86_64:
6
+ # arbitrary named sections, an arbitrary symbol table, and full SHT_RELA
7
+ # relocation tables carrying any relocation type and addend.
8
+ #
9
+ # It is the counterpart the linker's `ld -r` core needs and deliberately
10
+ # NOT the compiler's ELFWriter. That writer serves a single translation
11
+ # unit through a fixed menu — .text/.rodata/.data/.bss and three baked-in
12
+ # relocation kinds — because a compiler always emits the same shape. A
13
+ # merged object is open-ended: it may hold any section the inputs carried
14
+ # (.comment, .note.*, .eh_frame, ...), section symbols pointing at merged
15
+ # sections, and relocations of any numeric type against any symbol with any
16
+ # addend. Rather than contort the compiler's writer, this one accepts those
17
+ # pieces directly and lays them out.
18
+ #
19
+ # The API is build-then-emit: #add_section, #add_symbol and #add_relocation
20
+ # return opaque handles (the value structs below) that later calls reference
21
+ # by identity, so the caller never computes on-disk indices itself. #to_binary
22
+ # fixes the section order (NULL, the content sections in insertion order, one
23
+ # .rela.<name> per relocated target, then .symtab/.strtab/.shstrtab), resolves
24
+ # every cross-reference (symbol st_shndx, rela sh_link/sh_info, r_info's symbol
25
+ # index) against that order, and concatenates the image.
26
+ #
27
+ # Two invariants the caller relies on: symbols are emitted with every
28
+ # STB_LOCAL before the first non-local (a stable partition of insertion
29
+ # order, so relocation handles stay valid) and sh_info of .symtab lands on
30
+ # the first global; output is fully deterministic (N4) — identical build
31
+ # calls yield byte-identical bytes, with no timestamps embedded.
32
+ class RelocatableWriter
33
+ ELFCLASS64 = 2
34
+ ELFDATA2LSB = 1
35
+ EV_CURRENT = 1
36
+ ET_REL = 1
37
+ EM_X86_64 = 62
38
+ EM_AARCH64 = 183
39
+
40
+ SHN_UNDEF = 0
41
+ SHN_ABS = 0xFFF1
42
+
43
+ SHT_SYMTAB = 2
44
+ SHT_STRTAB = 3
45
+ SHT_RELA = 4
46
+ SHT_NOBITS = 8
47
+
48
+ SHF_INFO_LINK = 0x40
49
+
50
+ # Symbolic bind/type/visibility accepted from the caller, mapped to their
51
+ # st_info / st_other encodings. A caller may also pass the raw integer,
52
+ # which passes through unchanged.
53
+ BINDINGS = { local: 0, global: 1, weak: 2 }.freeze
54
+ TYPES = { notype: 0, object: 1, func: 2, section: 3, file: 4, tls: 6, ifunc: 10 }.freeze
55
+ VISIBILITIES = { default: 0, internal: 1, hidden: 2, protected: 3 }.freeze
56
+
57
+ SYM_ENTSIZE = 24
58
+ RELA_ENTSIZE = 24
59
+ SHDR_ENTSIZE = 64
60
+ EHDR_SIZE = 64
61
+
62
+ # A section to emit. `data` holds the file bytes for a non-NOBITS section;
63
+ # a NOBITS section (.bss) carries `size` instead and occupies no file
64
+ # space. `index` is filled in during layout.
65
+ Section = Struct.new(
66
+ :name, :type, :flags, :addralign, :entsize, :data, :size, :index,
67
+ keyword_init: true
68
+ )
69
+
70
+ # A symbol to emit. `section` (a Section handle) names the section that
71
+ # defines it, in which case st_shndx is that section's index; otherwise
72
+ # `shndx` carries a reserved value (SHN_UNDEF / SHN_ABS). `index` is filled
73
+ # in during layout.
74
+ Symbol = Struct.new(
75
+ :name, :bind, :type, :visibility, :section, :shndx, :value, :size, :index,
76
+ keyword_init: true
77
+ )
78
+
79
+ # A relocation to emit into the .rela table of `target`. `symbol` is a
80
+ # Symbol handle; `type` is the numeric x86_64 relocation type.
81
+ Relocation = Struct.new(:target, :offset, :symbol, :type, :addend, keyword_init: true)
82
+
83
+ # `machine` is the ELF e_machine value stamped into the output header. It
84
+ # defaults to x86_64 so an existing caller is unaffected; the partial-link
85
+ # merge passes the inputs' own machine through so a merged aarch64 object
86
+ # keeps its EM_AARCH64 identity (the final linker reads it back to pick the
87
+ # target's relocation and crt logic).
88
+ def initialize(machine: EM_X86_64)
89
+ @machine = machine
90
+ @sections = []
91
+ # The reserved null symbol is index 0 by the ELF ABI; expose it so a
92
+ # caller can re-point a "no symbol" relocation at it.
93
+ @null_symbol = Symbol.new(name: nil, bind: 0, type: 0, visibility: 0,
94
+ section: nil, shndx: SHN_UNDEF, value: 0, size: 0)
95
+ @symbols = [@null_symbol]
96
+ @relocations = []
97
+ end
98
+
99
+ attr_reader :null_symbol
100
+
101
+ # Registers a content section. `type`/`flags`/`entsize` are raw ELF values;
102
+ # `data` (bytes) or, for a NOBITS section, `size` gives its extent. Returns
103
+ # the Section handle to reference from symbols and relocations.
104
+ def add_section(name:, type:, flags:, addralign:, entsize: 0, data: nil, size: nil)
105
+ section = Section.new(name: name, type: type, flags: flags,
106
+ addralign: [addralign, 1].max, entsize: entsize,
107
+ data: data&.b, size: size)
108
+ @sections << section
109
+ section
110
+ end
111
+
112
+ # Registers a symbol. `bind`/`type`/`visibility` may be the symbolic forms
113
+ # (:local, :func, :hidden, ...) or raw integers. Returns the Symbol handle.
114
+ def add_symbol(name:, bind:, type:, visibility: :default, section: nil, shndx: SHN_UNDEF, value: 0, size: 0)
115
+ symbol = Symbol.new(
116
+ name: name, bind: BINDINGS.fetch(bind, bind), type: TYPES.fetch(type, type),
117
+ visibility: VISIBILITIES.fetch(visibility, visibility),
118
+ section: section, shndx: shndx, value: value, size: size
119
+ )
120
+ @symbols << symbol
121
+ symbol
122
+ end
123
+
124
+ # Records a relocation to emit into `target`'s .rela table.
125
+ def add_relocation(target:, offset:, symbol:, type:, addend:)
126
+ @relocations << Relocation.new(target: target, offset: offset, symbol: symbol,
127
+ type: type, addend: addend)
128
+ self
129
+ end
130
+
131
+ # Assembles and returns the object as an ASCII-8BIT String.
132
+ def to_binary
133
+ order_symbols
134
+ layout = build_layout
135
+ assemble(layout)
136
+ end
137
+
138
+ def write(path)
139
+ File.binwrite(path, to_binary)
140
+ end
141
+
142
+ private
143
+
144
+ # Stable-partitions the symbol table so every STB_LOCAL precedes the first
145
+ # non-local, as the ABI requires, without disturbing relative order within
146
+ # each class. The null symbol (bind 0 = local) naturally stays first.
147
+ def order_symbols
148
+ locals, globals = @symbols.partition { |sym| sym.bind == BINDINGS[:local] }
149
+ @symbols = locals + globals
150
+ @first_global = locals.size
151
+ @symbols.each_with_index { |sym, i| sym.index = i }
152
+ end
153
+
154
+ # The relocations aimed at `section`, in insertion order — matched by
155
+ # object identity, since a Section struct hashes by value and its :index
156
+ # field is mutated during layout (a value-keyed lookup would then miss).
157
+ def relocations_for(section)
158
+ @relocations.select { |reloc| reloc.target.equal?(section) }
159
+ end
160
+
161
+ # A section descriptor used only during layout, carrying the payload bytes
162
+ # and the resolved sh_link/sh_info as concrete indices.
163
+ LaidOut = Struct.new(:name, :type, :flags, :addr, :size, :link, :info,
164
+ :addralign, :entsize, :data, :offset, keyword_init: true)
165
+
166
+ # Fixes the on-disk section order and assigns every section its index, then
167
+ # builds the symbol/relocation/string payloads against those indices.
168
+ def build_layout
169
+ # Section order: NULL, the content sections, one .rela per relocated
170
+ # target, then the symbol and string tables. Indices are assigned as this
171
+ # list is built so the cross-references below resolve to concrete values.
172
+ sections = [null_layout]
173
+ index = 1
174
+ @sections.each do |sec|
175
+ sec.index = index
176
+ index += 1
177
+ end
178
+ rela_targets = @sections.select { |sec| relocations_for(sec).any? }
179
+ symtab_index = index + rela_targets.size
180
+ strtab_index = symtab_index + 1
181
+ shstrtab_index = strtab_index + 1
182
+
183
+ strtab, sym_name_offsets = build_strtab
184
+ symtab = build_symtab(sym_name_offsets)
185
+
186
+ @sections.each { |sec| sections << content_layout(sec) }
187
+ rela_targets.each do |target|
188
+ sections << rela_layout(target, relocations_for(target), symtab_index)
189
+ end
190
+ sections << symtab_layout(symtab, strtab_index)
191
+ sections << strtab_layout(strtab)
192
+ sections << shstrtab_layout
193
+
194
+ { sections: sections, shstrtab_index: shstrtab_index }
195
+ end
196
+
197
+ def null_layout
198
+ LaidOut.new(name: nil, type: 0, flags: 0, addr: 0, size: 0, link: 0, info: 0,
199
+ addralign: 0, entsize: 0, data: "".b)
200
+ end
201
+
202
+ # A NOBITS section reports its in-memory size but carries no file bytes; any
203
+ # other section carries its data (nil data means the layout pass skips it).
204
+ def content_layout(sec)
205
+ nobits = sec.type == SHT_NOBITS
206
+ LaidOut.new(
207
+ name: sec.name, type: sec.type, flags: sec.flags, addr: 0,
208
+ size: nobits ? (sec.size || 0) : (sec.data ? sec.data.bytesize : 0),
209
+ link: 0, info: 0, addralign: sec.addralign, entsize: sec.entsize,
210
+ data: nobits ? nil : (sec.data || "".b)
211
+ )
212
+ end
213
+
214
+ def rela_layout(target, relocs, symtab_index)
215
+ LaidOut.new(
216
+ name: ".rela#{target.name}", type: SHT_RELA, flags: SHF_INFO_LINK, addr: 0,
217
+ size: relocs.size * RELA_ENTSIZE, link: symtab_index, info: target.index,
218
+ addralign: 8, entsize: RELA_ENTSIZE, data: build_rela(relocs)
219
+ )
220
+ end
221
+
222
+ def symtab_layout(symtab, strtab_index)
223
+ LaidOut.new(
224
+ name: ".symtab", type: SHT_SYMTAB, flags: 0, addr: 0, size: symtab.bytesize,
225
+ link: strtab_index, info: @first_global, addralign: 8, entsize: SYM_ENTSIZE,
226
+ data: symtab
227
+ )
228
+ end
229
+
230
+ def strtab_layout(strtab)
231
+ LaidOut.new(name: ".strtab", type: SHT_STRTAB, flags: 0, addr: 0,
232
+ size: strtab.bytesize, link: 0, info: 0, addralign: 1, entsize: 0,
233
+ data: strtab)
234
+ end
235
+
236
+ def shstrtab_layout
237
+ # Filled with real bytes in #assemble, once every section name is known.
238
+ LaidOut.new(name: ".shstrtab", type: SHT_STRTAB, flags: 0, addr: 0, size: 0,
239
+ link: 0, info: 0, addralign: 1, entsize: 0, data: nil)
240
+ end
241
+
242
+ # Builds .strtab (symbol names). Returns [bytes, name -> offset]. Names are
243
+ # de-duplicated so two symbols with the same string share one entry.
244
+ def build_strtab
245
+ buf = +"\0".b
246
+ offsets = {}
247
+ @symbols.each do |sym|
248
+ name = sym.name
249
+ next if name.nil? || name.empty? || offsets.key?(name)
250
+
251
+ offsets[name] = buf.bytesize
252
+ buf << name.b << "\0".b
253
+ end
254
+ [buf, offsets]
255
+ end
256
+
257
+ def build_symtab(sym_name_offsets)
258
+ buf = +"".b
259
+ @symbols.each do |sym|
260
+ shndx = sym.section ? sym.section.index : sym.shndx
261
+ name_off = sym.name && !sym.name.empty? ? sym_name_offsets.fetch(sym.name) : 0
262
+ buf << sym_entry(name: name_off, info: (sym.bind << 4) | sym.type,
263
+ other: sym.visibility, shndx: shndx, value: sym.value, size: sym.size)
264
+ end
265
+ buf
266
+ end
267
+
268
+ # Builds one .rela payload: 24 bytes per entry (r_offset, r_info, r_addend).
269
+ # r_info packs the symbol's resolved table index with the numeric type.
270
+ def build_rela(relocs)
271
+ buf = +"".b
272
+ relocs.each do |reloc|
273
+ r_info = (reloc.symbol.index << 32) | (reloc.type & 0xFFFFFFFF)
274
+ buf << [reloc.offset].pack("Q<") << [r_info].pack("Q<") << [reloc.addend].pack("q<")
275
+ end
276
+ buf
277
+ end
278
+
279
+ # Computes file offsets, fills in .shstrtab, and concatenates the header,
280
+ # section payloads and section header table.
281
+ def assemble(layout)
282
+ sections = layout[:sections]
283
+ shstrtab, name_offsets = build_shstrtab(sections)
284
+ sections[layout[:shstrtab_index]].data = shstrtab
285
+ sections[layout[:shstrtab_index]].size = shstrtab.bytesize
286
+
287
+ offset = EHDR_SIZE
288
+ sections.each do |sec|
289
+ # A NOBITS section keeps an aligned sh_offset for tooling but does not
290
+ # advance the file cursor; a NULL section (nil-ish) is skipped likewise.
291
+ if sec.type == SHT_NOBITS
292
+ offset = align(offset, [sec.addralign, 1].max)
293
+ sec.offset = offset
294
+ next
295
+ end
296
+ next if sec.data.nil?
297
+
298
+ offset = align(offset, [sec.addralign, 1].max)
299
+ sec.offset = offset
300
+ offset += sec.data.bytesize
301
+ end
302
+ shoff = align(offset, 8)
303
+
304
+ out = +"".b
305
+ out << build_ehdr(shoff, sections.size, layout[:shstrtab_index])
306
+ sections.each do |sec|
307
+ next if sec.data.nil? || sec.type == SHT_NOBITS
308
+
309
+ pad_to(out, sec.offset)
310
+ out << sec.data
311
+ end
312
+ pad_to(out, shoff)
313
+ sections.each { |sec| out << build_shdr(sec, name_offsets) }
314
+ out
315
+ end
316
+
317
+ def build_shstrtab(sections)
318
+ buf = +"\0".b
319
+ offsets = {}
320
+ sections.each do |sec|
321
+ name = sec.name
322
+ next if name.nil? || offsets.key?(name)
323
+
324
+ offsets[name] = buf.bytesize
325
+ buf << name.b << "\0".b
326
+ end
327
+ [buf, offsets]
328
+ end
329
+
330
+ def build_ehdr(shoff, shnum, shstrndx)
331
+ e_ident = [0x7F, 0x45, 0x4C, 0x46, ELFCLASS64, ELFDATA2LSB, EV_CURRENT,
332
+ 0, 0, 0, 0, 0, 0, 0, 0, 0].pack("C16")
333
+ e_ident +
334
+ [ET_REL].pack("S<") +
335
+ [@machine].pack("S<") +
336
+ [EV_CURRENT].pack("L<") +
337
+ [0].pack("Q<") + # e_entry
338
+ [0].pack("Q<") + # e_phoff
339
+ [shoff].pack("Q<") + # e_shoff
340
+ [0].pack("L<") + # e_flags
341
+ [EHDR_SIZE].pack("S<") + # e_ehsize
342
+ [0].pack("S<") + # e_phentsize
343
+ [0].pack("S<") + # e_phnum
344
+ [SHDR_ENTSIZE].pack("S<") + # e_shentsize
345
+ [shnum].pack("S<") + # e_shnum
346
+ [shstrndx].pack("S<") # e_shstrndx
347
+ end
348
+
349
+ def build_shdr(sec, name_offsets)
350
+ [sec.name ? name_offsets[sec.name] : 0].pack("L<") +
351
+ [sec.type].pack("L<") +
352
+ [sec.flags].pack("Q<") +
353
+ [sec.addr].pack("Q<") +
354
+ [sec.offset || 0].pack("Q<") +
355
+ [sec.size].pack("Q<") +
356
+ [sec.link].pack("L<") +
357
+ [sec.info].pack("L<") +
358
+ [sec.addralign].pack("Q<") +
359
+ [sec.entsize].pack("Q<")
360
+ end
361
+
362
+ def sym_entry(name:, info:, other:, shndx:, value:, size:)
363
+ [name].pack("L<") + [info].pack("C") + [other].pack("C") +
364
+ [shndx].pack("S<") + [value].pack("Q<") + [size].pack("Q<")
365
+ end
366
+
367
+ def align(value, alignment)
368
+ (value + alignment - 1) / alignment * alignment
369
+ end
370
+
371
+ def pad_to(buffer, target_offset)
372
+ buffer << ("\0" * (target_offset - buffer.bytesize)).b if buffer.bytesize < target_offset
373
+ end
374
+ end
375
+ end
376
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Pkgconf
5
+ PROG = "rubycc-pkgconf"
6
+
7
+ # The exact option vocabulary mkmf's mkmf.rb#pkg_config can invoke
8
+ # $PKGCONFIG with (tallied against this environment's mkmf.rb — see the
9
+ # Step 59 report): `--exists` to probe availability, and, as `--#{option}`
10
+ # for each entry of pkg_config's own *options argument or of its built-in
11
+ # default path, `--modversion`/`--cflags`/`--cflags-only-I`/
12
+ # `--cflags-only-other`/`--libs`/`--libs-only-l`. No other pkg-config
13
+ # option (--static, --print-requires, ...) is in scope.
14
+ KNOWN_OPTIONS = %w[exists modversion cflags cflags-only-I cflags-only-other libs libs-only-l].freeze
15
+
16
+ # Runs the shim for +argv+ and returns the process exit status: 0 on
17
+ # success, 1 if a named module (or one of its Requires) cannot be found,
18
+ # the .pc text is malformed, or an unknown option/no module was given —
19
+ # the same pass/fail contract mkmf's xsystem(... "--exists" ...) and
20
+ # get[...] calls rely on.
21
+ def self.run(argv, stdout: $stdout, stderr: $stderr, resolver: Resolver.new, filter: SystemPathFilter.new)
22
+ Cli.new(argv, stdout: stdout, stderr: stderr, resolver: resolver, filter: filter).run
23
+ end
24
+
25
+ # Parses argv into requested options + module names and renders exactly
26
+ # the options given, in the order given, onto one line — mirroring how
27
+ # mkmf's pkg_config() invokes $PKGCONFIG with all of a single call's
28
+ # `--#{option}` flags at once and reads back one combined line of output.
29
+ class Cli
30
+ def initialize(argv, stdout:, stderr:, resolver:, filter:)
31
+ @argv = argv
32
+ @out = stdout
33
+ @err = stderr
34
+ @resolver = resolver
35
+ @filter = filter
36
+ @options = []
37
+ @modules = []
38
+ end
39
+
40
+ def run
41
+ parse_argv
42
+ return 1 if @options.empty?
43
+
44
+ if @modules.empty?
45
+ @err.puts "#{PROG}: no package name specified"
46
+ return 1
47
+ end
48
+
49
+ if @options.include?("exists")
50
+ @modules.all? { |name| @resolver.exists?(name) } ? 0 : 1
51
+ else
52
+ render
53
+ end
54
+ rescue PkgconfError => e
55
+ @err.puts "#{PROG}: #{e.message}"
56
+ 1
57
+ end
58
+
59
+ private
60
+
61
+ def parse_argv
62
+ @argv.each do |arg|
63
+ if arg.start_with?("--")
64
+ option = arg[2..]
65
+ unless KNOWN_OPTIONS.include?(option)
66
+ @err.puts "#{PROG}: unknown option '#{arg}'"
67
+ @options.clear
68
+ return
69
+ end
70
+ @options << option
71
+ else
72
+ @modules << arg
73
+ end
74
+ end
75
+ end
76
+
77
+ def render
78
+ parts = @options.map { |option| render_option(option) }
79
+ @out.puts parts.reject(&:empty?).join(" ")
80
+ 0
81
+ end
82
+
83
+ def render_option(option)
84
+ case option
85
+ when "modversion" then @modules.map { |name| @resolver.load(name).version.to_s }.join(" ")
86
+ when "cflags" then all_cflags.join(" ")
87
+ when "cflags-only-I" then all_cflags.select { |t| t.start_with?("-I") }.join(" ")
88
+ when "cflags-only-other" then all_cflags.reject { |t| t.start_with?("-I") }.join(" ")
89
+ when "libs" then all_libs.join(" ")
90
+ when "libs-only-l" then all_libs.select { |t| t.start_with?("-l") }.join(" ")
91
+ end
92
+ end
93
+
94
+ # Multiple module arguments are concatenated in the order given — the
95
+ # "複数モジュール" case mkmf's pkg_config() call shape allows for.
96
+ #
97
+ # The system-path filter is applied here, once, rather than inside
98
+ # Resolver or per rendered option: Resolver's token lists are what the
99
+ # Requires chain says (the parser-level tests assert exactly that), while
100
+ # every option that renders those tokens — --cflags, --cflags-only-I,
101
+ # --cflags-only-other, --libs, --libs-only-l — has to agree on which of
102
+ # them are suppressed. --cflags-only-other and --libs-only-l are
103
+ # unaffected in practice, since they keep only tokens the filter never
104
+ # touches.
105
+ def all_cflags
106
+ dedupe_search_paths(@filter.cflags(@modules.flat_map { |name| @resolver.cflags_tokens(name) }), "-I")
107
+ end
108
+
109
+ def all_libs
110
+ dedupe_search_paths(@filter.libs(@modules.flat_map { |name| @resolver.libs_tokens(name) }), "-L")
111
+ end
112
+
113
+ # Collapse a repeated search-path token to its first occurrence, the way
114
+ # the real pkg-config does. A directory named twice searches the same
115
+ # directory twice, so dropping the later copy cannot change what is found,
116
+ # while keeping the first preserves the order the .pc files asked for.
117
+ #
118
+ # This is not hypothetical tidying: zlib.pc writes
119
+ # `Libs: -L${libdir} -L${sharedlibdir} -lz` and on this environment both
120
+ # variables expand to the same directory, so the shim emitted that -L
121
+ # twice where the real tool emitted it once. It went unnoticed because the
122
+ # system-path filter happened to remove both copies on Debian; measured on
123
+ # Alpine, where that directory is not a system directory and neither copy
124
+ # is removed (docs/development/STEPS.md Step 199).
125
+ #
126
+ # Only search paths are collapsed. `-l` is deliberately left alone: a
127
+ # repeated library can matter to a static link's resolution order, and no
128
+ # measurement here covers it.
129
+ def dedupe_search_paths(tokens, flag)
130
+ seen = {}
131
+ tokens.reject do |token|
132
+ next false unless token.start_with?(flag) && token.length > flag.length
133
+
134
+ seen.key?(token).tap { seen[token] = true }
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ # The base error is normally provided by lib/rubycc.rb. Like rmake (M3 B1),
5
+ # Pkgconf is loadable on its own (its only caller so far is the
6
+ # exe/rubycc-pkgconf CLI, not the Rubycc aggregate), so define a stand-in only
7
+ # when the full library has not been required yet.
8
+ Error = Class.new(StandardError) unless defined?(Error)
9
+
10
+ module Pkgconf
11
+ # Base for every error this shim raises: a malformed .pc file, a module
12
+ # that cannot be found on the search path, or a construct outside the
13
+ # subset mkmf's pkg_config() actually calls for (R11/ROADMAP §6 B4).
14
+ class PkgconfError < Rubycc::Error; end
15
+
16
+ # The .pc text used a construct the parser does not accept: an
17
+ # unrecognized line, or a `${var}` reference to a variable that was never
18
+ # assigned (in this file, at the point the reference appears — pkg-config
19
+ # itself requires forward-references to be avoided the same way).
20
+ class ParseError < PkgconfError
21
+ def initialize(message, path:, line_number:)
22
+ super("#{path || '<pc>'}:#{line_number}: #{message}")
23
+ end
24
+ end
25
+
26
+ # No `<name>.pc` was found on the search path (PKG_CONFIG_PATH, then the
27
+ # default directories) — the same condition mkmf's `pkg-config --exists`
28
+ # probe reports as "not found".
29
+ class NotFoundError < PkgconfError; end
30
+
31
+ # A `Requires`/`Requires.private` entry carried a version comparison
32
+ # (`zlib >= 1.2`-style). mkmf's own `pkg_config(pkg, *options)` never
33
+ # passes a version constraint — the `pkg` argument is always a bare module
34
+ # name — so comparing versions is out of scope here (see the Step 59
35
+ # report for the mkmf.rb tally this is based on); a constraint inside a
36
+ # `Requires` field is diagnosed instead of silently ignored or guessed at.
37
+ class UnsupportedError < PkgconfError; end
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Pkgconf
5
+ # One parsed .pc file: the handful of fields mkmf's pkg_config() and its
6
+ # own Requires chain ever touch. `cflags`/`libs`/`libs_private` are the
7
+ # field text after `${var}` expansion, still a raw (unsplit) string —
8
+ # Resolver is the one that shellsplits it into tokens, since only Resolver
9
+ # knows whether a field is being consumed for --cflags or --libs.
10
+ class Package
11
+ attr_reader :name, :description, :version, :requires, :requires_private,
12
+ :cflags, :libs, :libs_private, :path
13
+
14
+ def initialize(name:, description:, version:, requires:, requires_private:,
15
+ cflags:, libs:, libs_private:, path:)
16
+ @name = name
17
+ @description = description
18
+ @version = version
19
+ @requires = requires
20
+ @requires_private = requires_private
21
+ @cflags = cflags
22
+ @libs = libs
23
+ @libs_private = libs_private
24
+ @path = path
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Pkgconf
5
+ # Parses one .pc file's text into a Package. The grammar covered is
6
+ # exactly what the real .pc files this environment ships (zlib.pc,
7
+ # libffi.pc, openssl.pc's Requires chain — see test/fixtures/pkgconfig)
8
+ # and mkmf's own pkg_config() call shape actually use: `#`-comments and
9
+ # blank lines, `name=value` variable assignments, `${name}` expansion
10
+ # inside both variable and field values, and the seven fields
11
+ # Name/Description/Version/Requires/Requires.private/Libs/Libs.private/
12
+ # Cflags. Any other field line (URL:, Conflicts:, ...) is accepted and
13
+ # ignored — mkmf's pkg_config() never reads them — rather than rejected,
14
+ # so a real-world .pc with extra fields still parses.
15
+ class Parser
16
+ VARIABLE_LINE = /\A([A-Za-z_][A-Za-z0-9_.]*)=(.*)\z/
17
+ FIELD_LINE = /\A([A-Za-z][A-Za-z0-9.]*)\s*:\s*(.*)\z/
18
+ VARIABLE_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_.]*)\}/
19
+
20
+ # A single `Requires`/`Requires.private` entry with a version
21
+ # comparison attached, spaced (`zlib >= 1.2`) or not (`zlib>=1.2`).
22
+ REQUIRES_OPERATOR = /\A(<=|>=|==|!=|<|>|=)\z/
23
+
24
+ KNOWN_FIELDS = %w[Name Description Version Requires Requires.private Libs Libs.private Cflags].freeze
25
+
26
+ def self.parse(text, path: nil)
27
+ new(text, path: path).parse
28
+ end
29
+
30
+ def initialize(text, path: nil)
31
+ @text = text
32
+ @path = path
33
+ @variables = {}
34
+ @fields = {}
35
+ end
36
+
37
+ def parse
38
+ @text.each_line.with_index(1) do |raw_line, line_number|
39
+ parse_line(raw_line.chomp, line_number)
40
+ end
41
+ build_package
42
+ end
43
+
44
+ private
45
+
46
+ def parse_line(line, line_number)
47
+ return if line.strip.empty?
48
+ return if line.lstrip.start_with?("#")
49
+
50
+ if (m = VARIABLE_LINE.match(line))
51
+ @variables[m[1]] = expand(m[2].strip, line_number)
52
+ elsif (m = FIELD_LINE.match(line))
53
+ @fields[m[1]] = expand(m[2].strip, line_number) if KNOWN_FIELDS.include?(m[1])
54
+ else
55
+ raise ParseError.new("unrecognized line: #{line.inspect}", path: @path, line_number: line_number)
56
+ end
57
+ end
58
+
59
+ # Variables are expanded at the point they are assigned, against
60
+ # whatever variables are already known — the same order the real
61
+ # fixtures rely on (prefix, then exec_prefix=${prefix}, then
62
+ # libdir=${exec_prefix}/lib, ...). Fields are expanded the same way,
63
+ # against the full variable table (fields conventionally follow all
64
+ # variable assignments in a .pc file).
65
+ def expand(value, line_number)
66
+ value.gsub(VARIABLE_REFERENCE) do
67
+ name = Regexp.last_match(1)
68
+ @variables.fetch(name) do
69
+ raise ParseError.new("undefined variable '#{name}'", path: @path, line_number: line_number)
70
+ end
71
+ end
72
+ end
73
+
74
+ def build_package
75
+ Package.new(
76
+ name: @fields["Name"],
77
+ description: @fields["Description"],
78
+ version: @fields["Version"],
79
+ requires: parse_requires(@fields["Requires"]),
80
+ requires_private: parse_requires(@fields["Requires.private"]),
81
+ cflags: @fields["Cflags"] || "",
82
+ libs: @fields["Libs"] || "",
83
+ libs_private: @fields["Libs.private"] || "",
84
+ path: @path
85
+ )
86
+ end
87
+
88
+ # A bare module-name list ("libssl libcrypto" or "libssl, libcrypto").
89
+ # A version comparison attached to an entry is diagnosed rather than
90
+ # evaluated: mkmf's pkg_config(pkg, *options) always passes `pkg` as a
91
+ # bare module name (see the Step 59 report), so this shim only needs to
92
+ # resolve Requires by name.
93
+ def parse_requires(value)
94
+ return [] if value.nil? || value.strip.empty?
95
+
96
+ spaced = value.gsub(/(<=|>=|==|!=|<|>|=)/) { " #{Regexp.last_match(1)} " }
97
+ tokens = spaced.split(/[,\s]+/).reject(&:empty?)
98
+ tokens.each do |token|
99
+ next unless REQUIRES_OPERATOR.match?(token)
100
+
101
+ raise UnsupportedError,
102
+ "version-constrained Requires (#{value.inspect}) is unsupported: mkmf's pkg_config() " \
103
+ "never passes a version constraint, so only bare module names in Requires are handled"
104
+ end
105
+ tokens
106
+ end
107
+ end
108
+ end
109
+ end