diff --git a/.agents/docs/2026-10-14-add-gsl-plan.md b/.agents/docs/2026-10-14-add-gsl-plan.md new file mode 100644 index 00000000..458c7053 --- /dev/null +++ b/.agents/docs/2026-10-14-add-gsl-plan.md @@ -0,0 +1,46 @@ +# 新增 GSL 收录(compat,install() 构建) + +## 1. 来源与形态 + +- 收录 GNU Scientific Library 2.8,官方站点列出的当前稳定版本。 +- 采用 GNU 源码归档 `gsl-2.8.tar.gz`,SHA-256 为 + `6a99eeed15632c6354895b1dd542ed5a855c0f15d9ad1326c6fe2b2c9e423190`。 +- GSL 2.8 是 autotools 工程:`configure` 生成 `config.h`,顶层构建再通过 libtool + 组合 `libgsl.a` 和 `libgslcblas.a`。其源码树包含大量相互依赖的子库和测试源,不能用 + 一个宽泛的 `sources` glob 安全表达生产库边界。 +- 描述符因此采用与 `compat.openblas`/`compat.openssl` 相同的 `install()` 触发方式: + 构建期运行上游 `configure`、`make`、`make install`,然后由 hook 写入 anchor TU。 + +## 2. 描述符设计 + +- 身份为 `compat:gsl`,文件为 `pkgs/c/compat.gsl.lua`。 +- Linux 使用官方源码包和 `xim:make@latest`,macOS 使用系统自带的 GNU Make; + 关闭 shared、启用 static,产物安装到包的 `include/` 与 `lib/`。 +- hook 显式使用 `CFLAGS=-std=c11`。GSL 2.8 的 configure 探测包含旧式 C + 兼容性代码,在 GCC 16 的默认 C23 模式下会被拒绝;固定到 C11 同时符合 + GSL 的 ANSI C 构建要求,并保持 Linux CI 与本地工具链一致。 +- Linux hook 显式选择 `/usr/bin/gcc`,而不是 PATH 中可能由 xlings 注入的 + payload compiler。GSL configure 会编译并运行 ABI 探测程序,宿主编译器 + 保证这些探测程序能在 CI runner 上执行。 +- 消费侧公开 `include/gsl/*.h`,链接顺序为 `libgsl`、`libgslcblas`、`libm`。 +- GSL 自带 CBLAS 随包默认提供,不再声明或隐式绑定 `compat.openblas`,避免 BLAS ABI + 和符号来源不明确。 +- Windows 暂不声明 xpm 版本:官方 2.8 归档没有原生 MSVC 构建路径,当前 index 也不 + 应凭空引入未经验证的预编译库。测试工程在非 Unix 平台编译 no-op。 + +## 3. Feature 评估 + +GSL 的各数值领域在上游构建中共同链接到 `libgsl`,不是可以由 mcpp `features` +(只能可靠门控源码列表)独立切分的消费者组件,因此本版本不增加 feature。CBLAS +同样作为完整 GSL 的默认组成部分。 + +## 4. 测试与验证目标 + +`tests/examples/gsl` 覆盖: + +- Bessel 特殊函数; +- 自适应积分; +- CBLAS `ddot` 的链接和结果。 + +验证包括描述符 lint、`mcpp xpkg parse`、`mcpp test -p gsl`,以及一次清理缓存后的 +冷构建,确认头文件和两个静态归档来自该包而不是宿主系统。 diff --git a/mcpp.toml b/mcpp.toml index 179e43c5..83ad9f21 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -62,6 +62,7 @@ members = [ "tests/examples/gzip-hpp", "tests/examples/huxerui-module", "tests/examples/gmp-gmpxx", + "tests/examples/gsl", "tests/examples/godot-cpp", "tests/examples/godot-cpp-module", "tests/examples/godot-cpp-module-v10", diff --git a/pkgs/c/compat.gsl.lua b/pkgs/c/compat.gsl.lua new file mode 100644 index 00000000..60939aa2 --- /dev/null +++ b/pkgs/c/compat.gsl.lua @@ -0,0 +1,156 @@ +-- compat.gsl -- GNU Scientific Library 2.8, built from the official +-- autotools release. GSL's configure step generates config.h and libtool +-- assembles two archives, so the install hook is used instead of trying to +-- approximate the upstream source graph with broad globs. +-- +-- The package exposes both libgsl and the GSL CBLAS implementation. Windows +-- is intentionally not declared: the official release has no native MSVC +-- build system, and silently using a host or third-party binary would break +-- the index's source-build and ABI guarantees. +package = { + spec = "1", + namespace = "compat", + name = "gsl", + description = "GNU Scientific Library with its CBLAS implementation (static, source-built)", + licenses = {"GPL-3.0-or-later"}, + repo = "https://www.gnu.org/software/gsl/", + type = "package", + + xpm = { + linux = { + deps = { "xim:make@latest" }, + ["2.8"] = { + url = "https://ftp.gnu.org/gnu/gsl/gsl-2.8.tar.gz", + sha256 = "6a99eeed15632c6354895b1dd542ed5a855c0f15d9ad1326c6fe2b2c9e423190", + }, + }, + macosx = { + ["2.8"] = { + url = "https://ftp.gnu.org/gnu/gsl/gsl-2.8.tar.gz", + sha256 = "6a99eeed15632c6354895b1dd542ed5a855c0f15d9ad1326c6fe2b2c9e423190", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + sources = { "mcpp_gsl_anchor.c" }, + include_dirs = { "include" }, + targets = { ["gsl"] = { kind = "lib" } }, + linux = { + ldflags = { "-Llib", "-lgsl", "-lgslcblas", "-lm" }, + }, + macosx = { + ldflags = { "-Llib", "-lgsl", "-lgslcblas", "-lm" }, + }, + }, +} + +import("xim.libxpkg.pkginfo") +import("xim.libxpkg.log") + +local function sh_quote(value) + return "'" .. tostring(value):gsub("'", "'\\''") .. "'" +end + +local function resolve_make() + local dep = pkginfo.build_dep("xim:make") or pkginfo.build_dep("make") + if dep and dep.bin then + local candidate = path.join(dep.bin, "make") + if os.isfile(candidate) then return candidate end + end + return "make" +end + +local function resolve_cc() + if os.host() == "linux" and os.isfile("/usr/bin/gcc") then + return "/usr/bin/gcc" + end + if os.host() == "macosx" and os.isfile("/usr/bin/cc") then + return "/usr/bin/cc" + end + return "cc" +end + +local function tail_lines(file, count) + local ok, content = pcall(io.readfile, file) + if not ok or not content then return "" end + local lines = {} + for line in (tostring(content) .. "\n"):gmatch("(.-)\n") do + lines[#lines + 1] = line + end + local first = math.max(1, #lines - count + 1) + return table.concat(lines, "\n", first, #lines) +end + +local function run(command, logf) + local ok, result = pcall(os.exec, string.format( + "bash -c %s", sh_quote(command .. " >> " .. sh_quote(logf) .. " 2>&1"))) + if not ok or not result then + log.error("compat.gsl: build command failed; see %s\n%s", + logf, tail_lines(logf, 80)) + return false + end + return true +end + +local function install_gsl() + local archive = pkginfo.install_file() + local source = archive and tostring(archive):replace(".tar.gz", "") + or ("gsl-" .. pkginfo.version()) + if not os.isdir(source) then + source = "gsl-" .. pkginfo.version() + end + + local prefix = pkginfo.install_dir() + os.tryrm(prefix) + os.mv(source, prefix) + os.cd(prefix) + + local logf = path.join(prefix, "mcpp_gsl_build.log") + local make = resolve_make() + local cc = resolve_cc() + local jobs = (os.default_njob and os.default_njob()) or 4 + local configure = string.format( + "CC=%s CFLAGS='-std=c11' %s --disable-shared --enable-static " + .. "--disable-dependency-tracking " + .. "--prefix=%s --libdir=%s", + sh_quote(cc), "./configure", sh_quote(prefix), + sh_quote(path.join(prefix, "lib"))) + if not run(configure, logf) then return false end + if not run(string.format("%s -j%d", sh_quote(make), jobs), logf) then + return false + end + if not run(string.format("%s install", sh_quote(make)), logf) then + return false + end + + local required = { + path.join(prefix, "include", "gsl", "gsl_math.h"), + path.join(prefix, "include", "gsl", "gsl_cblas.h"), + path.join(prefix, "lib", "libgsl.a"), + path.join(prefix, "lib", "libgslcblas.a"), + } + for _, file in ipairs(required) do + if not os.isfile(file) then + log.error("compat.gsl: build did not produce %s; see %s", file, logf) + return false + end + end + + io.writefile(path.join(prefix, "mcpp_gsl_anchor.c"), + "int mcpp_compat_gsl_anchor(void) { return 0; }\n") + return true +end + +function install() + if os.host() == "windows" then return true end + local ok, result = pcall(install_gsl) + if not ok then + log.error("compat.gsl install() failed: %s", tostring(result)) + return false + end + return result +end diff --git a/tests/examples/gsl/mcpp.toml b/tests/examples/gsl/mcpp.toml new file mode 100644 index 00000000..073e5a4a --- /dev/null +++ b/tests/examples/gsl/mcpp.toml @@ -0,0 +1,12 @@ +[package] +name = "gsl-tests" +version = "0.1.0" + +[indices] +compat = { path = "../../.." } + +[target.'cfg(unix)'.dependencies.compat] +gsl = "2.8" + +[target.'cfg(unix)'.build] +cxxflags = ["-DHAVE_GSL=1"] diff --git a/tests/examples/gsl/tests/gsl.cpp b/tests/examples/gsl/tests/gsl.cpp new file mode 100644 index 00000000..fe72bd8f --- /dev/null +++ b/tests/examples/gsl/tests/gsl.cpp @@ -0,0 +1,49 @@ +#ifdef HAVE_GSL + +#include +#include + +#include +#include +#include +#include + +namespace { + +double square(double x, void*) { + return x * x; +} + +} // namespace + +int main() { + if (std::abs(gsl_sf_bessel_J0(0.0) - 1.0) > 1e-12) return 1; + + double result = 0.0; + double error = 0.0; + gsl_function function; + function.function = □ + function.params = nullptr; + gsl_integration_workspace* workspace = gsl_integration_workspace_alloc(100); + if (!workspace) return 2; + gsl_integration_qags(&function, 0.0, 1.0, 0.0, 1e-10, 100, + workspace, &result, &error); + gsl_integration_workspace_free(workspace); + if (std::abs(result - 1.0 / 3.0) > 1e-8 || error > 1e-8) return 2; + + const double left[2] = {1.0, 2.0}; + const double right[2] = {3.0, 4.0}; + const double product = cblas_ddot(2, left, 1, right, 1); + if (std::abs(product - 11.0) > 1e-12) return 3; + + std::printf("compat.gsl: special functions, integration, and CBLAS ok\n"); + return 0; +} + +#else + +int main() { + return 0; +} + +#endif