-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcompat.vulkan-runtime.lua
More file actions
1106 lines (1069 loc) · 53.2 KB
/
Copy pathcompat.vulkan-runtime.lua
File metadata and controls
1106 lines (1069 loc) · 53.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- compat.vulkan-runtime — host Vulkan ICD adapter for mcpp Linux applications.
--
-- The exact counterpart of `compat.glx-runtime`, for the same reason and in the
-- same shape. A PROPRIETARY driver cannot be a package: its userspace is in ABI
-- lockstep with a kernel module and its licence forbids redistribution, so the
-- GL runtime plan (.agents/docs/2026-06-03-gl-runtime-packages-plan.md) settled
-- on modelling that as a HOST CAPABILITY. An open driver is a payload --
-- `xim:mesa-lavapipe` for the CPU, `xim:mesa` for AMD hardware -- and a machine
-- using one needs nothing from this farm at all. Nothing is vendored here; this
-- is a symlink farm plus the metadata that makes it reachable, and since
-- 2026.09.05 every farmed library a published payload also provides is taken
-- from the payload when the payload's symbol set covers the host copy's, so the
-- host surface it records is proprietary userspace and packaging backlog only.
--
-- WHAT IT FIXES. `compat.vulkan` builds the Khronos loader, and the loader finds
-- every ICD manifest on the host correctly. It then fails to dlopen a single
-- driver:
--
-- DRIVER: Found the following files: /usr/share/vulkan/icd.d/lvp_icd.json …
-- ERROR: libvulkan_lvp.so: cannot open shared object file
--
-- The libraries are right there in /usr/lib/x86_64-linux-gnu. What cannot reach
-- them is the process: an mcpp-built binary runs under mcpp's OWN glibc
--
-- interp: …/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2
-- rpath : …/xim-x-glibc/2.39/lib64:…/xim-x-gcc/…/lib64:$ORIGIN
--
-- so a bare-soname dlopen from inside the sandbox does not search the host's
-- library path at all. `runtime.library_dirs` below puts a package-owned
-- directory of symlinks on that path, which is precisely how `compat.glx-runtime`
-- makes host OpenGL work — and why the OpenGL backends already run while Vulkan
-- did not.
--
-- THE PATTERN LIST covers the ICDs plus their transitive dependencies, because
-- the whole chain has to resolve through the same directory. Mesa's software
-- rasterizer pulls LLVM; NVIDIA pulls its own family. `libstdc++` is in the list
-- and that is not an oversight: mcpp links libstdc++ STATICALLY (it is absent
-- from a built binary's NEEDED), so a dlopen'd C++ ICD like lavapipe has nothing
-- to resolve against unless the host copy is provided here.
--
-- NOTHING IS REQUIRED. Unlike `compat.glx-runtime`, which errors when libGL is
-- missing, a machine with no Vulkan driver at all is a legitimate configuration
-- — every CI runner in this repo is one. The farm is then simply empty and the
-- loader reports its own four extensions, which is what
-- `tests/examples/vulkan` asserts.
package = {
spec = "1",
namespace = "compat",
name = "vulkan-runtime",
description = "Host Vulkan ICD runtime adapter for mcpp Linux applications",
licenses = {"Apache-2.0"},
repo = "https://github.com/KhronosGroup/Vulkan-Loader",
type = "package",
xpm = {
linux = {
-- PLATFORM LEVEL, NOT PER VERSION. A `deps` inside a version entry
-- parses and is then not applied; compat.glx-runtime records the
-- measurement. The cost of that placement is that a consumer still
-- pinned to 2026.07.29 or 2026.09.05 also installs these, which is
-- a download it will not read and not a failure.
--
-- These are the packages whose sonames the farm substitutes for a
-- host copy, and the PAYLOAD_PACKAGES table below is the reader
-- that keeps the two lists honest: a soname it maps to a package
-- that is not installed is reported as a declaration that did not
-- take effect, rather than silently farmed from the host.
--
-- Floors, not pins. Every library here is ABI-stable at the soname
-- this farm asks for, and a pin would make one patch bump an edit
-- in this file.
deps = {
runtime = {
"xim:zlib@>=1.3", "xim:expat@>=2.6", "xim:libffi@>=3.4",
"xim:elfutils@>=0.19", "xim:libdrm@>=2.4",
"xim:libxcb@>=1.17", "xim:libX11@>=1.8",
"xim:libXau@>=1.0", "xim:libXdmcp@>=1.1",
"xim:libXext@>=1.3", "xim:libxshmfence@>=1.3",
"xim:wayland@>=1.23", "xim:gcc-runtime@>=15",
"xim:ncurses@>=6.5", "xim:zstd@>=1.5", "xim:xz@>=5.8",
"xim:libmd@>=1.2", "xim:libbsd@>=0.12",
-- Added 2026.09.10, when the farm began answering for what
-- its own members need rather than only for the ICD chain.
-- `libcrypto.so.3` is asked for by NVIDIA's PKCS#11
-- provider and `libgbm.so.1` by `libnvidia-egl-gbm`; both
-- are published here, so neither is taken from the host.
--
-- `xim:mesa` is the coarse answer for `libgbm.so.1`: it is
-- the package that ships it today. A dedicated `xim:libgbm`
-- would be the better shape and is the smaller-grained
-- follow-up, not a reason to reach into /usr/lib meanwhile.
"xim:mesa@>=25",
"xim:nvidia-video-host-link",
},
},
-- 2026.09.05: the farm is seeded from the ICD manifests, closes
-- over what they need, prefers installed payloads over host copies
-- they cover, and records the surface in HOST-SURFACE.txt. mcpp
-- identifies an installed package by (name, version), so the new
-- behaviour needs a new key; the anchor is the same file.
["latest"] = { ref = "2026.09.11" },
-- 2026.09.07: a soname carried by more than one installed payload
-- is now decided by symbol coverage rather than by which store
-- path sorts last. See find_in_store below for the measurement
-- that produced this version.
-- 2026.09.10: the farm answers for what its own members need,
-- not only for what an ICD manifest names. A member the vendor
-- pattern swept in is closed over too; only proprietary vendor
-- userspace may be taken from the host for it, anything else is
-- filled from an installed payload, and what nothing publishes is
-- recorded as unserved rather than left absent. A new key because
-- install() output is baked into the installed payload: without one
-- a host that already holds the previous version keeps the open
-- farm. The anchor is unchanged; only install() behaviour is.
-- 2026.09.11: the farm carries only what this runtime can
-- reach. `libnvidia-pkcs11*` is a PKCS#11 token module that matched
-- the vendor name pattern and nothing else -- measured with
-- LD_DEBUG on both examples, it is looked for zero times while a
-- member the driver does reach appears six -- so it leaves, and the
-- OpenSSL declaration it was the only reason for leaves with it.
-- A new key because install() output is baked into the installed
-- payload.
["2026.09.11"] = {
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
["2026.09.10"] = {
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
["2026.09.07"] = {
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
-- 2026.09.06: the payload set is DECLARED here rather than
-- discovered. Until this version the substitution pass took a
-- payload only when some earlier, unrelated install had already
-- put it in the store, so the same package produced a farm of
-- twenty payload libraries on a developer machine and a farm of
-- one in a fresh subos -- the environment decided, and the report
-- read "no installed payload provides this soname" for sonames
-- this index does publish. Measured 2026-09-05 in a fresh subos:
-- 30 host entries against 8 on the machine that happened to have
-- the stack installed.
--
-- WHAT IS NOT HERE AND WHY. `xim:icu` (78) and `xim:libedit` (0)
-- carry different sonames than the ones a host Mesa built against
-- Ubuntu 24.04 asks for (`libicuuc.so.74`, `libedit.so.2`); a
-- different soname is a different ABI, so those two cannot
-- substitute anything here and would only be a download. Same for
-- `xim:libllvm` and `xim:libxml2`, whose payloads the symbol test
-- rejects (12215 and 195 symbols short of the host copies).
["2026.09.06"] = {
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
["2026.09.05"] = {
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
["2026.07.29"] = {
-- Nothing is downloaded that matters: the package's content is
-- the symlink farm install() builds from the host. This is just
-- a stable, tiny anchor so the xpm entry is well-formed, the
-- same trick compat.glx-runtime uses with an OpenGL-Registry
-- README.
url = "https://raw.githubusercontent.com/KhronosGroup/Vulkan-Loader/vulkan-sdk-1.4.357.0/README.md",
sha256 = "21ec0987a05bd680ecd11f8be747e27744d7558f7318736f6cb8a5c5ec1b8ba8",
},
},
},
mcpp = {
language = "c++23",
import_std = false,
c_standard = "c11",
sources = { "mcpp_generated/vulkan_runtime_empty.c" },
targets = { ["vulkan_runtime"] = { kind = "lib" } },
deps = {},
runtime = {
library_dirs = { "mcpp_generated/vulkan_runtime/lib" },
capabilities = { "vulkan.icd.driver" },
provides = { "vulkan.icd.driver" },
},
},
}
import("xim.libxpkg.pkginfo")
import("xim.libxpkg.log")
local function sh_quote(value)
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
end
local function split_paths(value)
local out = {}
if not value or value == "" then
return out
end
for item in tostring(value):gmatch("[^:]+") do
if item ~= "" then
table.insert(out, item)
end
end
return out
end
local function candidate_dirs()
local out = {}
local seen = {}
local function add(dir)
if dir and dir ~= "" and not seen[dir] and os.isdir(dir) then
seen[dir] = true
table.insert(out, dir)
end
end
for _, dir in ipairs(split_paths(os.getenv("MCPP_HOST_VULKAN_LIBRARY_PATH"))) do
add(dir)
end
add("/lib/x86_64-linux-gnu")
add("/usr/lib/x86_64-linux-gnu")
-- The same layout on the other Linux architecture this index builds for.
-- Absent until 2026.09.06, which made the farm empty on a Debian-family
-- aarch64 machine: every candidate directory was an x86_64 one and
-- `os.isdir` skipped them all.
add("/lib/aarch64-linux-gnu")
add("/usr/lib/aarch64-linux-gnu")
add("/lib64")
add("/usr/lib64")
add("/usr/lib")
return out
end
-- WHAT IS FARMED BY PATTERN: PROPRIETARY VENDOR USERSPACE ONLY.
--
-- Until 2026.09.05 this list also named the X client stack, libdrm, LLVM,
-- zlib, zstd, expat, libxml2, libffi, libedit, ICU and libstdc++, and harvested
-- every host copy the candidate directories held -- five LLVM versions and the
-- driver's settings GUI among them. None of that belongs on a program's
-- runtime path: what an ICD needs is computed below by closing over the
-- manifests' libraries with `ldd`, and a library a published payload also
-- provides is then taken from the payload when it covers the host copy.
--
-- The vendor family stays a pattern because a proprietary driver dlopens
-- members of its own family by name at run time (`libnvidia-glvkspirv`,
-- `libnvidia-gpucomp`), which no `DT_NEEDED` walk can see. Its settings GUI
-- (`libnvidia-gtk*`) is excluded: it is not part of any driver and would put a
-- host GTK on the path of every consumer.
--
-- EVERY PATTERN IS VERSIONED (`lib*.so.*`), deliberately. mcpp puts
-- `runtime.library_dirs` on the LINK line as well as the runtime path, so a
-- bare `libxcb.so` here would shadow this index's own `compat.xcb` at link
-- time. Versioned sonames are invisible to the linker and are exactly what
-- dlopen asks for.
--
-- The host's own `libvulkan.so*` is deliberately NOT harvested: `compat.vulkan`
-- builds the loader itself, as a shared object with the canonical
-- `libvulkan.so.1` soname, and a second one on the path would be resolved by
-- SDL2's `dlopen` instead. One loader per process is the whole point.
local host_vulkan_patterns = {
"libGLX_nvidia.so.*",
"libnvidia*.so.*",
}
-- WHAT THE PATTERN SWEEPS IN THAT THIS RUNTIME CANNOT REACH.
--
-- `libnvidia-gtk*` is the driver's settings GUI. `libnvidia-pkcs11*` is its
-- PKCS#11 provider: a cryptographic token module, loaded by an application's
-- own PKCS#11 configuration and by nothing in a GPU driver's dispatch. Both
-- match `libnvidia*.so.*` by name alone.
--
-- MEASURED, WITH A CONTROL, before removing them. `LD_DEBUG=libs` on a run of
-- each example says which files the process actually looked for:
--
-- pkcs11 0 occurrences (Vulkan and SYCL/OpenCL alike)
-- libnvidia-glvkspirv 6 occurrences (Vulkan)
-- libur_adapter_opencl / libOpenCL 23 (SYCL/OpenCL)
--
-- The last two are the control: this instrument does report a member the
-- driver reaches, so a zero for `pkcs11` is a reading rather than a silence.
-- An earlier attempt -- remove the member and see whether the example still
-- runs -- could NOT tell the two apart: removing `libnvidia-glvkspirv` left the
-- example working too, because the binary under test had been built against a
-- different farm than the one being edited.
--
-- They are also the only members that need `libcrypto` at all, so removing them
-- removes this package's reason to declare OpenSSL, and with it the
-- `libcrypto.so.1.1` that no ecosystem package can serve: OpenSSL 1.1 is
-- end-of-life upstream. A member that cannot be reached is not a capability
-- being dropped -- it is a file the name pattern collected.
-- SHELL GLOBS, because the only reader is a shell `case`. They were Lua
-- patterns, and the loop that consumes them passed a hard-coded
-- `libnvidia-gtk*` instead of the element it had just bound -- so the table
-- read like a list and behaved like one entry. Adding `libnvidia-pkcs11` to it
-- changed nothing at all, which is how this was found: the farm still had 81
-- members and both PKCS#11 providers after the "exclusion" was written.
local never_farm_patterns = { "libnvidia-gtk*", "libnvidia-pkcs11*" }
-- THE DECLARATION'S READER. `xpm.linux.deps` above names the packages whose
-- libraries this farm substitutes; this table says which soname each one is
-- declared for. When a soname it maps is still taken from the host, the report
-- says the declaration did not take effect -- which is the only way a drift
-- between the two lists becomes visible, since a missing dependency otherwise
-- looks exactly like a machine that has no payload for it.
local PAYLOAD_PACKAGES = {
["libz.so.1"] = "xim:zlib",
["libexpat.so.1"] = "xim:expat",
["libffi.so.8"] = "xim:libffi",
["libelf.so.1"] = "xim:elfutils",
["libdrm.so.2"] = "xim:libdrm",
["libxcb.so.1"] = "xim:libxcb",
["libxcb-dri2.so.0"] = "xim:libxcb",
["libxcb-dri3.so.0"] = "xim:libxcb",
["libxcb-present.so.0"] = "xim:libxcb",
["libxcb-randr.so.0"] = "xim:libxcb",
["libxcb-shm.so.0"] = "xim:libxcb",
["libxcb-sync.so.1"] = "xim:libxcb",
["libxcb-xfixes.so.0"] = "xim:libxcb",
["libX11.so.6"] = "xim:libX11",
["libX11-xcb.so.1"] = "xim:libX11",
["libXau.so.6"] = "xim:libXau",
["libXdmcp.so.6"] = "xim:libXdmcp",
["libXext.so.6"] = "xim:libXext",
["libxshmfence.so.1"] = "xim:libxshmfence",
["libwayland-client.so.0"]= "xim:wayland",
["libstdc++.so.6"] = "xim:gcc-runtime",
["libtinfo.so.6"] = "xim:ncurses",
["libzstd.so.1"] = "xim:zstd",
["liblzma.so.5"] = "xim:xz",
["libmd.so.0"] = "xim:libmd",
["libbsd.so.0"] = "xim:libbsd",
["libgbm.so.1"] = "xim:mesa",
["libwayland-server.so.0"]= "xim:wayland",
}
local never_farm = {
["libc.so.6"] = true, ["libm.so.6"] = true, ["libdl.so.2"] = true,
["libpthread.so.0"] = true, ["librt.so.1"] = true, ["libresolv.so.2"] = true,
["ld-linux-x86-64.so.2"] = true, ["ld-linux-aarch64.so.1"] = true,
["libgcc_s.so.1"] = true, ["libvulkan.so.1"] = true,
}
-- THE PATTERN LIST NAMES WHAT IS DLOPENED; THIS CLOSES WHAT IT NEEDS.
--
-- A hand-written list of transitive dependencies is a list someone has to keep
-- correct against libraries nobody in this repository builds, and the comment
-- above already says incomplete is worse than absent. Measured 2026-09-05:
-- every pattern above matched, `libvulkan_lvp.so` and `libLLVM.so.20.1` were
-- both in the farm, and the loader still reported
--
-- ERROR: libicuuc.so.74: cannot open shared object file
-- ERROR | DRIVER: loader_icd_scan: Failed loading library associated with
-- ICD JSON libvulkan_lvp.so. Ignoring this JSON
--
-- so a machine with a software rasterizer installed enumerated no CPU device at
-- all. LLVM 20 links ICU; nothing in the list said so, and nothing could have
-- without someone reading LLVM's dependencies by hand.
--
-- `ldd` is asked instead, and it answers TRANSITIVELY, which is the property a
-- list cannot have. Its output feeds only symlink creation, so the farm keeps
-- the property the pattern list was written for: what `ldd` reports is a
-- `DT_NEEDED` soname, always versioned, so nothing this pass adds can shadow a
-- `libfoo.so` the linker resolves.
--
-- THE SEED IS THE ICD SET, NOT THE FARM. Closing over every file the pattern
-- list matched pulled 64 libraries here, GTK 2 and GTK 3 among them, because
-- `libnvidia*.so.*` also matches the driver's settings GUI. Those libraries are
-- on the consuming binary's runtime path, where a host GTK can shadow an index
-- package's; a driver the loader will never dlopen has no business putting it
-- there. The manifests state exactly which libraries the loader loads, so they
-- are what gets closed over.
--
-- THE `ldd` ON `PATH` IS NOT NECESSARILY THE HOST'S. Under xlings it is the
-- payload's own, and a private loader's default search path is its build prefix
-- rather than the host's — measured on one machine, in one shell, seconds apart:
--
-- $ ldd /usr/lib/x86_64-linux-gnu/libvulkan_lvp.so
-- libLLVM.so.20.1 => not found
-- $ /usr/bin/ldd /usr/lib/x86_64-linux-gnu/libvulkan_lvp.so
-- libLLVM.so.20.1 => /lib/x86_64-linux-gnu/libLLVM.so.20.1
--
-- The first spelling is not an error the pass can detect: every line reads
-- `not found`, the `=> /path` pattern matches nothing, and the pass reports
-- closing over zero libraries — the same reading it gives on a machine that
-- genuinely needs nothing. So the search path is supplied explicitly rather
-- than inherited, which makes the answer independent of which `ldd` runs.
local function icd_manifest_dirs()
local out, seen = {}, {}
local function add(dir)
if dir and dir ~= "" and not seen[dir] and os.isdir(dir) then
seen[dir] = true
table.insert(out, dir)
end
end
-- The loader's own order. `VK_DRIVER_FILES` is deliberately not consulted:
-- it overrides the machine's drivers for one run, and this farm is built
-- once, at install time, for every run afterwards.
local data_home = os.getenv("XDG_DATA_HOME")
if data_home and data_home ~= "" then
add(path.join(data_home, "vulkan", "icd.d"))
elseif os.getenv("HOME") then
add(path.join(os.getenv("HOME"), ".local", "share", "vulkan", "icd.d"))
end
for _, base in ipairs(split_paths(os.getenv("XDG_DATA_DIRS"))) do
add(path.join(base, "vulkan", "icd.d"))
end
add("/usr/local/share/vulkan/icd.d")
add("/usr/share/vulkan/icd.d")
add("/etc/vulkan/icd.d")
return out
end
-- Every `library_path` an ICD manifest names, resolved the way the loader
-- resolves it: a path with a separator is relative to the manifest, a bare
-- soname is searched for.
local function icd_seed_libraries(dirs)
local mdirs = icd_manifest_dirs()
if #mdirs == 0 then return {} end
local args = {}
for _, d in ipairs(mdirs) do table.insert(args, sh_quote(d)) end
local f = io.popen(
"for d in " .. table.concat(args, " ") .. "; do " ..
"for j in \"$d\"/*.json; do [ -e \"$j\" ] || continue; " ..
"sed -n 's/.*\"library_path\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' \"$j\" " ..
"| head -1 | while read -r v; do printf '%s\\t%s\\n' \"$d\" \"$v\"; done; " ..
"done; done")
if not f then return {} end
local seeds, seen = {}, {}
for line in f:lines() do
local dir, value = line:match("^([^\t]*)\t(.*)$")
if value and value ~= "" then
local candidates = {}
if value:sub(1, 1) == "/" then
table.insert(candidates, value)
elseif value:find("/") then
table.insert(candidates, path.join(dir, value))
else
for _, libdir in ipairs(dirs) do
table.insert(candidates, path.join(libdir, value))
end
end
for _, c in ipairs(candidates) do
if not seen[c] and os.isfile(c) then
seen[c] = true
table.insert(seeds, c)
end
end
end
end
f:close()
return seeds
end
local host_prefixes = {"/usr/", "/lib/", "/lib64/", "/opt/"}
local function close_over_needed(outdir, dirs)
local seeds = icd_seed_libraries(dirs)
if #seeds == 0 then return 0 end
local accept = {}
for _, dir in ipairs(dirs) do accept[dir] = true end
local function is_host_library(full)
local dir = full:match("^(.*)/[^/]+$")
if dir and accept[dir] then return true end
for _, prefix in ipairs(host_prefixes) do
if full:sub(1, #prefix) == prefix then return true end
end
-- Anything else is a payload: mcpp's own C library and toolchain live
-- under the user's home, and a second copy of either on this path is
-- the one failure worse than a missing driver.
return false
end
local args = {}
for _, seed in ipairs(seeds) do table.insert(args, sh_quote(seed)) end
-- One pass suffices: `ldd` reports the whole transitive closure of a file,
-- not just its direct `DT_NEEDED` entries.
local f = io.popen(string.format(
[[for lib in %s; do LD_LIBRARY_PATH=%s ldd "$lib" 2>/dev/null; done ]] ..
[[| sed -n 's/.*=> \(\/[^ ]*\).*/\1/p' | sort -u]],
table.concat(args, " "), sh_quote(table.concat(dirs, ":"))))
if not f then return 0 end
local wanted = {}
for line in f:lines() do
local full = line:gsub("[\r\n]+$", "")
if full ~= "" then wanted[#wanted + 1] = full end
end
f:close()
local added = 0
for _, full in ipairs(wanted) do
local base = full:match("([^/]+)$")
if base and not never_farm[base] and is_host_library(full)
and not os.isfile(path.join(outdir, base)) then
os.exec(string.format([[ln -sf "%s" "%s"]], full, path.join(outdir, base)))
added = added + 1
end
end
return added
end
-- WHAT THE FARM STILL TAKES FROM THE HOST, WRITTEN DOWN.
--
-- This package exists because a GPU driver cannot be a package, and that is
-- true of the driver. It is not true of `libxcb`, `libz` or `libxml2`, which
-- this index publishes and which the pattern list nonetheless harvests from
-- /usr/lib. The distinction was never recorded anywhere, so "how much of the
-- host does a Vulkan program still touch" could only be answered by reading
-- the list and guessing.
--
-- Three classes, and only the first is irreducible:
--
-- * PROPRIETARY VENDOR USERSPACE -- `libGLX_nvidia.so.*`, `libnvidia*.so.*`,
-- and `libcuda.so.1` alongside them. In ABI lockstep with a kernel module
-- and not redistributable, which is why the ecosystem LINKS them
-- (`xim:nvidia-gl-host-link`, `xim:libcuda-host-link`) and never copies.
-- * THE HOST MESA AND WHAT IT WAS LINKED AGAINST -- `libvulkan_*.so` and the
-- `libLLVM.so.20.1` whose soname names that build. Neither is irreducible:
-- Mesa is open source, `xim:mesa` builds it in a subos, and a machine
-- using the PAYLOAD driver has neither entry. The soname cannot be
-- substituted, so the answer is not to substitute it -- it is to stop
-- loading the host's Mesa, which is what `xim:mesa-lavapipe` already does
-- for the software driver and what extending `xim:mesa`'s driver set does
-- for AMD and Intel.
-- * EVERYTHING ELSE -- the X protocol stack, zlib, expat, libxml2, libffi,
-- libdrm, and the C++ runtime. Every one of these is, or should be, an xim
-- package; `libstdc++`/`libgcc_s` in particular are redistributable and
-- `xim:gcc-runtime` publishes them.
--
-- THE SUBSTITUTION IS DIRECTIONAL, NOT FORBIDDEN. A host ICD was linked
-- against the host's copies of the third class, so an OLDER package copy fails
-- as a missing symbol version at dlopen time; a NEWER one is what a
-- distribution upgrade does every day. What this farm does today is the
-- monotone half -- fill only what the host cannot resolve at all -- because the
-- version comparison that would license the rest has no reader here yet. The
-- entries it leaves on the host are recorded rather than accepted.
--
-- What IS done: a name the host cannot resolve at all is filled from the
-- store, which can only add resolutions. A container with an NVIDIA driver and
-- no X stack is the case this covers, and it used to fail with `libX11.so.6:
-- cannot open shared object file` naming nothing that could be installed.
--
-- And the surface is written to `HOST-SURFACE.txt` inside the package, so the
-- next round of packaging reads a measurement instead of this comment.
local function xim_store_roots()
local roots = {}
local home = (os.getenv and os.getenv("XLINGS_HOME")) or ""
if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end
roots[#roots + 1] = path.join(home, "data/xpkgs")
local pfx = pkginfo.install_dir()
if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end
return roots
end
-- The sonames the seeds still cannot resolve with the farm in place.
local function unresolved_names(outdir, seeds, dirs)
if #seeds == 0 then return {} end
local args = {}
for _, seed in ipairs(seeds) do table.insert(args, sh_quote(seed)) end
local search = table.concat(dirs, ":")
if outdir ~= "" then search = outdir .. ":" .. search end
local f = io.popen(string.format(
[[for lib in %s; do LD_LIBRARY_PATH=%s ldd "$lib" 2>/dev/null; done ]] ..
-- No POSIX character class here: `[[:space:]]` contains `]]`, which
-- ends a Lua long-bracket string. The file parsed as far as this line
-- and then reported `unexpected symbol near '\'` two lines later.
[[| sed -n 's/^[ \t]*\([^ \t]*\) => not found.*/\1/p' | sort -u]],
table.concat(args, " "), sh_quote(search)))
if not f then return {} end
local out = {}
for line in f:lines() do
local n = line:gsub("[\r\n]+$", "")
if n ~= "" and not never_farm[n] then out[#out + 1] = n end
end
f:close()
return out
end
-- One soname, looked for in the payloads this home already has. EVERY copy,
-- not the last one sorted.
--
-- More than one payload can carry a soname, and the extra copy is usually a
-- driver's vendored one: `xim:mesa-lavapipe` ships its own `libX11.so.6`
-- beside the driver. Sorting by version and taking the tail picked that copy
-- over `xim:libX11`'s -- "mesa-lavapipe/26.2.1" sorts after "libX11/1.8.10" --
-- and it was 5 symbols short of the host's, so the farm kept the host copy for
-- a soname this index publishes. Measured 2026-09-06 in a fresh subos.
--
-- The caller decides between the candidates with the test that matters, which
-- is symbol coverage; this function's job is to not hide one.
local function find_in_store(soname)
local out, seen = {}, {}
for _, root in ipairs(xim_store_roots()) do
local f = io.popen(string.format(
[[ls -1 "%s"/xim-x-*/*/lib/%s "%s"/xim-x-*/*/lib64/%s 2>/dev/null | sort -V]],
root, soname, root, soname))
if f then
for line in f:lines() do
local hit = line:gsub("[\r\n]+$", "")
if hit ~= "" and not seen[hit] then
seen[hit] = true
out[#out + 1] = hit
end
end
f:close()
end
end
return out
end
-- Proprietary vendor userspace: linked from the host by design and never
-- compared against a payload, because none exists or may exist.
local vendor_userspace = {
"^libnvidia", "^libGLX_nvidia%.so", "^libEGL_nvidia%.so", "^libGLESv[12]_nvidia%.so",
"^libcuda%.so", "^libnvcuvid%.so", "^libnvoptix%.so",
}
local function is_vendor_userspace(base)
for _, pat in ipairs(vendor_userspace) do
if base:match(pat) then return true end
end
return false
end
-- The host's own Mesa ICDs are the driver. A payload driver replaces them as
-- a whole through its own manifest; substituting one of their libraries would
-- mix two Mesa builds in one process.
local function is_host_driver(base)
return base:match("^libvulkan_") ~= nil
end
local function is_store_path(p)
for _, root in ipairs(xim_store_roots()) do
if p:sub(1, #root) == root then return true end
end
return false
end
-- `nm` from an installed toolchain payload, else from PATH, else nothing. The
-- comparison below is skipped and recorded when there is none; a farm that
-- cannot compare keeps the host copy rather than guessing.
local function find_tool(name)
for _, root in ipairs(xim_store_roots()) do
local f = io.popen(string.format(
[[ls -1 "%s"/xim-x-binutils/*/bin/%s "%s"/xim-x-gcc/*/bin/%s 2>/dev/null | sort -V | tail -1]],
root, name, root, name))
if f then
local hit = (f:read("l") or ""):gsub("[\r\n]+$", "")
f:close()
if hit ~= "" then return hit end
end
end
local f = io.popen(string.format([[command -v %s 2>/dev/null]], name))
if f then
local hit = (f:read("l") or ""):gsub("[\r\n]+$", "")
f:close()
if hit ~= "" then return hit end
end
return nil
end
-- THE MACHINE THE OBJECT WAS BUILT FOR, from the ELF header (`e_machine`,
-- two bytes at offset 18 on both little-endian classes this index targets).
-- The symbol test cannot see this: `nm` reads an x86_64 object on an aarch64
-- host perfectly well and reports a covering symbol set, so a store that holds
-- a foreign payload -- which is what an aarch64 machine gets today, since every
-- Linux payload in this index publishes one x86_64 artifact -- would otherwise
-- have that payload substituted into the farm and every dlopen would fail with
-- `wrong ELF class` at run time.
local function elf_machine(file)
local f = io.popen(string.format(
[[od -An -tu1 -j18 -N2 %s 2>/dev/null | tr -s " "]], sh_quote(file)))
if not f then return nil end
local line = (f:read("l") or ""):gsub("^%s+", ""):gsub("%s+$", "")
f:close()
local lo, hi = line:match("^(%d+) (%d+)$")
if not lo then return nil end
return tonumber(lo) + tonumber(hi) * 256
end
-- Both answers are required before the guard fires. An unreadable header --
-- a dangling farm link, a file the reader cannot open -- is not evidence of a
-- foreign machine, and reporting it as one would put a wrong reason in the
-- record; the symbol test below then rejects it for the reason that applies.
local function machines_differ(a, b)
local ma, mb = elf_machine(a), elf_machine(b)
return ma ~= nil and mb ~= nil and ma ~= mb
end
-- The versioned dynamic symbols a library defines, as a set. `name@@VERSION`
-- for a versioned symbol, so the GLIBCXX and CXXABI nodes of a C++ runtime
-- take part in the comparison exactly as the loader would apply them.
local function symbol_set(nm, lib)
local f = io.popen(string.format(
[[%s -D --defined-only --with-symbol-versions %s 2>/dev/null | awk '{print $NF}']],
sh_quote(nm), sh_quote(lib)))
if not f then return nil end
local set, n = {}, 0
for line in f:lines() do
local sym = line:gsub("[\r\n]+$", "")
if sym ~= "" then set[sym] = true; n = n + 1 end
end
f:close()
if n == 0 then return nil end
return set
end
-- PAYLOADS FIRST. Every farmed soname an installed payload also provides is
-- re-pointed at the payload when the payload's versioned symbol set covers the
-- host copy's. The direction matters and is what the symbol test decides: an
-- ICD linked against the host's libstdc++ needs every GLIBCXX node the host
-- copy has, which a NEWER payload provides and an older one does not. The
-- outcome is recorded per entry, so HOST-SURFACE.txt says why each library is
-- where it is rather than only where it points.
local function prefer_payloads(outdir)
local classes = {}
local nm = find_tool("nm")
local lsf = io.popen(string.format([[ls -1 "%s" 2>/dev/null]], outdir))
if not lsf then return classes end
local names = {}
for line in lsf:lines() do
local base = line:gsub("[\r\n]+$", "")
if base ~= "" then names[#names + 1] = base end
end
lsf:close()
local moved = 0
for _, base in ipairs(names) do
local link = path.join(outdir, base)
local rf = io.popen(string.format([[readlink -f "%s" 2>/dev/null]], link))
local target = ""
if rf then
target = (rf:read("l") or ""):gsub("[\r\n]+$", "")
rf:close()
end
local entry = { target = target, class = "" }
if is_vendor_userspace(base) then
entry.class = "vendor userspace; linked from the host by design"
elseif is_host_driver(base) then
entry.class = "host driver; a payload driver replaces it as a whole"
elseif is_store_path(target) then
entry.class = "payload"
else
local candidates = find_in_store(base)
local declared = PAYLOAD_PACKAGES[base]
if #candidates == 0 then
entry.class = declared
and ("host; " .. declared .. " is declared for this soname "
.. "and is not installed, so the declaration did not "
.. "take effect")
or "host; no installed payload provides this soname"
elseif not nm then
entry.class = "host; no nm to compare against " .. candidates[1]
else
local host_syms = symbol_set(nm, target)
-- The last reason any candidate was refused, so a farm that
-- keeps a host copy says why rather than only that it did.
local why = nil
for _, hit in ipairs(candidates) do
if machines_differ(hit, target) then
why = string.format(
"host; the payload %s is built for another machine", hit)
else
local pay_syms = symbol_set(nm, hit)
if not host_syms or not pay_syms then
why = "host; symbol tables unreadable, not compared against " .. hit
else
local missing = 0
for sym in pairs(host_syms) do
if not pay_syms[sym] then missing = missing + 1 end
end
if missing == 0 then
os.exec(string.format([[ln -sf "%s" "%s"]], hit, link))
entry.target = hit
entry.class = "payload; its symbol set covers the host copy " .. target
moved = moved + 1
why = nil
break
end
why = string.format(
"host; payload %s lacks %d symbol(s) the host copy defines",
hit, missing)
end
end
end
if why then entry.class = why end
end
end
classes[base] = entry
end
if moved > 0 then
log.info("compat.vulkan-runtime: %d farmed libraries re-pointed at installed payloads", moved)
end
return classes
end
-- THE FARM IS ALSO A SEED SET, AND THE HOST SURFACE DOES NOT GROW FOR IT.
--
-- The pattern list exists because a proprietary driver dlopens members of its
-- own family by name, which no `DT_NEEDED` walk can see. Having said that, this
-- package has to treat those members as reachable everywhere else too -- and it
-- did not: closure, gap-filling and the report all took the ICD manifests'
-- libraries, so the half of the farm that was never in doubt is the half that
-- got verified.
--
-- THE REASON RECORDED AGAINST SEEDING FROM THE FARM MEASURED A DIFFERENT SET.
-- It says closing over the farm pulled 64 libraries, GTK among them. That is
-- true of closing over every file the PATTERN matched: `libnvidia*.so.*` also
-- matches the driver's settings GUI. The farm is the pattern's matches MINUS
-- `never_farm_patterns`, and closing over THAT adds five sonames on this host
-- -- `libnvcuvid.so.1`, `libcrypto.so.3`, `libcrypto.so.1.1`, `libgbm.so.1`,
-- `libwayland-server.so.0` -- and no GTK, GLib, Pango or Cairo. Measured
-- 2026-09-10 against the installed farm, with the host `ldd`.
--
-- THE FARM REACHES THE HOST THROUGH A NAMED PACKAGE, NOT THROUGH
-- /usr/lib. What a farmed member needs is answered in this order:
--
-- * an installed payload publishes it -> link the payload's copy.
-- `xim:nvidia-video-host-link` covers `libnvcuvid.so.1`, which is
-- what the two encode/optical-flow members need. Both are declared
-- in `xpm.linux.deps` above, so the reach is visible in the
-- recipe rather than discovered at install time.
-- * nothing does, and nothing can -> named in UNSERVED with the
-- reason, and linked into a directory this package never creates.
-- * anything else -> a warning naming it.
--
-- There is no branch that harvests a file from /usr/lib for a farmed
-- member. A library that cannot be redistributed still comes from the
-- host, but it comes through a `*-host-link` sentinel that owns exactly
-- that question -- the shape `libcuda-host-link` and
-- `nvidia-gl-host-link` already have -- so each consumer declares the
-- host reach it actually has instead of inheriting an open one.
--
-- The ICD closure below is left exactly as it is: it is what makes a
-- driver load at all, and narrowing it is a separate question from
-- completing the members the vendor pattern swept in.
local function farm_members(outdir)
local out = {}
local f = io.popen(string.format([[ls -1 "%s" 2>/dev/null]], outdir))
if not f then return out end
for line in f:lines() do
local base = line:gsub("[\r\n]+$", "")
if base ~= "" then out[#out + 1] = path.join(outdir, base) end
end
f:close()
return out
end
-- WHAT A MEMBER NEEDS IS READ FROM THE MEMBER, NOT FROM A LOADER.
--
-- `ldd` answers "can this resolve HERE", and here includes the host's default
-- directories. A soname the host happens to carry therefore reads as resolved
-- and is never recorded -- while the consumer, whose search path is this farm
-- and not the host, cannot load it. Measured 2026-09-10: with `ldd` supplying
-- the host directories, the two `libnvidia-pkcs11` providers' `libcrypto`
-- needs were invisible to this pass and were reported by mcpp one layer up,
-- from the same directory.
--
-- `readelf -d` answers what the FILE says, and membership is decided against
-- this directory alone. That is the question mcpp asks, and asking a different
-- one is how a farm's own check passes while its consumer's does not.
local function unresolved_against_farm(outdir)
local readelf = find_tool("readelf")
-- A MISSING TOOL IS NOT AN EMPTY ANSWER.
--
-- Returning `{}` here would report "no member needs anything this farm
-- lacks", which is the reading a fully closed farm produces -- so the one
-- environment where this pass cannot run would be indistinguishable from
-- the one where it ran and found nothing. That is the confusion this whole
-- change exists to remove, one layer down, in the tool lookup.
if not readelf then
log.warn("compat.vulkan-runtime: readelf was not found, so the farm's own members were "
.. "not checked. This is NOT the same as finding no gaps: "
.. "install xim:binutils, or read HOST-SURFACE.txt with the "
.. "knowledge that it is incomplete.")
return {}
end
local have, members = {}, {}
local lsf = io.popen(string.format([[ls -1 "%s" 2>/dev/null]], outdir))
if not lsf then return {} end
for line in lsf:lines() do
local b = line:gsub("[\r\n]+$", "")
if b ~= "" then have[b] = true; members[#members + 1] = b end
end
lsf:close()
local out, seen = {}, {}
for _, base in ipairs(members) do
local f = io.popen(string.format(
[[%s -d %s 2>/dev/null | sed -n 's/.*(NEEDED).*\[\(.*\)\]/\1/p']],
sh_quote(readelf), sh_quote(path.join(outdir, base))))
if f then
for line in f:lines() do
local n = line:gsub("[\r\n]+$", "")
-- AN ABSOLUTE `DT_NEEDED` NEVER GOES THROUGH A SEARCH PATH.
--
-- The loader opens it directly, so this farm can neither serve
-- it nor honestly record it as unserved -- and treating it as a
-- soname produces a lookup for a name with slashes in it and,
-- worse, an `unserved` link whose name is a path. Measured on
-- this farm: four members -- the glvnd vendor entries
-- `libEGL_nvidia`, `libGLESv1_CM_nvidia`, `libGLESv2_nvidia`
-- and `libGLX_nvidia` -- name `/lib/x86_64-linux-gnu/...`
-- outright. They are a host reach that bypasses everything this
-- package arranges, which is worth knowing and is not this
-- pass's to answer.
if n:sub(1, 1) == "/" then
goto continue
end
if n ~= "" and not have[n] and not never_farm[n] and not seen[n] then
seen[n] = true
out[#out + 1] = n
end
::continue::
end
f:close()
end
end
return out
end
-- EVERY SONAME A FARMED MEMBER NEEDS HAS AN ANSWER, AND NONE OF THEM IS
-- SILENCE. Four classes, in the order they are tried:
--
-- * a package this ecosystem publishes -- declared in `xpm.linux.deps`,
-- mapped in PAYLOAD_PACKAGES, and taken from the installed payload.
-- * proprietary vendor userspace -- also a package, and deliberately so:
-- `xim:nvidia-video-host-link` owns the one question "where is the host's
-- `libnvcuvid.so.1`". The library still comes from the host, because it is
-- in ABI lockstep with a kernel module and is not redistributable, but the
-- reach is named and declared instead of open.
-- * neither, and it cannot become one -- named in UNSERVED below WITH THE
-- REASON, and linked into a directory this package never creates, so the
-- state reads as "considered and not served here".
-- * anything else -- a warning naming it. There is deliberately no branch
-- that quietly absorbs an unknown soname: what this farm takes from
-- outside the ecosystem has to be a list somebody wrote, not a residue.
-- Empty, and the table stays so that the day something lands here somebody has
-- to write down why it cannot be a package. The warning below is what makes
-- leaving it blank impossible to do by accident. `libcrypto.so.1.1` was the
-- only entry and left with the PKCS#11 member that asked for it.
local UNSERVED = {}
local function link_runtime_libs(outdir)
os.mkdir(outdir)
for _, dir in ipairs(candidate_dirs()) do
for _, pattern in ipairs(host_vulkan_patterns) do
os.exec(
"for lib in " .. sh_quote(dir) .. "/" .. pattern ..
"; do [ -e \"$lib\" ] || continue; " ..
"ln -sf \"$lib\" " .. sh_quote(outdir) .. "/\"$(basename \"$lib\")\"; " ..
"done"
)
end
end
-- One `case` with every glob, and the globs come from the table. The
-- previous shape bound `pat` and then ignored it.
do
local removed = 0
for _, pat in ipairs(never_farm_patterns) do
local f = io.popen(string.format(
[[for lib in "%s"/*; do case "$(basename "$lib")" in %s) ]] ..
[[rm -f "$lib" && echo x;; esac; done | wc -l]], outdir, pat))
if f then
removed = removed + (tonumber((f:read("l") or "0")) or 0)
f:close()
end
end
if removed > 0 then
log.info("compat.vulkan-runtime: %d farmed files this runtime "
.. "cannot reach were removed", removed)
end
end
local dirs = candidate_dirs()
-- The ICD libraries themselves. A manifest names its driver by bare
-- soname or by path; the loader dlopens the former by name, which only
-- resolves through this directory, and the latter's dependencies still do.
for _, seed in ipairs(icd_seed_libraries(dirs)) do
local base = seed:match("([^/]+)$")
if base and not os.isfile(path.join(outdir, base)) then
os.exec(string.format([[ln -sf "%s" "%s"]], seed, path.join(outdir, base)))
end
end
local n = close_over_needed(outdir, dirs)
if n > 0 then
log.info("compat.vulkan-runtime: %d transitive libraries closed over", n)
end
local classes = prefer_payloads(outdir)
-- Gap-filling, then the record. Both read the FARM, so what the report
-- describes is every library a consumer can reach through this directory
-- rather than only the ones an ICD manifest happens to name.
local seeds = icd_seed_libraries(dirs)
local filled, missing = {}, {}
for _, soname in ipairs(unresolved_against_farm(outdir)) do
-- The FIRST candidate, and a list is what find_in_store returns since
-- 2026.09.07. There is no host copy to compare against here -- this
-- pass exists precisely for the names the host cannot resolve at all --
-- so coverage cannot be the criterion, and the newest copy of a soname
-- nothing else provides is the only answer available.
local candidates = find_in_store(soname)
local hit = candidates[#candidates]
if hit then