|
| 1 | +-- compat.sycl-runtime -- put the SYCL runtime on an mcpp binary's runtime |
| 2 | +-- search path. |
| 3 | +-- |
| 4 | +-- WHAT IT FIXES. An mcpp-built program runs under mcpp's OWN loader, so a |
| 5 | +-- soname that is not in its RPATH is not found at all: the host's /usr/lib is |
| 6 | +-- never consulted. A program whose device half was compiled by the dpcpp |
| 7 | +-- payload links `-lsycl`, which the rule package satisfies at LINK time from |
| 8 | +-- the payload's own directory, and then fails the runtime closure check: |
| 9 | +-- |
| 10 | +-- libsycl.so.9 not found on the search path this artifact will actually use. |
| 11 | +-- Its PT_INTERP is a private loader, so the host's /usr/lib is NOT consulted |
| 12 | +-- |
| 13 | +-- `runtime.library_dirs` below puts a package-owned directory on that path -- |
| 14 | +-- the same mechanism compat.cuda-runtime and compat.vulkan-runtime use for the |
| 15 | +-- same reason, and for the same class of library: one a program reaches |
| 16 | +-- through the loader rather than through a header. |
| 17 | +-- |
| 18 | +-- WHY EVERY VERSIONED SONAME AND NOT JUST libsycl. |
| 19 | +-- |
| 20 | +-- SYCL's device support is a chain of dlopens: `libsycl.so.9` loads |
| 21 | +-- `libur_loader.so.0`, which loads one `libur_adapter_*.so.0` per back end, |
| 22 | +-- and those need `libumf.so.1`. The loader finds an adapter by looking beside |
| 23 | +-- ITSELF, and `$ORIGIN` resolves to the directory of the symlink rather than |
| 24 | +-- of its target -- so a farm holding `libsycl.so.9` alone would load, then |
| 25 | +-- enumerate no devices, which is the failure that looks like "this machine has |
| 26 | +-- no GPU". Every versioned soname in the payload is linked, so the whole chain |
| 27 | +-- resolves inside one directory. |
| 28 | +-- |
| 29 | +-- ONLY VERSIONED SONAMES. mcpp puts `runtime.library_dirs` on the LINK line as |
| 30 | +-- well as the runtime path, so an unversioned `libsycl.so` here would be found |
| 31 | +-- by `-lsycl` and would bind the build to this farm instead of to the payload |
| 32 | +-- the project declared. A versioned soname is invisible to the linker and is |
| 33 | +-- exactly what dlopen asks for -- the reasoning compat.cuda-runtime records |
| 34 | +-- for `libcuda.so.1`. |
| 35 | +-- |
| 36 | +-- THE PROBE IS NOT REPEATED HERE. Where the SYCL runtime is, is a question the |
| 37 | +-- `xim:dpcpp` payload already answers by existing; this package declares the |
| 38 | +-- edge and reads the directory back. `xpm.<platform>.deps` rather than the |
| 39 | +-- package's own `[xlings]`, because mcpp materialises `[xlings] deps` for the |
| 40 | +-- ROOT project only and this must resolve when the package itself installs. |
| 41 | +-- |
| 42 | +-- NOTHING IS REQUIRED. A machine with no GPU is a legitimate configuration -- |
| 43 | +-- every runner in this repository is one. The SYCL runtime still loads, still |
| 44 | +-- enumerates whatever it finds, and a program that needs a device says so |
| 45 | +-- itself. |
| 46 | +package = { |
| 47 | + spec = "1", |
| 48 | + namespace = "compat", |
| 49 | + name = "sycl-runtime", |
| 50 | + description = "SYCL runtime adapter for mcpp Linux applications (Intel DPC++ payload)", |
| 51 | + licenses = {"Apache-2.0"}, -- the recipe; the payload is Apache-2.0 WITH LLVM-exception |
| 52 | + repo = "https://github.com/mcpp-community/mcpp-index", |
| 53 | + type = "package", |
| 54 | + |
| 55 | + xpm = { |
| 56 | + linux = { |
| 57 | + -- The install-time edge. Materialised when THIS package installs, |
| 58 | + -- which is what makes the payload's directory exist by the time |
| 59 | + -- install() below reads it. |
| 60 | + -- `xim:zlib` is not decoration. `libur_loader.so.0` needs |
| 61 | + -- `libz.so.1`, which the payload does not carry, and the farm's |
| 62 | + -- RUNPATH is the only place the loader looks for it. On a |
| 63 | + -- developer machine that had zlib installed for other reasons the |
| 64 | + -- omission was invisible; a fresh runner reported |
| 65 | + -- `FAIL libsycl.so.9: libz.so.1: cannot open shared object file`. |
| 66 | + deps = { "xim:dpcpp@7.1.0", "xim:zlib" }, |
| 67 | + ["2026.09.06"] = { |
| 68 | + -- Nothing downloaded matters: the content is the set of |
| 69 | + -- symlinks install() creates. A stable, tiny anchor keeps the |
| 70 | + -- xpm entry well-formed, the same trick compat.cuda-runtime |
| 71 | + -- and compat.vulkan-runtime use. |
| 72 | + url = "https://raw.githubusercontent.com/intel/llvm/v7.1.0/sycl/LICENSE.TXT", |
| 73 | + sha256 = "410f3a23b4bbacbd246310d8c014a20af18cfc8c0d740ddf0f673ea20894da9c", |
| 74 | + }, |
| 75 | + ["latest"] = { ref = "2026.09.06" }, |
| 76 | + }, |
| 77 | + }, |
| 78 | + |
| 79 | + mcpp = { |
| 80 | + language = "c++23", |
| 81 | + import_std = false, |
| 82 | + c_standard = "c11", |
| 83 | + sources = { "mcpp_generated/sycl_runtime_empty.c" }, |
| 84 | + targets = { ["sycl_runtime"] = { kind = "lib" } }, |
| 85 | + deps = {}, |
| 86 | + runtime = { |
| 87 | + library_dirs = { "mcpp_generated/sycl_runtime/lib" }, |
| 88 | + capabilities = { "sycl.runtime" }, |
| 89 | + provides = { "sycl.runtime" }, |
| 90 | + }, |
| 91 | + }, |
| 92 | +} |
| 93 | + |
| 94 | +import("xim.libxpkg.pkginfo") |
| 95 | +import("xim.libxpkg.log") |
| 96 | + |
| 97 | +-- The payload's install directory. |
| 98 | +-- |
| 99 | +-- `pkginfo.install_dir` scans only the member-local xpkgs roots; a dependency |
| 100 | +-- installed into the shared registry cache is invisible to it and comes back |
| 101 | +-- nil, so the known roots are tried before giving up. The same fallback |
| 102 | +-- compat.cuda-runtime needs, for the same reason. |
| 103 | +local function payload_dir() |
| 104 | + local dir = pkginfo.install_dir("xim:dpcpp", "7.1.0") |
| 105 | + if dir then return dir end |
| 106 | + local roots = {} |
| 107 | + local pfx = pkginfo.install_dir() |
| 108 | + if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end |
| 109 | + local home = (os.getenv and os.getenv("XLINGS_HOME")) or "" |
| 110 | + if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end |
| 111 | + roots[#roots + 1] = path.join(home, "data/xpkgs") |
| 112 | + for _, root in ipairs(roots) do |
| 113 | + for _, ns in ipairs({ "xim-x-dpcpp", "local-x-dpcpp" }) do |
| 114 | + local cand = path.join(root, ns, "7.1.0") |
| 115 | + if os.isdir(cand) then return cand end |
| 116 | + end |
| 117 | + end |
| 118 | + return nil |
| 119 | +end |
| 120 | + |
| 121 | +-- THE C LIBRARY'S COMPATIBILITY STUBS HAVE TO BE FARMED TOO. |
| 122 | +-- |
| 123 | +-- The payload's shared objects carry `RUNPATH = $ORIGIN`, and a non-empty |
| 124 | +-- RUNPATH on an intermediate library TURNS OFF the executable's inherited |
| 125 | +-- DT_RPATH for that library's own dependencies (mcpp#460). So the artifact's |
| 126 | +-- search path, which reaches mcpp's glibc payload, is not consulted when the |
| 127 | +-- loader looks for what `libsycl.so.9` needs, and the program dies before |
| 128 | +-- `main`: |
| 129 | +-- |
| 130 | +-- error while loading shared libraries: libdl.so.2: cannot open shared |
| 131 | +-- object file: No such file or directory |
| 132 | +-- |
| 133 | +-- `$ORIGIN` is the SYMLINK's directory, not the target's, so linking the stubs |
| 134 | +-- into this farm beside the payload's own symlinks is what makes a farm of |
| 135 | +-- links sufficient and a copy of the payload unnecessary. |
| 136 | +-- |
| 137 | +-- Only the libraries glibc 2.34 MERGED INTO libc are farmed. They are empty |
| 138 | +-- compatibility stubs, so which payload version answers does not matter, and |
| 139 | +-- nothing else is loading them: `libc.so.6`, `libm.so.6`, `libgcc_s.so.1` and |
| 140 | +-- `libstdc++.so.6` are already loaded on the executable's own behalf by the |
| 141 | +-- time `libsycl.so.9` asks, and are found by soname without a search. Farming |
| 142 | +-- those would risk a second C library in one address space, which is the one |
| 143 | +-- failure worse than this one. |
| 144 | +-- |
| 145 | +-- This is the third copy of this helper in this index (compat.cudart and |
| 146 | +-- compat.curand carry the other two) and it is duplicated rather than shared |
| 147 | +-- for the reason those record: a recipe is loaded in a sandbox that has no |
| 148 | +-- module of this index's own to import. |
| 149 | +local function farm_libc_stubs(dst, payload) |
| 150 | + -- The three glibc 2.34 merged stubs, AND the two GCC runtime libraries. |
| 151 | + -- |
| 152 | + -- compat.cudart farms the stubs alone and says why the C++ runtime is not |
| 153 | + -- among them: for a CUDA program `libstdc++.so.6` is already loaded on the |
| 154 | + -- executable's own behalf by the time the component asks. That reasoning |
| 155 | + -- does not carry here. `libsycl.so.9` needs libstdc++ itself, and an mcpp |
| 156 | + -- artifact links libc++ -- so unless the consumer happens to have named |
| 157 | + -- libstdc++ on its own link line, nothing has loaded it and the farm's |
| 158 | + -- RUNPATH is the only place the loader looks: |
| 159 | + -- |
| 160 | + -- FAIL libsycl.so.9: libstdc++.so.6: cannot open shared object file |
| 161 | + -- |
| 162 | + -- Farming it is safe for the reason farming libc would not be: there is |
| 163 | + -- exactly one libstdc++ in this ecosystem, `xim:gcc-runtime`'s, so a |
| 164 | + -- consumer that also links it resolves the same file under the same soname |
| 165 | + -- and it is loaded once. |
| 166 | + local roots = {} |
| 167 | + -- FIRST, THE STORE THE PAYLOAD ITSELF CAME FROM. The other roots below are |
| 168 | + -- derived from the environment, and this package installs into a |
| 169 | + -- PROJECT-LOCAL store while its payload is resolved from the shared |
| 170 | + -- registry -- so `install_dir`'s grandparent holds this package alone and |
| 171 | + -- $HOME may be remapped. Measured: with the environment-derived roots |
| 172 | + -- alone the stub search found nothing, and the program died on |
| 173 | + -- `libdl.so.2` with a farm that otherwise looked complete. The payload's |
| 174 | + -- own directory cannot be wrong: glibc is its sibling by construction. |
| 175 | + if payload then roots[#roots + 1] = path.directory(path.directory(payload)) end |
| 176 | + local pfx = pkginfo.install_dir() |
| 177 | + if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end |
| 178 | + local home = (os.getenv and os.getenv("XLINGS_HOME")) or "" |
| 179 | + if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.xlings" end |
| 180 | + roots[#roots + 1] = path.join(home, "data/xpkgs") |
| 181 | + roots[#roots + 1] = path.join((os.getenv and os.getenv("HOME")) or "", |
| 182 | + ".mcpp/registry/data/xpkgs") |
| 183 | + local wanted = { |
| 184 | + { pkg = "xim-x-glibc", names = { "librt.so.1", "libpthread.so.0", "libdl.so.2" } }, |
| 185 | + { pkg = "xim-x-gcc-runtime", names = { "libstdc++.so.6", "libgcc_s.so.1" } }, |
| 186 | + -- zlib ships under lib/ rather than lib64/, so the search below tries |
| 187 | + -- both rather than assuming one layout per package. |
| 188 | + { pkg = "xim-x-zlib", names = { "libz.so.1" } }, |
| 189 | + } |
| 190 | + local found = 0 |
| 191 | + for _, group in ipairs(wanted) do |
| 192 | + for _, name in ipairs(group.names) do |
| 193 | + if not os.isfile(path.join(dst, name)) then |
| 194 | + for _, root in ipairs(roots) do |
| 195 | + local f = io.popen(string.format( |
| 196 | + [[ls -1d "%s"/%s/*/lib64/%s "%s"/%s/*/lib/%s 2>/dev/null ]] |
| 197 | + .. [[| sort -V | tail -1]], |
| 198 | + root, group.pkg, name, root, group.pkg, name)) |
| 199 | + local hit = f and (f:read("l") or "") or "" |
| 200 | + if f then f:close() end |
| 201 | + if hit ~= "" then |
| 202 | + os.exec(string.format([[ln -sfn "%s" "%s"]], hit, path.join(dst, name))) |
| 203 | + found = found + 1 |
| 204 | + break |
| 205 | + end |
| 206 | + end |
| 207 | + end |
| 208 | + end |
| 209 | + end |
| 210 | + return found |
| 211 | +end |
| 212 | + |
| 213 | +function install() |
| 214 | + os.tryrm(pkginfo.install_dir()) |
| 215 | + os.mkdir(pkginfo.install_dir()) |
| 216 | + |
| 217 | + local generated = path.join(pkginfo.install_dir(), "mcpp_generated") |
| 218 | + os.mkdir(generated) |
| 219 | + io.writefile(path.join(generated, "sycl_runtime_empty.c"), |
| 220 | + "int mcpp_compat_sycl_runtime_anchor(void) { return 0; }\n") |
| 221 | + |
| 222 | + local outdir = path.join(generated, "sycl_runtime", "lib") |
| 223 | + os.mkdir(outdir) |
| 224 | + |
| 225 | + local src = payload_dir() |
| 226 | + if not src then |
| 227 | + -- Reported, not fatal. The farm is empty, the link still succeeds |
| 228 | + -- because the rule package puts the payload's own directory on the |
| 229 | + -- link line, and the runtime closure check reports the missing soname |
| 230 | + -- by name -- which is a better message than this package could write. |
| 231 | + log.warn("compat.sycl-runtime: the xim:dpcpp payload was not found; " |
| 232 | + .. "the runtime library directory will be empty") |
| 233 | + return true |
| 234 | + end |
| 235 | + |
| 236 | + -- `io.popen` rather than `os.files`: the latter is not available in the |
| 237 | + -- recipe sandbox (`attempt to call a nil value`), which the llvm and |
| 238 | + -- cuda-cccl recipes record the same way. |
| 239 | + local n = 0 |
| 240 | + local p = io.popen("ls -1 " .. path.join(src, "lib") .. " 2>/dev/null") |
| 241 | + if p then |
| 242 | + for line in p:lines() do |
| 243 | + -- `libfoo.so.N` and `libfoo.so.N.M.P`, never a bare `libfoo.so`: |
| 244 | + -- see the header for why an unversioned name here would reach the |
| 245 | + -- linker. |
| 246 | + -- `libfoo.so.N`, `libfoo.so.N.M.P` and upstream's `.so.9.0.0-0`, |
| 247 | + -- but not the `-gdb.py` sidecars that sit beside them and match a |
| 248 | + -- looser test for a versioned soname. |
| 249 | + if line:match("%.so%.%d") and not line:match("%.py$") then |
| 250 | + os.exec("ln -sf " .. path.join(src, "lib", line) .. " " |
| 251 | + .. path.join(outdir, line)) |
| 252 | + n = n + 1 |
| 253 | + end |
| 254 | + end |
| 255 | + p:close() |
| 256 | + end |
| 257 | + if n == 0 then |
| 258 | + log.warn("compat.sycl-runtime: the payload at %s has no versioned " |
| 259 | + .. "library in lib/; the farm is empty", src) |
| 260 | + return true |
| 261 | + end |
| 262 | + |
| 263 | + local stubs = farm_libc_stubs(outdir, src) |
| 264 | + log.info("compat.sycl-runtime: %d versioned sonames from %s, %d C-library stubs", |
| 265 | + n, src, stubs) |
| 266 | + return true |
| 267 | +end |
0 commit comments