Skip to content

Commit 798a524

Browse files
authored
feat: add source-built MySQL client packages (#185)
* feat: add libmysqlclient and MySQL Connector/C++ * fix(pkg): refresh compat.libmysqlclient sha after config repair The vendored libmysqlclient staging repo now defines HAVE_SYS_RESOURCE_H (plus the other POSIX macros upstream configure.cmake pairs with them) and carries Windows support sources; tag 8.4.6 was force-moved to the repaired commit, so the linux/macosx archives changed. mcpp-index still declares only linux+macosx for this package. * fix(pkg): bump compat.libmysqlclient sha (strlcat/strlcpy on Linux) Linux toolchain glibc >= 2.38 declares strlcat/strlcpy; vendored config now defines HAVE_STRLCAT/HAVE_STRLCPY there so crypt_genhash_impl.cc uses libc instead of its static fallback. Tag 8.4.6 updated again. * diag(connector): surface install-hook failures to Actions stdout The connector install hook fails with only an empty E_INTERNAL on macOS CI with no build log. Add stdout diagnostics (pcall wrapper, step values, run() error dump) to locate the failing step. Temporary diagnostic commit. * diag(connector): write hook trace to a findable build log xlings swallows install-hook stdout/log.error, so the previous stdout diagnostics never reached the Actions log. Write every step (including a descriptor-load marker) to $HOME/.mcpp/registry/data/mcpp_mysql_connector_cpp_build.log, which validate.yml's 'find ... -name mcpp_*_build.log' dump tail-prints. * diag(connector): guard diag helper for descriptor-executing lint check_mirror_urls/check_package_name execute the descriptor with only import stubbed; top-level path.join crashed lint. Make diag lazy and a no-op unless the xlings runtime's path global is present. * fix(connector): find deps in shared cache + surface hook errors Root cause (from CI diagnostics): pkginfo.install_dir('compat:openssl') returns nil when openssl was installed into the shared registry cache by the sibling libmysqlclient member instead of this member's local xpkgs, so the install hook bailed with only an empty E_INTERNAL. - declare compat:libmysqlclient/compat:openssl as xpm deps so the resolver records them for this package - add find_dep_install_dir() fallback scanning the known xpkgs roots (member-local + $MCPP_HOME/registry/data/xpkgs) - write the hook trace + failure reason to $HOME/.mcpp/registry/data/mcpp_mysql_connector_cpp_build.log, which the validate.yml find-dump tail-prints (log.error/stdout are swallowed) - macOS: sanitize PATH to /usr/bin:/bin:/usr/sbin:/sbin so a GNU libtool on PATH (conda/homebrew) cannot shadow Apple's libtool in the CMake merge * fix(connector): resolve xim cmake .app bundle, prepend system PATH CI reached CMake configure for the first time and failed with exit 127: resolve_tool fell back to bare 'cmake' because the xim cmake macOS payload is CMake.app/Contents/bin/cmake, not bin/cmake, and the previous fix replaced PATH entirely so the fallback had nothing to resolve. Handle the .app layout for an absolute path and prepend /usr/bin:/bin:/usr/sbin:/sbin to the existing PATH (system libtool wins, tools stay reachable). * fix(connector): quote PATH value in env invocation A dev-machine PATH containing spaces (e.g. 'Library/Application Support/...') split the env command line and failed CMake configure with exit 127. Wrap the prepended PATH value in single quotes (sh_quote).
1 parent 13b1fe3 commit 798a524

7 files changed

Lines changed: 425 additions & 0 deletions

File tree

mcpp.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ members = [
4444
"tests/examples/imgui-module",
4545
"tests/examples/imgui-window",
4646
"tests/examples/marzer.tomlplusplus",
47+
"tests/examples/libmysqlclient",
48+
"tests/examples/mysql-connector-cpp",
4749
"tests/examples/nlohmann.json",
4850
"tests/examples/openblas",
4951
"tests/examples/openssl",

pkgs/c/compat.libmysqlclient.lua

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Form A: 发布归档自带根级 mcpp.toml,消费者直接用 mcpp 构建客户端静态库。
2+
package = {
3+
spec = "1",
4+
namespace = "compat",
5+
name = "libmysqlclient",
6+
description = "MySQL C API client library (static, client-only source)",
7+
licenses = {"GPL-2.0-only", "Universal-FOSS-exception-1.0"},
8+
repo = "https://github.com/wellwei/libmysqlclient",
9+
type = "package",
10+
11+
xpm = {
12+
linux = {
13+
["8.4.6"] = {
14+
url = "https://github.com/wellwei/libmysqlclient/archive/refs/tags/8.4.6.tar.gz",
15+
sha256 = "27decd6716591d84a6f765a3d63fcd24031373841c684369445b83a840b38bea",
16+
},
17+
},
18+
macosx = {
19+
["8.4.6"] = {
20+
url = "https://github.com/wellwei/libmysqlclient/archive/refs/tags/8.4.6.tar.gz",
21+
sha256 = "27decd6716591d84a6f765a3d63fcd24031373841c684369445b83a840b38bea",
22+
},
23+
},
24+
},
25+
26+
-- 无 `mcpp` 字段:默认查找会命中归档根目录的 mcpp.toml。
27+
}
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
-- compat.mysql-connector-cpp -- MySQL Connector/C++ X DevAPI and JDBC.
2+
package = {
3+
spec = "1",
4+
namespace = "compat",
5+
name = "mysql-connector-cpp",
6+
description = "MySQL Connector/C++ X DevAPI and JDBC (static, source-built)",
7+
licenses = {"GPL-2.0-only", "Universal-FOSS-exception-1.0"},
8+
repo = "https://github.com/mysql/mysql-connector-cpp",
9+
type = "package",
10+
11+
xpm = {
12+
linux = {
13+
deps = {
14+
"compat:libmysqlclient@8.4.6",
15+
"compat:openssl@3.5.1",
16+
"xim:cmake@latest",
17+
"xim:make@latest",
18+
},
19+
["26.7.0"] = {
20+
url = "https://github.com/mysql/mysql-connector-cpp/archive/refs/tags/26.7.0.tar.gz",
21+
sha256 = "b2299862eefc33fd71c0aac68328305671805fc955e6bd2578ef205c10f98550",
22+
},
23+
},
24+
macosx = {
25+
deps = {
26+
"compat:libmysqlclient@8.4.6",
27+
"compat:openssl@3.5.1",
28+
"xim:cmake@latest",
29+
},
30+
["26.7.0"] = {
31+
url = "https://github.com/mysql/mysql-connector-cpp/archive/refs/tags/26.7.0.tar.gz",
32+
sha256 = "b2299862eefc33fd71c0aac68328305671805fc955e6bd2578ef205c10f98550",
33+
},
34+
},
35+
},
36+
37+
mcpp = {
38+
language = "c++23",
39+
import_std = false,
40+
sources = { "mcpp_mysql_connector_cpp_anchor.c" },
41+
include_dirs = { "include" },
42+
targets = { ["mysql_connector_cpp"] = { kind = "lib" } },
43+
deps = {
44+
["compat.libmysqlclient"] = "8.4.6",
45+
["compat.openssl"] = "3.5.1",
46+
},
47+
48+
linux = {
49+
ldflags = {
50+
"-Llib", "-l:libmysqlcppconnx-static.a",
51+
"-l:libmysqlcppconn-static.a", "-lresolv",
52+
},
53+
},
54+
macosx = {
55+
ldflags = {
56+
"-Llib", "-lmysqlcppconnx-static",
57+
"-lmysqlcppconn-static", "-lresolv",
58+
},
59+
},
60+
},
61+
}
62+
63+
import("xim.libxpkg.pkginfo")
64+
import("xim.libxpkg.log")
65+
-- Hook diagnostics: xlings swallows install-hook stdout and log.error, so a
66+
-- failure used to surface only as an empty E_INTERNAL with no build log. This
67+
-- hook writes a step trace + the failing reason to
68+
-- $HOME/.mcpp/registry/data/mcpp_mysql_connector_cpp_build.log, which
69+
-- validate.yml's `find ... -name 'mcpp_*_build.log'` dump tail-prints. It is a
70+
-- no-op outside the xlings runtime (the `path` global is absent there), so the
71+
-- descriptor-executing lint helpers stay green.
72+
local hook_logf
73+
local function hook_log(msg)
74+
if type(path) ~= "table" then return end
75+
if not hook_logf then
76+
local home = os.getenv and os.getenv("HOME") or "/tmp"
77+
hook_logf = home .. "/.mcpp/registry/data/mcpp_mysql_connector_cpp_build.log"
78+
end
79+
local f = io.open(hook_logf, "a")
80+
if f then
81+
f:write(os.date("%H:%M:%S") .. " " .. tostring(msg) .. "\n")
82+
f:close()
83+
end
84+
end
85+
hook_log("descriptor loaded")
86+
87+
local function sh_quote(value)
88+
return "'" .. tostring(value):gsub("'", "'\\''") .. "'"
89+
end
90+
91+
local function resolve_tool(dep, name, fallback)
92+
local pkg = pkginfo.build_dep(dep)
93+
if pkg then
94+
local candidates = {}
95+
if pkg.bin then candidates[#candidates + 1] = path.join(pkg.bin, name) end
96+
if pkg.path then
97+
-- macOS .app bundles (xim cmake ships CMake.app/Contents/bin).
98+
candidates[#candidates + 1] =
99+
path.join(pkg.path, name .. ".app", "Contents", "bin", name)
100+
end
101+
for _, cand in ipairs(candidates) do
102+
if os.isfile(cand) then return cand end
103+
end
104+
end
105+
return fallback
106+
end
107+
108+
local function tail_lines(file, count)
109+
local ok, content = pcall(io.readfile, file)
110+
if not ok or not content then return "<log unavailable>" end
111+
local lines = {}
112+
for line in (tostring(content) .. "\n"):gmatch("(.-)\n") do
113+
lines[#lines + 1] = line
114+
end
115+
return table.concat(lines, "\n", math.max(1, #lines - count + 1), #lines)
116+
end
117+
118+
local function run(step, logf, command)
119+
-- xlings 的 os.exec 通过返回值报告非零退出;pcall 只负责捕获 Lua 错误。
120+
local invoked, result, reason, code =
121+
pcall(os.exec, string.format("bash -c %s", sh_quote(command)))
122+
if invoked and result then return true end
123+
local err = invoked and (tostring(reason) .. " " .. tostring(code))
124+
or tostring(result)
125+
local msg = "compat.mysql-connector-cpp: " .. step .. " failed ("
126+
.. tostring(err) .. ")\n--- last 40 lines of " .. logf .. " ---\n"
127+
.. tail_lines(logf, 40)
128+
log.error("%s", msg)
129+
hook_log(msg)
130+
return false
131+
end
132+
133+
local function find_source_root()
134+
local ifile = pkginfo.install_file()
135+
local roots = {}
136+
if ifile then roots[#roots + 1] = tostring(ifile):replace(".tar.gz", "") end
137+
roots[#roots + 1] = "mysql-connector-cpp-" .. pkginfo.version()
138+
for _, root in ipairs(roots) do
139+
if os.isfile(path.join(root, "CMakeLists.txt")) then return root end
140+
end
141+
end
142+
143+
local function find_mysql_source_root(prefix)
144+
local direct = path.join(prefix, "libmysqlclient-8.4.6")
145+
if os.isfile(path.join(direct, "mcpp.toml")) then return direct end
146+
for _, manifest in ipairs(os.files(path.join(prefix, "**", "mcpp.toml")) or {}) do
147+
local root = path.directory(manifest)
148+
if os.isfile(path.join(root, "include", "mysql.h")) then return root end
149+
end
150+
end
151+
152+
-- `pkginfo.install_dir` scans only the member-local xpkgs roots; a dependency
153+
-- that was installed into the shared registry cache (e.g. compat.openssl built
154+
-- by the sibling libmysqlclient member and reused here without a member-local
155+
-- copy) is invisible to it and comes back nil. Fall back to the known xpkgs
156+
-- roots before giving up.
157+
local function find_dep_install_dir(dep_name, dep_version)
158+
local dir = pkginfo.install_dir(dep_name, dep_version)
159+
if dir then return dir end
160+
local ns, bare = dep_name:match("^([^:]+):(.+)$")
161+
if not ns then return nil end
162+
local store = ns .. "-x-" .. bare
163+
local pfx = pkginfo.install_dir()
164+
local roots = {}
165+
if pfx then roots[#roots + 1] = path.directory(path.directory(pfx)) end
166+
local home = os.getenv and os.getenv("MCPP_HOME") or ""
167+
if home == "" then home = ((os.getenv and os.getenv("HOME")) or "") .. "/.mcpp" end
168+
roots[#roots + 1] = path.join(home, "registry/data/xpkgs")
169+
for _, root in ipairs(roots) do
170+
local cand = path.join(root, store, dep_version)
171+
if os.isdir(cand) then return cand end
172+
end
173+
return nil
174+
end
175+
176+
local function patch_internal_compression_helper(srcroot)
177+
local source = path.join(srcroot, "common", "session.cc")
178+
local content = io.readfile(source)
179+
local replacement =
180+
"\nstatic TCPIP_options::compression_algorithm_t get_compression_algorithm(std::string alg)"
181+
if content:find(replacement, 1, true) then return true end
182+
183+
local signature =
184+
"\nTCPIP_options::compression_algorithm_t get_compression_algorithm%(std::string alg%)"
185+
local patched, count = content:gsub(signature, replacement)
186+
if count ~= 1 then
187+
log.error("compat.mysql-connector-cpp: expected one compression helper, found "
188+
.. tostring(count))
189+
return false
190+
end
191+
io.writefile(source, patched)
192+
return true
193+
end
194+
195+
function install()
196+
hook_log("install() called; install_file=" .. tostring(pkginfo.install_file())
197+
.. " install_dir=" .. tostring(pkginfo.install_dir())
198+
.. " deps=" .. tostring(pkginfo.deps_list()))
199+
local ok, result = pcall(function()
200+
local srcroot = find_source_root()
201+
local mysql = find_dep_install_dir("compat:libmysqlclient", "8.4.6")
202+
local openssl = find_dep_install_dir("compat:openssl", "3.5.1")
203+
hook_log("srcroot=" .. tostring(srcroot) .. " mysql=" .. tostring(mysql)
204+
.. " openssl=" .. tostring(openssl))
205+
if not srcroot or not mysql or not openssl then
206+
local why = "source/dependency prefix not found (srcroot="
207+
.. tostring(srcroot) .. " mysql=" .. tostring(mysql)
208+
.. " openssl=" .. tostring(openssl) .. ")"
209+
log.error("compat.mysql-connector-cpp: %s", why)
210+
hook_log(why)
211+
return false
212+
end
213+
214+
local mysql_src = find_mysql_source_root(mysql)
215+
hook_log("mysql_src=" .. tostring(mysql_src))
216+
if not mysql_src then
217+
log.error("compat.mysql-connector-cpp: libmysqlclient Form A source root not found")
218+
return false
219+
end
220+
221+
local prefix = pkginfo.install_dir()
222+
local builddir = path.join(srcroot, "mcpp-build")
223+
local logf = path.join(prefix, "mcpp_mysql_connector_cpp_build.log")
224+
hook_log("prefix=" .. tostring(prefix) .. " builddir=" .. tostring(builddir)
225+
.. " logf=" .. tostring(logf))
226+
os.tryrm(prefix)
227+
os.mkdir(prefix)
228+
os.tryrm(builddir)
229+
230+
-- 此 helper 只在 session.cc 内使用;内部链接避免与 MySQL 8.4
231+
-- libmysqlclient 中同签名的实现发生静态链接符号冲突。
232+
if not patch_internal_compression_helper(srcroot) then return false end
233+
234+
-- Connector/C++ 的配置阶段只要求能找到一个 mysqlclient archive;真正的
235+
-- libmysqlclient.a 会在最终消费时由 mcpp 的依赖图链接。这里的探测库不安装。
236+
local mysql_probe = path.join(builddir, "mysqlclient-probe")
237+
os.mkdir(path.join(mysql_probe, "lib"))
238+
os.mkdir(path.join(mysql_probe, "include"))
239+
local probe_src = path.join(mysql_probe, "probe.c")
240+
local probe_obj = path.join(mysql_probe, "probe.o")
241+
local probe_lib = path.join(mysql_probe, "lib", "libmysqlclient.a")
242+
io.writefile(probe_src, "int mcpp_mysqlclient_probe(void) { return 0; }\n")
243+
local probe_command = string.format(
244+
"cp -R %s/. %s/ && cp %s/*.h %s/ && "
245+
.. "cc -c %s -o %s && ar rcs %s %s",
246+
sh_quote(path.join(mysql_src, "include")),
247+
sh_quote(path.join(mysql_probe, "include")),
248+
sh_quote(path.join(mysql_src, "generated")),
249+
sh_quote(path.join(mysql_probe, "include")),
250+
sh_quote(probe_src), sh_quote(probe_obj), sh_quote(probe_lib), sh_quote(probe_obj))
251+
if not run("mysqlclient probe archive", logf,
252+
probe_command .. " >" .. sh_quote(logf) .. " 2>&1") then
253+
return false
254+
end
255+
256+
local cmake = resolve_tool("xim:cmake", "cmake", "cmake")
257+
local make = resolve_tool("xim:make", "make", "make")
258+
local jobs = (os.default_njob and os.default_njob()) or 4
259+
local clean_env = "env -u CPPFLAGS -u CFLAGS -u CXXFLAGS -u LDFLAGS "
260+
local compiler = ""
261+
if os.host() == "macosx" then
262+
-- Connector 在 project() 前启动 bootstrap CMake;必须通过环境变量
263+
-- 将最低系统版本同步给 bootstrap 及其后续的内置依赖构建。
264+
clean_env = clean_env .. "MACOSX_DEPLOYMENT_TARGET=14.0 "
265+
-- Sanitize PATH: the connector's CMake merge step invokes Apple's
266+
-- libtool, and a GNU libtool earlier on PATH (conda/homebrew) breaks
267+
-- the archive merge. cmake/make come from xim by absolute path and
268+
-- cc/c++ are pinned below, so system-only PATH is safe here.
269+
local system_path = "/usr/bin:/bin:/usr/sbin:/sbin:" .. (os.getenv("PATH") or "")
270+
clean_env = clean_env .. "PATH=" .. sh_quote(system_path) .. " "
271+
compiler = "-DCMAKE_C_COMPILER=/usr/bin/cc -DCMAKE_CXX_COMPILER=/usr/bin/c++ "
272+
.. "-DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 "
273+
end
274+
275+
local configure = string.format(
276+
"%s%s -S %s -B %s -G \"Unix Makefiles\" "
277+
.. "-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%s "
278+
.. "-DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_MAKE_PROGRAM=%s "
279+
.. "-DBUILD_STATIC=ON -DWITH_JDBC=ON -DWITH_TESTS=OFF "
280+
.. "-DWITH_DOC=OFF -DWITH_HEADER_CHECKS=OFF "
281+
.. "-DOPENSSL_USE_STATIC_LIBS=TRUE -DWITH_SSL=%s "
282+
.. "-DMYSQL_INCLUDE_DIR=%s/include -DMYSQL_LIB_DIR=%s/lib "
283+
.. "-DMYSQLCLIENT_STATIC_LINKING=ON -DMYSQLCLIENT_STATIC_BINDING=ON %s",
284+
clean_env, sh_quote(cmake), sh_quote(srcroot), sh_quote(builddir),
285+
sh_quote(prefix), sh_quote(make), sh_quote(openssl),
286+
sh_quote(mysql_probe), sh_quote(mysql_probe), compiler)
287+
if not run("CMake configure", logf, configure .. " >" .. sh_quote(logf) .. " 2>&1") then
288+
return false
289+
end
290+
291+
local build = string.format(
292+
"%s%s --build %s --target connector -- -j%d >>%s 2>&1",
293+
clean_env, sh_quote(cmake), sh_quote(builddir), jobs, sh_quote(logf))
294+
if not run("X DevAPI build", logf, build) then return false end
295+
296+
local jdbc_build = string.format(
297+
"%s%s --build %s --target connector-jdbc -- -j%d >>%s 2>&1",
298+
clean_env, sh_quote(cmake), sh_quote(builddir), jobs, sh_quote(logf))
299+
if not run("JDBC build", logf, jdbc_build) then return false end
300+
301+
local install_cmd = string.format(
302+
"%s%s --install %s --component XDevAPIDev >>%s 2>&1",
303+
clean_env, sh_quote(cmake), sh_quote(builddir), sh_quote(logf))
304+
if not run("X DevAPI install", logf, install_cmd) then return false end
305+
306+
local jdbc_install = string.format(
307+
"%s%s --install %s --component JDBCDev >>%s 2>&1",
308+
clean_env, sh_quote(cmake), sh_quote(builddir), sh_quote(logf))
309+
if not run("JDBC install", logf, jdbc_install) then return false end
310+
311+
if not os.isfile(path.join(prefix, "include", "mysqlx", "xdevapi.h"))
312+
or not os.isfile(path.join(prefix, "include", "mysql", "jdbc.h"))
313+
or not os.isfile(path.join(prefix, "lib", "libmysqlcppconnx-static.a"))
314+
or not os.isfile(path.join(prefix, "lib", "libmysqlcppconn-static.a")) then
315+
log.error("compat.mysql-connector-cpp: incomplete X DevAPI/JDBC install")
316+
return false
317+
end
318+
319+
io.writefile(path.join(prefix, "mcpp_mysql_connector_cpp_anchor.c"),
320+
"int mcpp_compat_mysql_connector_cpp_anchor(void) { return 0; }\n")
321+
end)
322+
if not ok then
323+
hook_log("UNCAUGHT Lua error: " .. tostring(result))
324+
return false
325+
end
326+
hook_log("install() result=" .. tostring(result))
327+
return result
328+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "libmysqlclient-tests"
3+
version = "0.1.0"
4+
5+
# libmysqlclient 的 mcpp 源码构建目前只验证 Linux/macOS。
6+
[target.'cfg(linux)'.dependencies.compat]
7+
libmysqlclient = "8.4.6"
8+
9+
[target.'cfg(linux)'.build]
10+
cxxflags = ["-DHAVE_LIBMYSQLCLIENT=1"]
11+
12+
[target.'cfg(macos)'.dependencies.compat]
13+
libmysqlclient = "8.4.6"
14+
15+
[target.'cfg(macos)'.build]
16+
cxxflags = ["-DHAVE_LIBMYSQLCLIENT=1"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#ifdef HAVE_LIBMYSQLCLIENT
2+
#include <mysql.h>
3+
4+
int main() {
5+
static_assert(MYSQL_VERSION_ID == 80406);
6+
if (mysql_get_client_version() != 80406) return 1;
7+
8+
MYSQL* client = mysql_init(nullptr);
9+
if (client == nullptr) return 2;
10+
mysql_close(client);
11+
return 0;
12+
}
13+
#else
14+
int main() { return 0; }
15+
#endif

0 commit comments

Comments
 (0)