Skip to content

Commit 0d3cc8e

Browse files
committed
feat(compat.openblas): new package + eigen backend-openblas feature
compat.openblas builds OpenBLAS 0.3.33 from source via its xpkg install() hook (build-dep xim:make), BLAS-only (NO_FORTRAN/NO_LAPACK, TARGET=GENERIC, static), producing libopenblas.a. It `provides = ["blas"]` and exposes <cblas.h>, so it serves both as the `blas` capability provider for Eigen and as a standalone BLAS. CN mirror: gitcode mcpp-res/openblas (sha256-verified). compat.eigen gains a `backend-openblas` feature: `implies use_blas` (defines EIGEN_USE_BLAS + requires the `blas` capability) and pulls compat.openblas as a feature-dep. The resolver binds the single `blas` provider automatically and the provider's -lopenblas links. Requires mcpp >= 0.0.72: EIGEN_USE_BLAS is an interface define that must reach the consumer's TUs (Eigen is header-only); interface-define propagation landed in 0.0.72. Validated end-to-end: a consumer of compat.eigen[backend-openblas] builds, links libopenblas.a, pulls OpenBLAS dgemm_ (not Eigen's built-in GEMM), and runs.
1 parent 570c2e9 commit 0d3cc8e

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

pkgs/c/compat.eigen.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,20 @@ package = {
128128
-- CONSUMER: delegate Eigen's kernels to an external BLAS / LAPACK.
129129
["use_blas"] = { defines = { "EIGEN_USE_BLAS" }, requires = { "blas" } },
130130
["use_lapacke"] = { defines = { "EIGEN_USE_LAPACKE" }, requires = { "lapack" } },
131+
-- BACKEND (Feature System v2, mcpp >= 0.0.72): one-liner that PULLS an
132+
-- external BLAS provider AND turns on the consumer switch.
133+
-- `implies use_blas` defines EIGEN_USE_BLAS + requires the `blas`
134+
-- capability; `deps compat.openblas` pulls the provider that
135+
-- `provides=["blas"]`, so the resolver binds it automatically. Do NOT
136+
-- combine with `eigen_blas` (provider vs consumer are exclusive).
137+
-- Requires mcpp >= 0.0.72: EIGEN_USE_BLAS is an INTERFACE define that
138+
-- must reach the consumer's TUs (Eigen is header-only) — interface-
139+
-- define propagation landed in 0.0.72. On 0.0.71 the dep is still
140+
-- pulled and linked, but Eigen keeps its built-in GEMM in the consumer.
141+
["backend-openblas"] = {
142+
implies = { "use_blas" },
143+
deps = { ["compat.openblas"] = "0.3.33" },
144+
},
131145
-- Pure package-owned define knob.
132146
["mpl2only"] = { defines = { "EIGEN_MPL2_ONLY" } },
133147
},

pkgs/c/compat.openblas.lua

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
-- compat.openblas — OpenBLAS built from source as a portable, BLAS-only static
2+
-- library that satisfies the `blas` capability for consumers (e.g. Eigen's
3+
-- `use_blas` feature, which defines EIGEN_USE_BLAS and links the Fortran-ABI
4+
-- BLAS symbols sgemm_/dgemm_/ddot_/…) AND is usable standalone via <cblas.h>.
5+
--
6+
-- OpenBLAS builds through its own GNU Make system (getarch config generation +
7+
-- per-arch kernels), which does not fit mcpp's "list the .c files" model. The
8+
-- xpkg install() hook runs that Make build (build-dep `xim:make`) and lays the
9+
-- lib + headers under the install dir. Built BLAS-ONLY (NO_LAPACK — LAPACK
10+
-- needs Fortran) with TARGET=GENERIC (portable C kernels), static (NO_SHARED).
11+
--
12+
-- How mcpp is made to run install(): mcpp runs an xpkg's install() hook when its
13+
-- build needs a source that install() PRODUCES (the same way compat.xcb's
14+
-- c_client.py generates xproto.c, which mcpp then compiles). So instead of a
15+
-- mcpp `generated_files` anchor (which would make mcpp self-sufficient and skip
16+
-- install()), install() WRITES the anchor TU itself. mcpp extracts the tarball,
17+
-- finds the anchor missing, runs install() — which builds the lib, writes the
18+
-- headers, and emits the anchor — then compiles the anchor and links the lib
19+
-- (`-Llib -lopenblas`, with `-Llib` rewritten to <verdir>/lib).
20+
--
21+
-- Platforms: linux/macosx build from source via Make. Windows (no upstream Make
22+
-- path; OpenBLAS ships prebuilt zips) is a follow-up.
23+
package = {
24+
spec = "1",
25+
namespace = "compat",
26+
name = "compat.openblas",
27+
description = "OpenBLAS — optimized BLAS, built from source (BLAS-only, no Fortran/LAPACK)",
28+
licenses = {"BSD-3-Clause"},
29+
repo = "https://github.com/OpenMathLib/OpenBLAS",
30+
type = "package",
31+
32+
xpm = {
33+
linux = {
34+
deps = { "xim:make@latest" },
35+
["0.3.33"] = {
36+
url = {
37+
GLOBAL = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.33/OpenBLAS-0.3.33.tar.gz",
38+
CN = "https://gitcode.com/mcpp-res/openblas/releases/download/0.3.33/OpenBLAS-0.3.33.tar.gz",
39+
},
40+
sha256 = "6761af1d9f5d353ab4f0b7497be2643313b36c8f31caec0144bfef198e71e6ab",
41+
},
42+
},
43+
macosx = {
44+
deps = { "xim:make@latest" },
45+
["0.3.33"] = {
46+
url = {
47+
GLOBAL = "https://github.com/OpenMathLib/OpenBLAS/releases/download/v0.3.33/OpenBLAS-0.3.33.tar.gz",
48+
CN = "https://gitcode.com/mcpp-res/openblas/releases/download/0.3.33/OpenBLAS-0.3.33.tar.gz",
49+
},
50+
sha256 = "6761af1d9f5d353ab4f0b7497be2643313b36c8f31caec0144bfef198e71e6ab",
51+
},
52+
},
53+
},
54+
55+
mcpp = {
56+
language = "c++23",
57+
import_std = false,
58+
c_standard = "c11",
59+
-- The anchor is NOT a generated_files entry: it is emitted by install()
60+
-- so mcpp must run install() (which also builds the lib) before it can
61+
-- compile this source. include/ + lib/ are produced by `make install`.
62+
sources = { "mcpp_openblas_anchor.c" },
63+
targets = { ["openblas"] = { kind = "lib" } },
64+
include_dirs = { "include" },
65+
ldflags = { "-Llib", "-lopenblas" },
66+
provides = { "blas" },
67+
deps = { },
68+
},
69+
}
70+
71+
import("xim.libxpkg.pkginfo")
72+
import("xim.libxpkg.log")
73+
74+
local function sh_quote(value)
75+
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
76+
end
77+
78+
local function resolve_make()
79+
local mk = pkginfo.build_dep("xim:make") or pkginfo.build_dep("make")
80+
if mk and mk.bin then
81+
local cand = path.join(mk.bin, "make")
82+
if os.isfile(cand) then return cand end
83+
end
84+
return "make"
85+
end
86+
87+
-- The Make build runs inside the install dir and writes a log there (xim's
88+
-- interface mode suppresses subprocess stdout, so an on-disk log is the only way
89+
-- to inspect a failed compile after the fact).
90+
local function _install_impl()
91+
-- The fetched tarball unpacks to OpenBLAS-<ver>/ beside the archive.
92+
local ifile = pkginfo.install_file()
93+
local srcroot = ifile and tostring(ifile):replace(".tar.gz", "")
94+
or ("OpenBLAS-" .. pkginfo.version())
95+
if not os.isdir(srcroot) then
96+
srcroot = "OpenBLAS-" .. pkginfo.version()
97+
end
98+
99+
-- Move the unpacked source into the install dir and build in place (the
100+
-- compat.xcb pattern — the extracted srcroot is transient). `os.cd` is the
101+
-- only directory primitive xim's restricted Lua exposes here (no os.curdir
102+
-- / os.files / os.trymkdir), so paths are formed explicitly via path.join.
103+
local prefix = pkginfo.install_dir()
104+
os.tryrm(prefix)
105+
os.mv(srcroot, prefix)
106+
os.cd(prefix)
107+
108+
-- BLAS-only, portable C kernels, static, single-threaded — no Fortran/LAPACK
109+
-- (those need a Fortran compiler). CC is pinned to gcc for a stable C ABI
110+
-- with the consumer's toolchain. The Make build-dep (`xim:make`) provides a
111+
-- musl-static GNU Make; fall back to PATH `make` if unresolved.
112+
local make = resolve_make()
113+
local jobs = (os.default_njob and os.default_njob()) or 4
114+
local flags = "TARGET=GENERIC NO_FORTRAN=1 NO_LAPACK=1 NO_SHARED=1 "
115+
.. "USE_THREAD=0 USE_OPENMP=0 CC=gcc"
116+
local logf = path.join(prefix, "mcpp_openblas_build.log")
117+
os.exec(string.format("bash -c %s", sh_quote(string.format(
118+
"cd %s && %s -j%d %s libs > %s 2>&1",
119+
sh_quote(prefix), make, jobs, flags, sh_quote(logf)))))
120+
os.exec(string.format("bash -c %s", sh_quote(string.format(
121+
"cd %s && %s %s PREFIX=%s install >> %s 2>&1",
122+
sh_quote(prefix), make, flags, sh_quote(prefix), sh_quote(logf)))))
123+
124+
-- Materialise lib/libopenblas.a. `make install` lays a versioned archive +
125+
-- a `libopenblas.a` symlink under lib/; if the symlink is absent, copy the
126+
-- (root- or lib-) built archive there. xim's Lua has no os.files glob, so
127+
-- the candidate names are enumerated explicitly.
128+
local libdir = path.join(prefix, "lib")
129+
local target_a = path.join(libdir, "libopenblas.a")
130+
if not os.isfile(target_a) then
131+
local versioned = "libopenblas_generic-r" .. pkginfo.version() .. ".a"
132+
local candidates = {
133+
path.join(prefix, "libopenblas.a"),
134+
path.join(libdir, versioned),
135+
path.join(prefix, versioned),
136+
}
137+
local picked
138+
for _, c in ipairs(candidates) do
139+
if os.isfile(c) then picked = c; break end
140+
end
141+
if not picked then
142+
log.error("compat.openblas: build produced no libopenblas archive "
143+
.. "(see %s)", logf)
144+
return false
145+
end
146+
os.cp(picked, target_a)
147+
end
148+
149+
-- Emit the anchor TU mcpp compiles. Its absence after extraction is what
150+
-- makes mcpp run this install() before the build (same trigger as
151+
-- compat.xcb's generated xproto.c); building it here also produces the lib.
152+
io.writefile(path.join(prefix, "mcpp_openblas_anchor.c"),
153+
"int mcpp_compat_openblas_anchor(void) { return 0; }\n")
154+
return true
155+
end
156+
157+
function install()
158+
local ok, err = pcall(_install_impl)
159+
if not ok then
160+
log.error("compat.openblas install() failed: %s", tostring(err))
161+
return false
162+
end
163+
return true
164+
end

0 commit comments

Comments
 (0)