From 97e67b1ba5e9236036f83f4e89a8ff687b054fe5 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Sun, 26 Jul 2026 22:57:48 +0800 Subject: [PATCH 1/4] chore: pin mcpp 0.0.109, hoist the compat index redirect, fix compat.openssl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## mcpp 0.0.109 (MCPP_VERSION + index.toml floor) 0.0.109 makes a bare dependency take BOTH halves of its wire address from the descriptor the identity gate accepted (mcpp#286). That is the client-side other half of the SPEC-001 short-name migration (#120): mcpp used to take the name from the descriptor and the namespace from the request, so a bare `gtest = "1.15.2"` addressed `mcpplibs:gtest`, a key no index has. It survived only on a hardcoded `compat.` retry that the short names removed — leaving every bare request against this index broken on 0.0.108. min_mcpp moves with the pin so consumers get a client that can address the index they are handed. ## [indices]: hoisted to the workspace root The 13 members that consume `compat` no longer repeat `[indices] compat = { path = "../../.." }`; the root declares it once and they inherit it, with the relative path resolved against the workspace root (mcpp#224, 0.0.97). Exactly one entry, and `compat` specifically, because the table is keyed by NAMESPACE: * declared under a name no dependency asks for, the index is never registered and resolution silently falls through to the published remote index — the checkout under test is then not what is being tested; * declared under several namespaces it registers N separate project repos, and every lookup afterwards fails with an N-way ambiguity between what is physically one descriptor (mcpp#238 / xlings#374). Members needing another namespace keep their own declaration, which REPLACES the inherited table rather than merging — which is what holds each member to one project index repo. mcpp.toml and index.toml also join the workflow's paths filter; a change to either can break every member and neither gated a run before. ## compat.openssl `name`: `compat.openssl` -> `openssl`. It was the index's last legacy fully-qualified spelling (SPEC-001 §3.2). `./config` now passes `--libdir=lib`. Unset, OpenSSL derives the install libdir as `lib$target{multilib}` and `linux-x86_64` declares `multilib => "64"`, so the archives landed in $prefix/lib64 while `-Llib` and the post-build check looked at $prefix/lib. linux-aarch64 and both darwin64 targets declare no multilib and resolve to plain `lib` — which is how this validated on an arm64 Mac while being broken for every x86_64 Linux consumer. Reproduced against the published descriptor before fixing: the install tree came out as bin/ include/ lib64/ and install() failed its own archive check. Also: linux links `-l:libssl.a -l:libcrypto.a` (naming the archive rather than letting the driver resolve a name a shared object would win — one stray -L ahead of ours and the consumer picks up the host libssl.so.3) plus `-ldl -lpthread` for musl and pre-2.34 glibc; the build log moved into the install prefix, since the old spot was deleted mid-build by the very os.tryrm that preceded install; `RANLIB=/usr/bin/ranlib` is confined to macOS, the platform whose llvm-ranlib rejects `-c`; a redundant `os.cd` that would have broken the relative-srcroot fallback is gone; and install() probes for `perl` up front, since OpenSSL's ./config IS a Perl script and no xim:perl exists to declare. ## Two members for it `tests/examples/openssl` — direct dependency, inherits the root `compat` redirect, so the descriptor in this checkout is what gets built. Asserts both archives really link (SSL_CTX_new + EVP sha256) and the binary carries no dynamic ssl/crypto dependency. `tests/examples/asio-ssl` — the asio `ssl` feature end-to-end, a real TLS handshake over loopback. It declares no member-level [indices] on purpose: inheriting `compat` means asio resolves from the published index while its feature dep, compat.openssl, comes from this checkout — which is how an unmerged compat descriptor can be exercised through a published consumer. The test moved out of asio-module, where it had been guarded by MCPP_FEATURE_SSL. A feature's `defines` apply to the package's own TUs, not to consumers, so that file compiled to nothing and passed while asserting nothing — the reason the libdir bug reached main green. It now keys off HAVE_ASIO_SSL, which the member sets in its own cfg-gated cxxflags. ## Docs docs/package-types.md still taught `name = "compat."`. README, the repository/schema doc (new "索引重定向" section, corrected `features` row) and the openssl design spec are updated to match what the code does. --- .github/workflows/validate.yml | 28 ++++- README.md | 7 +- docs/package-types.md | 13 ++- docs/repository-and-schema.md | 39 +++++-- .../2026-07-26-openssl-asio-tls-design.md | 32 ++++- index.toml | 4 +- mcpp.toml | 48 ++++++-- pkgs/c/compat.openssl.lua | 110 ++++++++++++++---- tests/examples/archive/mcpp.toml | 3 - tests/examples/asio-module/mcpp.toml | 10 +- tests/examples/asio-ssl/mcpp.toml | 41 +++++++ .../{asio-module => asio-ssl}/tests/ssl.cpp | 19 ++- tests/examples/cjson/mcpp.toml | 3 - tests/examples/core/mcpp.toml | 3 - tests/examples/eigen/mcpp.toml | 14 ++- tests/examples/ffmpeg-module/mcpp.toml | 19 +-- tests/examples/ffmpeg/mcpp.toml | 3 - tests/examples/fmtlib.fmt/mcpp.toml | 11 +- tests/examples/gui-stack/mcpp.toml | 3 - tests/examples/imgui-module/mcpp.toml | 13 ++- tests/examples/imgui-window/mcpp.toml | 3 - tests/examples/imgui/mcpp.toml | 3 - tests/examples/marzer.tomlplusplus/mcpp.toml | 10 +- tests/examples/nlohmann.json/mcpp.toml | 10 +- tests/examples/openblas/mcpp.toml | 3 - tests/examples/opencv-module-dnn/mcpp.toml | 10 +- .../examples/opencv-module-unifont/mcpp.toml | 10 +- tests/examples/opencv-module/mcpp.toml | 19 +-- tests/examples/openssl/mcpp.toml | 27 +++++ tests/examples/openssl/tests/tls.cpp | 50 ++++++++ tests/examples/spdlog-compiled/mcpp.toml | 3 - tests/examples/spdlog/mcpp.toml | 8 +- tests/examples/tinyhttps/mcpp.toml | 10 +- 33 files changed, 446 insertions(+), 143 deletions(-) create mode 100644 tests/examples/asio-ssl/mcpp.toml rename tests/examples/{asio-module => asio-ssl}/tests/ssl.cpp (89%) create mode 100644 tests/examples/openssl/mcpp.toml create mode 100644 tests/examples/openssl/tests/tls.cpp diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 467bb51..833e26f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,7 +2,10 @@ name: validate on: pull_request: - paths: ["pkgs/**/*.lua", "tests/**", "README.md", ".github/workflows/validate.yml"] + # mcpp.toml and index.toml carry the workspace member list, the inherited + # [indices] redirect and the client version floor — a change to any of them + # can break every member, so they gate the run like the descriptors do. + paths: ["pkgs/**/*.lua", "tests/**", "README.md", "mcpp.toml", "index.toml", ".github/workflows/validate.yml"] push: branches: [main] schedule: @@ -11,6 +14,21 @@ on: workflow_dispatch: env: + # 0.0.109: a bare dependency's wire address takes BOTH halves from the + # descriptor the identity gate accepted (mcpp#286). This is the client-side + # other half of the SPEC-001 migration below: mcpp used to take the NAME from + # the descriptor and the NAMESPACE from the request, so a bare `gtest = + # "1.15.2"` addressed `mcpplibs:gtest` — a key no index has. It only ever + # worked because the pre-migration literal `package.name` read + # "compat.gtest", which the hardcoded `compat.` retry then caught. + # Short names removed that coincidence and left every bare request against + # this index broken on 0.0.108, which is why min_mcpp moves to 0.0.109. + # Note this index cannot cover that spelling itself: the `[indices]` redirect + # is keyed by the REQUEST's namespace, so a bare dependency resolves from the + # published remote index rather than the checkout under test. Bare-name + # resolution against a short-name index is upstream's e2e 165; what moving + # the floor buys here is that consumers of THIS index get a client that can + # address it. # 0.0.106: SPEC-001 package identity (mcpp#280). `package.name` is a SINGLE # ATOMIC SEGMENT — all hierarchy lives in `package.namespace` — and mcpp # addresses a package by the LITERAL name it read, so descriptors no longer @@ -57,7 +75,7 @@ env: # 0.0.94 fixed feature-gated `sources` under `mcpp test` (mcpp#218); 0.0.91 # added standard = "c++fly" to the resolver grammar, so c++fly descriptors # get the lint WARN below, not a hard grammar-parse rejection. - MCPP_VERSION: "0.0.108" + MCPP_VERSION: "0.0.109" jobs: lint: @@ -222,21 +240,21 @@ jobs: ext: tar.gz mcpp: bin/mcpp xlings: registry/bin/xlings - mcpp_version: "0.0.108" # keep in sync with env.MCPP_VERSION + mcpp_version: "0.0.109" # keep in sync with env.MCPP_VERSION - platform: macos os: macos-15 suffix: macosx-arm64 ext: tar.gz mcpp: bin/mcpp xlings: registry/bin/xlings - mcpp_version: "0.0.108" # keep in sync with env.MCPP_VERSION + mcpp_version: "0.0.109" # keep in sync with env.MCPP_VERSION - platform: windows os: windows-latest suffix: windows-x86_64 ext: zip mcpp: bin/mcpp.exe xlings: registry/bin/xlings.exe - mcpp_version: "0.0.108" # keep in sync with env.MCPP_VERSION + mcpp_version: "0.0.109" # keep in sync with env.MCPP_VERSION env: MCPP_EFFECTIVE: ${{ matrix.mcpp_version }} steps: diff --git a/README.md b/README.md index 3490596..9344d42 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | 原生模块库(Form A) | [`mcpplibs.xpkg`](pkgs/x/xpkg.lua) · [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`tensorvia-cpu`](pkgs/t/tensorvia-cpu.lua) · [`ffmpeg`](pkgs/f/ffmpeg.lua)(模块层,源码经 `compat.ffmpeg` 直编) · [`opencv`](pkgs/o/opencv.lua)(单仓库:模块层与 OpenCV 5 全源码构建同在包内,索引侧只留本描述符) | | C 源码 compat(含 `features`) | [`compat.cjson`](pkgs/c/compat.cjson.lua) · [`compat.zlib`](pkgs/c/compat.zlib.lua) | | header-only(含 `features`) | [`compat.eigen`](pkgs/c/compat.eigen.lua) | -| 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) | +| 外部构建系统(`install()` 从源码构建) | [`compat.openblas`](pkgs/c/compat.openblas.lua)(Make) · [`compat.openssl`](pkgs/c/compat.openssl.lua)(Perl Configure + Make,静态 libssl/libcrypto) | | 全源码直编(config 快照 + 源列表,零外部构建系统) | [`compat.ffmpeg`](pkgs/c/compat.ffmpeg.lua)(2281 TU 含 NASM 汇编,28 个目录 glob 声明) | | C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) · [`marzer.tomlplusplus`](pkgs/m/marzer.tomlplusplus.lua) | @@ -59,8 +59,9 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 - 字段规范见 [mcpp 扩展字段文档](https://github.com/mcpp-community/mcpp/blob/main/docs/04-schema-xpkg-extension.md)。 > 提交 PR 后,`validate` 自动执行 lint 并按改动库选跑对应 workspace 成员(整个测试面是一个 mcpp -> workspace,公开模块包 `imgui`/`ffmpeg`/`opencv`/`tinyhttps` 也是普通成员——各成员经 `[indices]` -> 把所消费命名空间重定向到 checkout,零 shell 驱动);合并后,`deploy-site` 将其发布至在线浏览站。 +> workspace,公开模块包 `imgui`/`ffmpeg`/`opencv`/`tinyhttps` 也是普通成员——`compat` 的重定向声明在 +> workspace 根并由成员继承,消费其他命名空间的成员各自覆盖,零 shell 驱动);合并后,`deploy-site` +> 将其发布至在线浏览站。 ## 相关链接 diff --git a/docs/package-types.md b/docs/package-types.md index 9e2f440..3c48557 100644 --- a/docs/package-types.md +++ b/docs/package-types.md @@ -17,8 +17,8 @@ A、B、C 三类共用的骨架(`package` 头与 `xpm`)如下: ```lua package = { spec = "1", - namespace = "compat", -- compat / nlohmann / mcpplibs 等,决定 import 前缀与依赖 key - name = "compat.", -- 完整包名,决定 pkgs/<首字母>/ 的落点 + namespace = "compat", -- 点分层级路径;compat / nlohmann / mcpplibs 等,决定 import 前缀与依赖 key + name = "", -- 单一原子段,不重复 namespace(SPEC-001 §3.2) description = "…", licenses = {"MIT"}, -- SPDX repo = "https://…", @@ -36,6 +36,10 @@ package = { } ``` +身份是 `(namespace, name)` 二元组:层级一律放 `namespace`,`name` 只写一段。文件名不参与解析,推荐 +`pkgs/<首字母>/..lua`(命中 mcpp 的快路径)。详见 +[仓库结构与 schema](repository-and-schema.md#包身份namespace-name)。 + --- ## A. C 源码 compat(`compat.cjson` / `compat.zlib`) @@ -132,8 +136,9 @@ name = "-example" version = "0.1.0" [toolchain] default = "gcc@16.1.0" -[indices] -compat = { path = "../../.." } # 指回仓根,以使用本地描述符 +# `compat` 由 workspace 根的 [indices] 继承,成员无需再写; +# 消费其他命名空间时才在此声明,例如 `[indices] fmtlib = { path = "../../.." }` +# —— 成员级声明会**替换**根级表而非与之合并(这正是保持单个项目索引 repo 的方式)。 [dependencies.compat] = "1.2.3" # 或: = { version = "1.2.3", features = ["…"] } [targets.-example] diff --git a/docs/repository-and-schema.md b/docs/repository-and-schema.md index 4bca5b9..eb624ec 100644 --- a/docs/repository-and-schema.md +++ b/docs/repository-and-schema.md @@ -4,12 +4,13 @@ ``` pkgs//.lua 描述符。 取完整包名首字母(compat.* → c,nlohmann.json → n,imgui → i) -mcpp.toml workspace 清单(members 列表) +mcpp.toml workspace 清单(members 列表)+ 根级 [indices] compat = { path = "." }, + 由成员继承(相对路径按 workspace 根解析,mcpp ≥ 0.0.97) tests/examples// 每库测试工程(workspace 成员; 为包名去前缀,模块包为 - mcpp.toml -module)。恰好一条 [indices] = { path = "../../.." } - 把所消费命名空间重定向到本 checkout(模块包用 default, - mcpp ≥ 0.0.97;单条是硬约束——xlings 多项目级 repo 静默失败, - mcpp#238,修复后再做根级集中化)。依赖按平台自门控 + mcpp.toml -module)。消费 compat 的成员不写 [indices];消费其他 + 命名空间的成员写恰好一条(模块包用 default),该声明**替换** + 根级表而非合并 —— 每个成员最多一个项目级索引 repo 是硬约束, + 详见下文「索引重定向」。依赖按平台自门控 ([target.'cfg(...)']) tests/*.cpp 行为断言(独立 main,退出码非 0 即失败) tests/check_mirror_urls.lua lint:GLOBAL+CN 表完整性,以及 CN 指向 mcpp-res @@ -75,8 +76,27 @@ namespace = "mcpplibs", name = "capi.lua" -- ❌ 短名仍带点 | `sources` | glob 列表,编入 lib 的源码 | | `cflags` / `cxxflags` / `ldflags` | 追加至对应规则 | | `targets` | `{ ["name"]={ kind="lib"/"bin", main=…, soname=… } }` | -| `features` | `{ ["f"]={ sources={…} } }`,仅识别 sources | -| `deps` | `{ ["ns.name"]="ver" }`,扁平或点号式 | +| `features` | `{ ["f"]={ sources={…}, defines={…}, deps={…}, implies={…}, requires={…} } }`;`defines` 只作用于**包自身**的 TU,消费端若要按 feature 分支须自行声明(见 `tests/examples/openssl`、`openblas` 的 `[target.'cfg(…)'.build] cxxflags`) | +| `deps` | `{ ["ns.name"]="ver" }`,扁平或点号式;feature 内同形 | + +## 索引重定向(`[indices]`) + +测试面要验证的是 **checkout 里的描述符**,而不是已发布的远程索引,这靠 `[indices]` 把命名空间重定向到本仓完成。 + +**根级继承**:workspace 根的 `mcpp.toml` 声明 `[indices] compat = { path = "." }`,相对路径按 **workspace 根**解析(mcpp ≥ 0.0.97,[mcpp#224](https://github.com/mcpp-community/mcpp/issues/224)),成员直接继承,不必各写一份 `path = "../../.."`。 + +**为什么只有一条,而且是 `compat`**: + +- 索引表**按命名空间取键**。声明在一个没有任何依赖会请求的名字下,该索引根本不会被注册,解析会静默回落到已发布的远程索引 —— 此时被测的根本不是这个 checkout。 +- 同一路径声明成多个命名空间确实都会注册,但会变成 N 个各自独立的项目 repo,之后任何查找都以 N 路歧义失败(物理上是同一个描述符;[mcpp#238](https://github.com/mcpp-community/mcpp/issues/238) / [xlings#374](https://github.com/openxlings/xlings/issues/374),在 xlings 0.4.69 后由静默 exit 1 变为响亮报错)。 + +所以根级只能承载一个命名空间,`compat` 是收益最大的那个(13 个成员 vs 其余合计 10 个)。 + +**成员级覆盖**:消费其他命名空间的成员自己声明 `[indices]`,该表**替换**继承来的根级表而非与之合并 —— 这正是每个成员只保留一个项目索引 repo 的机制。 + +**跨命名空间的取舍**:一个成员无法同时从本 checkout 解析两个命名空间。`tests/examples/asio-ssl` 有意利用了这一点:它不写成员级声明、继承根级 `compat`,于是 asio 本身走已发布的远程索引,而它的 `ssl` feature 依赖 `compat.openssl` 从本 checkout 解析 —— 这样**未合并的 compat 描述符可以通过一个已发布的消费者去验证**。反过来,本地 asio 描述符由 `tests/examples/asio-module` 覆盖。 + +**裸名依赖不适用**:重定向按**请求侧**的命名空间取键,而裸写的 `eigen = "5.0.1"` 是以默认命名空间发出的请求,即使最终落到 `compat` 描述符上,也会从远程索引解析。因此各成员一律使用限定写法;裸名解析本身由 mcpp 上游的 e2e 165 覆盖。 ## index 版本契约(index.toml) @@ -101,7 +121,8 @@ mcpp 跑 `xpkg parse`(strict:未知键即失败),所以需要更新文法/键的 - `mirror-cn-reachable`(始终运行):逐个 `curl` CN url,均须返回 200。 - `workspace (linux|macos|windows)`:整个测试面就是一个 mcpp workspace,**唯一的构建/运行通道**—— 没有任何 shell 驱动的例外(公开模块包 imgui/ffmpeg/opencv/tinyhttps 也是普通成员,经成员级 - `[indices] default = { path = "../../.." }` 从 checkout 解析,mcpp ≥ 0.0.97)。 + `[indices] default = { path = "../../.." }` 从 checkout 解析,mcpp ≥ 0.0.97;消费 `compat` 的 + 成员则继承根级声明,见上文「索引重定向」)。 - 选择性成员测试:PR 时由 `git diff` 将改动文件映射到受影响成员 (`pkgs//.lua` → mcpp.toml 引用 `` 的成员;`tests/examples//**` → 成员 ``), 仅 `mcpp test -p ` 这些成员;workflow 本身、workspace 清单非成员部分、`tools/` 等 @@ -135,3 +156,5 @@ done | C++23 module(generated wrapper) | `pkgs/n/nlohmann.json.lua` | `tests/examples/nlohmann.json/` | 同上 / #48 | | header-only + source-gated feature | `pkgs/c/compat.eigen.lua` | `tests/examples/eigen/` | `.agents/docs/2026-06-28-add-eigen-plan.md` / #50 | | header-only(纯头) | `pkgs/c/compat.opengl.lua`、`compat.khrplatform.lua` | — | `.agents/docs/2026-06-03-gl-runtime-packages-plan.md` | +| 外部构建系统(`install()` 驱动) | `pkgs/c/compat.openblas.lua`(Make)、`compat.openssl.lua`(Perl Configure + Make) | `tests/examples/openblas/`、`openssl/` | `docs/superpowers/specs/2026-07-26-openssl-asio-tls-design.md` / #124 | +| feature 拉起依赖(跨包) | `pkgs/c/chriskohlhoff.asio.lua` 的 `ssl` feature → `compat.openssl` | `tests/examples/asio-ssl/` | 同上 | diff --git a/docs/superpowers/specs/2026-07-26-openssl-asio-tls-design.md b/docs/superpowers/specs/2026-07-26-openssl-asio-tls-design.md index 003c799..0c08df5 100644 --- a/docs/superpowers/specs/2026-07-26-openssl-asio-tls-design.md +++ b/docs/superpowers/specs/2026-07-26-openssl-asio-tls-design.md @@ -1,8 +1,38 @@ # Add OpenSSL package + Asio SSL support -> Created: 2026-07-26 · Status: design +> Created: 2026-07-26 · Status: design (superseded in part — see §0) > Repo: `mcpplibs/mcpp-index` · Branch: `feat/add-openssl-asio-tls` +## 0. Corrections after landing + +The design below is kept as written; three of its claims did not survive +contact with the implementation, and the descriptor now differs accordingly. + +1. **`name = "compat.openssl"` → `name = "openssl"`.** SPEC-001 (mcpp 0.0.106) + makes `name` a single atomic segment with all hierarchy in `namespace`; the + fully-qualified spelling is only a compatible legacy form. + +2. **`MCPP_FEATURE_SSL` is NOT propagated to consumer TUs.** A feature's + `defines` apply to the package's own translation units. The original + `tests/ssl.cpp` keyed its whole body off that macro and therefore compiled + to nothing — it passed while asserting nothing. A consumer that branches on + a feature declares its own macro (`tests/examples/asio-ssl` sets + `HAVE_ASIO_SSL` in its `[target.'cfg(…)'.build] cxxflags`), which is the + same shape the openblas member already used. + +3. **`./config` needs an explicit `--libdir=lib`.** Unset, OpenSSL derives it + as `lib$target{multilib}`, and `linux-x86_64` declares `multilib => "64"` — + so the archives install to `$prefix/lib64` while `-Llib` and the post-build + check look at `$prefix/lib`. linux-aarch64 and both darwin64 targets declare + no multilib, which is why the build validated on an arm64 Mac and was still + broken for every x86_64 Linux consumer. + +Also changed while landing: linux links `-l:libssl.a -l:libcrypto.a` (naming +the archive rather than letting the driver resolve a name that a shared object +would win) plus `-ldl -lpthread`; the build log moved into the install prefix +so it survives a failed build; `RANLIB` is pinned only on macOS, which is the +platform that needs it; and `install()` probes for `perl` up front. + ## 1. Motivation The mcpp ecosystem needs TLS support. `chriskohlhoff.asio@1.38.1` (C++23 diff --git a/index.toml b/index.toml index da3834c..2915961 100644 --- a/index.toml +++ b/index.toml @@ -7,5 +7,5 @@ # "floor first, new grammar after" rollout rule mechanically. [index] spec = "1" -min_mcpp = "0.0.108" -latest_mcpp = "0.0.108" +min_mcpp = "0.0.109" +latest_mcpp = "0.0.109" diff --git a/mcpp.toml b/mcpp.toml index ba2835d..90fd673 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,15 +1,15 @@ # mcpp-index is simultaneously the package index (pkgs/) AND a mcpp workspace that # tests it: a virtual workspace whose members are per-library TEST projects under -# tests/examples/ — each depends on this repo's own packages via a per-member -# local [indices] redirect and asserts behavior in tests/*.cpp. The whole suite runs as a -# single shell-free command, `mcpp test --workspace` (or `mcpp test -p `). -# "Eat our own dog food." See -# .agents/docs/2026-06-29-mcpp-native-workspace-ci-design.md and +# tests/examples/ — each consumes this repo's own packages and asserts behavior in +# tests/*.cpp. The whole suite runs as a single shell-free command, +# `mcpp test --workspace` (or `mcpp test -p `). "Eat our own dog food." +# See .agents/docs/2026-06-29-mcpp-native-workspace-ci-design.md and # mcpp .agents/docs/2026-06-30-workspace-test-and-zero-shell-index-design.md. [workspace] members = [ "tests/examples/archive", "tests/examples/asio-module", + "tests/examples/asio-ssl", "tests/examples/build-mcpp", "tests/examples/cjson", "tests/examples/core", @@ -24,6 +24,7 @@ members = [ "tests/examples/marzer.tomlplusplus", "tests/examples/nlohmann.json", "tests/examples/openblas", + "tests/examples/openssl", "tests/examples/opencv-module", "tests/examples/opencv-module-dnn", "tests/examples/opencv-module-unifont", @@ -32,10 +33,33 @@ members = [ "tests/examples/tinyhttps", ] - -# NOTE: each member declares exactly ONE `[indices] = { path = "../../.." }` -# entry for the namespace it consumes (module packages use `default`, the -# builtin mcpplibs namespace — mcpp >= 0.0.97). Root-level centralization of -# these redirects (mcpp #224) is deliberately NOT used yet: xlings fails -# silently when a project has more than one project-scoped index repo -# (mcpp-community/mcpp#238); revisit once fixed. +# ── The index redirect, hoisted to the workspace root ─────────────────── +# Members inherit this, and the relative `path` resolves against the +# WORKSPACE ROOT rather than the member directory (mcpp#224, fixed in +# 0.0.97) — so the 13 members that consume `compat` no longer each repeat +# `path = "../../.."`, and member depth stops mattering. +# +# Why exactly ONE entry, and why `compat` specifically: +# +# * The key is looked up by NAMESPACE. An index declared under a name that +# is not a namespace any dependency asks for is never registered, and +# resolution silently falls through to the published remote index — the +# checkout under test is then not what is being tested at all. +# * Declaring the same path under several namespaces does register them, +# as N separate project repos, and every lookup afterwards fails with an +# N-way ambiguity between what is physically one descriptor +# (mcpp#238 / xlings#374 — now a loud error rather than a silent exit 1). +# +# So the root can carry exactly one namespace, and `compat` is the one that +# pays: 13 members against 10 for everything else combined. A member that +# needs a different namespace declares its own `[indices]`, which REPLACES +# this table rather than merging with it — which is precisely what keeps it +# at one repo per member. +# +# A member inheriting this still resolves OTHER namespaces from the +# published remote index. tests/examples/asio-ssl leans on that deliberately: +# asio itself comes from the published index while its `ssl` feature dep, +# compat.openssl, resolves from this checkout — which is what lets an +# unmerged compat descriptor be tested through a feature that activates it. +[indices] +compat = { path = "." } diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index b7c6e20..9d38dd9 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -7,6 +7,15 @@ -- build (build-dep `xim:make@latest`) and lays the lib + headers under the -- install dir. -- +-- HOST REQUIREMENT — perl. `./config` IS a Perl script, and there is no +-- `xim:perl` to declare as a build dep, so this is the one thing the package +-- cannot bring itself. CI runners and every mainstream distro ship it; a +-- stripped container may not. install() probes for it up front and fails with +-- that sentence rather than letting `./config` die with a shell error nobody +-- can read. (This is also why mbun's OpenSSL package chose a vendored prebuilt +-- over a source build; here a source build is the only option that covers both +-- linux and macOS.) +-- -- Platforms: -- * linux/macosx — build a fully static libcrypto.a + libssl.a from source -- via install() hook (anchor-triggered build, same pattern as compat.openblas). @@ -14,7 +23,7 @@ package = { spec = "1", namespace = "compat", - name = "compat.openssl", + name = "openssl", description = "OpenSSL — TLS/crypto library (static, install()-driven build)", licenses = {"Apache-2.0"}, repo = "https://github.com/openssl/openssl", @@ -58,8 +67,30 @@ package = { include_dirs = { "include" }, deps = { }, - -- -lssl must precede -lcrypto (libssl depends on libcrypto). - linux = { ldflags = { "-Llib", "-lssl", "-lcrypto" } }, + linux = { + ldflags = { + "-Llib", + -- `-l:` names the file and goes straight to ld, so the + -- static archive is what gets linked no matter what else is on + -- the search path. Plain `-lssl` asks the driver to *resolve* a + -- name, and a resolver prefers a shared object — one stray -L + -- ahead of ours (a toolchain sysroot, a distro multiarch dir) + -- and the link silently picks up the host libssl.so.3, giving + -- the consumer a runtime dependency this package exists to + -- avoid. ssl precedes crypto: libssl depends on libcrypto. + "-l:libssl.a", + "-l:libcrypto.a", + -- Static libcrypto's own system deps. Configured `no-dso + -- no-engine` (so no dlopen) and glibc >= 2.34 folds both into + -- libc, which is why CI links without them — but musl and + -- older glibc still need them spelled out. + "-ldl", + "-lpthread", + }, + }, + -- macOS: ld64 has no `-l:` spelling. lib/ holds only the .a archives + -- this package built, so name resolution has nothing else to find, and + -- libSystem already carries dl/pthread. macosx = { ldflags = { "-Llib", "-lssl", "-lcrypto" } }, }, } @@ -80,8 +111,24 @@ local function resolve_make() return "make" end +local function have(tool) + return pcall(function() + os.exec(string.format("bash -c %s", + sh_quote("command -v " .. tool .. " >/dev/null 2>&1"))) + end) +end + local function _install_impl() - -- The fetched tarball unpacks to openssl-/ beside the archive. + if not have("perl") then + log.error("compat.openssl: `perl` not found on PATH. OpenSSL's " + .. "./config is a Perl script and there is no xim:perl build " + .. "dep to fall back on — install perl and retry.") + return false + end + + -- The fetched tarball unpacks to openssl-/ beside the archive. Every + -- command below cd's into srcroot itself, so the process cwd is left alone + -- (an os.cd here would break the relative-path fallback on the next line). local ifile = pkginfo.install_file() local srcroot = ifile and tostring(ifile):replace(".tar.gz", "") or ("openssl-" .. pkginfo.version()) @@ -89,36 +136,51 @@ local function _install_impl() srcroot = "openssl-" .. pkginfo.version() end + -- Build in the extracted source directory with --prefix pointing at a + -- clean install directory. Building in-place (prefix == srcroot) makes + -- `make install_sw` fail with "cp: source and dest are identical". + -- + -- The prefix is emptied HERE, before the build, so the build log can live + -- inside it: a log that a later os.tryrm would delete, or one left in the + -- transient srcroot, is gone exactly when a failed build needs reading. + -- xim's interface mode swallows subprocess stdout, so this file is the + -- only record of what the compiler said. local prefix = pkginfo.install_dir() - local logf = path.join(srcroot, "mcpp_openssl_build.log") - - -- Build in the extracted source directory (srcroot) with --prefix - -- pointing to the clean install directory (prefix). This avoids the - -- in-source "cp: source and dest are identical" failure that happens - -- when prefix == srcroot. - os.cd(srcroot) + os.tryrm(prefix) + os.mkdir(prefix) + local logf = path.join(prefix, "mcpp_openssl_build.log") -- Static-only build: no shared libs, no DSO, no tests, no apps, no engine. - -- `./config` auto-detects the target platform (equivalent to - -- `perl Configure `). + -- `./config` auto-detects the target (equivalent to `perl Configure + -- `). + -- + -- --libdir=lib is NOT cosmetic. Left unset, OpenSSL derives the install + -- libdir as "lib$target{multilib}" (Configurations/unix-Makefile.tmpl), + -- and Configurations/10-main.conf gives linux-x86_64 `multilib => "64"`. + -- So on the single most common target the archives land in $prefix/lib64 + -- while `-Llib` and the check below look at $prefix/lib. linux-aarch64 and + -- both darwin64 targets declare no multilib and resolve to plain "lib" — + -- which is how a source build can pass on an arm64 Mac and still be broken + -- for everyone on x86_64 Linux. local make = resolve_make() local jobs = (os.default_njob and os.default_njob()) or 4 local flags = "no-shared no-dso no-tests no-apps no-engine" os.exec(string.format("bash -c %s", sh_quote(string.format( - "cd %s && ./config --prefix=%s %s >> %s 2>&1", + "cd %s && ./config --prefix=%s --libdir=lib %s >> %s 2>&1", sh_quote(srcroot), sh_quote(prefix), flags, sh_quote(logf))))) os.exec(string.format("bash -c %s", sh_quote(string.format( "cd %s && %s -j%d >> %s 2>&1", sh_quote(srcroot), make, jobs, sh_quote(logf))))) - -- Install to the clean prefix directory. - -- Override RANLIB to use the system ranlib (which supports the macOS - -- `-c` flag). LLVM's llvm-ranlib (injected into PATH by the toolchain) - -- rejects `-c`, which causes install_dev to fail. - os.tryrm(prefix) + -- macOS only: `make install_dev` runs `$(RANLIB) -c`, and the toolchain + -- puts llvm-ranlib (which rejects -c) ahead of the system one on PATH. + -- Pinning an absolute /usr/bin/ranlib is itself a host assumption, so it + -- is confined to the platform that needs it — a Linux container without + -- /usr/bin/ranlib would otherwise fail install_sw for no reason. + local ranlib = (os.host() == "macosx") and "RANLIB=/usr/bin/ranlib " or "" os.exec(string.format("bash -c %s", sh_quote(string.format( - "cd %s && %s RANLIB=/usr/bin/ranlib install_sw >> %s 2>&1", - sh_quote(srcroot), make, sh_quote(logf))))) + "cd %s && %s%s install_sw >> %s 2>&1", + sh_quote(srcroot), ranlib, make, sh_quote(logf))))) -- Verify the build produced the expected archives. local libdir = path.join(prefix, "lib") @@ -126,7 +188,7 @@ local function _install_impl() local ssl_a = path.join(libdir, "libssl.a") if not os.isfile(crypto_a) or not os.isfile(ssl_a) then log.error("compat.openssl: build produced no libcrypto.a / libssl.a " - .. "(see %s)", logf) + .. "under %s (see %s)", libdir, logf) return false end @@ -139,7 +201,9 @@ local function _install_impl() end function install() - -- Windows is deferred. + -- Windows is deferred: there is no windows xpm block, so version + -- resolution already fails before this point. Kept as a named error in + -- case a windows entry is added before this hook learns to build there. if os.host() == "windows" then log.error("compat.openssl: windows is not yet supported") return false diff --git a/tests/examples/archive/mcpp.toml b/tests/examples/archive/mcpp.toml index 07d6652..4866c3b 100644 --- a/tests/examples/archive/mcpp.toml +++ b/tests/examples/archive/mcpp.toml @@ -4,9 +4,6 @@ name = "archive-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] libarchive = "3.8.7" zlib = "1.3.2" diff --git a/tests/examples/asio-module/mcpp.toml b/tests/examples/asio-module/mcpp.toml index 5b0bb60..d1b20ab 100644 --- a/tests/examples/asio-module/mcpp.toml +++ b/tests/examples/asio-module/mcpp.toml @@ -1,11 +1,15 @@ # Asio C++23-module consumer test project: `import std; import asio;`. # The public package is `chriskohlhoff.asio@1.38.1` (module-only). +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `chriskohlhoff`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +chriskohlhoff = { path = "../../.." } + [package] name = "asio-module-tests" version = "0.1.0" -[indices] -chriskohlhoff = { path = "../../.." } - [dependencies.chriskohlhoff] asio = "1.38.1" diff --git a/tests/examples/asio-ssl/mcpp.toml b/tests/examples/asio-ssl/mcpp.toml new file mode 100644 index 0000000..7f9ef40 --- /dev/null +++ b/tests/examples/asio-ssl/mcpp.toml @@ -0,0 +1,41 @@ +# chriskohlhoff.asio `ssl` FEATURE test — the end-to-end assertion for the +# opt-in TLS surface: activating the feature compiles asio_ssl.cpp into the +# package, pulls compat.openssl (3.5.1, built from source), and links +# libssl.a + libcrypto.a. tests/ssl.cpp then runs a real TLS handshake over +# loopback. +# +# Why this is its own member rather than a flag on tests/examples/asio-module: +# features are resolved per consuming project, so one project cannot have asio +# both with and without `ssl`; and the base member must keep asserting the +# no-ssl build, which is what nearly every consumer takes. +# +# It needs BOTH `chriskohlhoff` and `compat`, and a project can only carry one +# index repo — a second one pointing at the same tree makes every lookup +# ambiguous (xlings#374). So the split is deliberate: this member declares NO +# member-level [indices] and inherits the root's `compat`, which is the +# namespace of the descriptor under test; asio itself is already published and +# resolves from the remote index. The local asio descriptor, including this +# feature's own wiring, is covered by tests/examples/asio-module. +# +# That inversion is the point — a feature dep resolves through whatever index +# is configured for ITS namespace, so an unmerged compat descriptor can be +# exercised through a published consumer that activates it. +# +# linux + macOS only: compat.openssl has no windows xpm entry (prebuilt MSVC +# archives not yet published), so on windows the member carries no dependency +# and tests/ssl.cpp compiles to a no-op main(). +[package] +name = "asio-ssl-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.chriskohlhoff] +asio = { version = "1.38.1", features = ["ssl"] } + +[target.'cfg(linux)'.build] +cxxflags = ["-DHAVE_ASIO_SSL=1"] + +[target.'cfg(macos)'.dependencies.chriskohlhoff] +asio = { version = "1.38.1", features = ["ssl"] } + +[target.'cfg(macos)'.build] +cxxflags = ["-DHAVE_ASIO_SSL=1"] diff --git a/tests/examples/asio-module/tests/ssl.cpp b/tests/examples/asio-ssl/tests/ssl.cpp similarity index 89% rename from tests/examples/asio-module/tests/ssl.cpp rename to tests/examples/asio-ssl/tests/ssl.cpp index d6fb2d0..be157c7 100644 --- a/tests/examples/asio-module/tests/ssl.cpp +++ b/tests/examples/asio-ssl/tests/ssl.cpp @@ -2,11 +2,18 @@ // async handshake → write/read round-trip. PEM cert+key embedded as // string literals (use_certificate / use_private_key from const_buffer). // -// MCPP_FEATURE_SSL guards the test body; when the ssl feature is not -// active, the test is a trivially-passing no-op. (The `ssl` feature -// cannot be activated in the workspace while compat.openssl is -// unpublished because xlings#374 prevents two local-path index repos.) -#ifdef MCPP_FEATURE_SSL +// This is the real end-to-end assertion for chriskohlhoff.asio's `ssl` +// feature: the member activates it, which pulls in compat.openssl, compiles +// asio_ssl.cpp, and links libssl.a/libcrypto.a. +// +// HAVE_ASIO_SSL is set by THIS project's own [target.'cfg(...)'.build] +// cxxflags, not by the package's feature. A feature's `defines` apply to the +// package's own translation units; a consumer that keys its source off one has +// to declare it itself (same shape as the openblas member's HAVE_OPENBLAS). +// Getting that backwards is how the previous version of this file passed while +// compiling to nothing. Windows has no openssl dep yet, so there the whole +// body drops out and main() is a no-op. +#ifdef HAVE_ASIO_SSL import std; import asio; @@ -146,5 +153,5 @@ int main() { return failure ? failure : (client_done ? 0 : 8); } #else -int main() { return 0; } +int main() { return 0; } // ssl feature inactive (windows); no-op #endif diff --git a/tests/examples/cjson/mcpp.toml b/tests/examples/cjson/mcpp.toml index 8f281c8..d32122c 100644 --- a/tests/examples/cjson/mcpp.toml +++ b/tests/examples/cjson/mcpp.toml @@ -5,8 +5,5 @@ name = "cjson-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] cjson = "1.7.19" diff --git a/tests/examples/core/mcpp.toml b/tests/examples/core/mcpp.toml index b171e0e..e506e68 100644 --- a/tests/examples/core/mcpp.toml +++ b/tests/examples/core/mcpp.toml @@ -5,9 +5,6 @@ name = "core-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] gtest = { version = "1.15.2", features = ["main"] } ftxui = "6.1.9" diff --git a/tests/examples/eigen/mcpp.toml b/tests/examples/eigen/mcpp.toml index f8bb57a..5ae0f41 100644 --- a/tests/examples/eigen/mcpp.toml +++ b/tests/examples/eigen/mcpp.toml @@ -1,12 +1,18 @@ # Eigen test project: header-only linear algebra, asserted under `mcpp test`. # Resolves compat.eigen (source build puts the tree root on the include path) -# from this repo's own index. +# from this repo's own index, via the inherited workspace-root `[indices]`. +# +# The dependency is spelled QUALIFIED on purpose. A bare `eigen = "5.0.1"` is +# what most real consumers write, and it does resolve (0.0.109 takes both +# halves of the wire address from the accepted descriptor, mcpp#286) — but the +# index redirect is keyed by the REQUEST's namespace, and a bare request asks +# under the default namespace, not `compat`. It would therefore resolve from +# the published remote index and quietly stop testing this checkout, which is +# the one thing this member exists to do. Bare-name resolution against a +# short-name index is covered upstream by mcpp's e2e 165. [package] name = "eigen-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] eigen = "5.0.1" diff --git a/tests/examples/ffmpeg-module/mcpp.toml b/tests/examples/ffmpeg-module/mcpp.toml index 392239d..c18d4d9 100644 --- a/tests/examples/ffmpeg-module/mcpp.toml +++ b/tests/examples/ffmpeg-module/mcpp.toml @@ -1,20 +1,23 @@ # Public `ffmpeg` C++23-module package test (bare-name consumption from the # builtin default namespace — resolved against THIS checkout via the -# member-level `[indices] default` redirect, mcpp >= 0.0.97; kept per-member -# and single-entry because xlings breaks with >1 project index repo, mcpp#238). Replaces the +# workspace-root `[indices]` redirect, which every member inherits). Replaces the # former tests/smoke_ffmpeg_module.sh reseeding driver. The package is # The package builds on linux + macOS + windows (compat.ffmpeg 3-platform). +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "ffmpeg-module-tests" version = "0.1.0" # The module package's own transitive dep (compat.ffmpeg) resolves from the -# GLOBAL published compat index (the compat namespace is not redirected here -# — see the mcpp#238 single-repo constraint); the compat descriptor itself is -# pre-merge-validated by its dedicated member. -[indices] -default = { path = "../../.." } - +# GLOBAL published compat index: this member redirects `default`, and a +# member-level [indices] replaces the root's `compat` rather than adding to +# it. The compat descriptor itself is pre-merge-validated by its own member. [target.'cfg(linux)'.dependencies] ffmpeg = "0.0.3" diff --git a/tests/examples/ffmpeg/mcpp.toml b/tests/examples/ffmpeg/mcpp.toml index dea9cbb..fbd406a 100644 --- a/tests/examples/ffmpeg/mcpp.toml +++ b/tests/examples/ffmpeg/mcpp.toml @@ -8,9 +8,6 @@ name = "ffmpeg-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [target.'cfg(linux)'.dependencies.compat] ffmpeg = "8.1.2" diff --git a/tests/examples/fmtlib.fmt/mcpp.toml b/tests/examples/fmtlib.fmt/mcpp.toml index 118f1ed..a2dc0f5 100644 --- a/tests/examples/fmtlib.fmt/mcpp.toml +++ b/tests/examples/fmtlib.fmt/mcpp.toml @@ -1,14 +1,15 @@ # {fmt} test project: consumes the C++23 module `fmt` (`import fmt;`, no # #include) from the `fmtlib.fmt` package and asserts behavior under `mcpp test`. -# Part of the mcpp-index self-referential workspace: `[indices] fmtlib` points at -# this repo, so the dependency resolves to the checked-in recipe (pkgs/f/ +# Part of the mcpp-index self-referential workspace: the workspace-root +# `[indices]` redirect points at this repo, so the dependency resolves to the +# checked-in recipe (pkgs/f/ # fmtlib.fmt.lua) rather than a remote index. The exported module is plain `fmt`. +[indices] +fmtlib = { path = "../../.." } + [package] name = "fmtlib-fmt-tests" version = "0.1.0" -[indices] -fmtlib = { path = "../../.." } - [dependencies.fmtlib] fmt = "12.2.0" diff --git a/tests/examples/gui-stack/mcpp.toml b/tests/examples/gui-stack/mcpp.toml index 9847e56..fb4aba1 100644 --- a/tests/examples/gui-stack/mcpp.toml +++ b/tests/examples/gui-stack/mcpp.toml @@ -6,9 +6,6 @@ name = "gui-stack-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [target.'cfg(linux)'.dependencies.compat] glfw = "3.4" x11 = "1.8.13" diff --git a/tests/examples/imgui-module/mcpp.toml b/tests/examples/imgui-module/mcpp.toml index 02458e5..d2e3d74 100644 --- a/tests/examples/imgui-module/mcpp.toml +++ b/tests/examples/imgui-module/mcpp.toml @@ -1,19 +1,22 @@ # Public `imgui` C++23-module package test (bare-name consumption from the # builtin default namespace — resolved against THIS checkout via the -# member-level `[indices] default` redirect, mcpp >= 0.0.97; kept per-member -# and single-entry because xlings breaks with >1 project index repo, mcpp#238). Replaces the +# workspace-root `[indices]` redirect, which every member inherits). Replaces the # former tests/smoke_imgui_module.sh reseeding driver. Linux-gated to the # validated surface: the package descriptor covers three platforms, but only # linux has ever been exercised by index CI; off-Linux the test compiles to a # no-op main(). Toolchain: workspace default (gcc@16.1.0) — the old smoke's # llvm@20.1.7 pin predates gcc16 module support; imgui-m upstream CI builds # the module layer with gcc@16.1.0. +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "imgui-module-tests" version = "0.1.0" -[indices] -default = { path = "../../.." } - [target.'cfg(linux)'.dependencies] imgui = "0.0.1" diff --git a/tests/examples/imgui-window/mcpp.toml b/tests/examples/imgui-window/mcpp.toml index 7b37285..6878de3 100644 --- a/tests/examples/imgui-window/mcpp.toml +++ b/tests/examples/imgui-window/mcpp.toml @@ -5,9 +5,6 @@ name = "imgui-window-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [target.'cfg(linux)'.dependencies.compat] imgui = "1.92.8" glfw = "3.4" diff --git a/tests/examples/imgui/mcpp.toml b/tests/examples/imgui/mcpp.toml index 6a275ea..84a254b 100644 --- a/tests/examples/imgui/mcpp.toml +++ b/tests/examples/imgui/mcpp.toml @@ -3,8 +3,5 @@ name = "imgui-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] imgui = "1.92.8" diff --git a/tests/examples/marzer.tomlplusplus/mcpp.toml b/tests/examples/marzer.tomlplusplus/mcpp.toml index 6aadc4f..0c1ceb7 100644 --- a/tests/examples/marzer.tomlplusplus/mcpp.toml +++ b/tests/examples/marzer.tomlplusplus/mcpp.toml @@ -1,11 +1,15 @@ # toml++ test project: consumes the C++23 module `tomlplusplus` and asserts # parse + typed access + round-trip serialization under `mcpp test`. +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `marzer`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +marzer = { path = "../../.." } + [package] name = "tomlplusplus-tests" version = "0.1.0" -[indices] -marzer = { path = "../../.." } - [dependencies.marzer] tomlplusplus = "3.4.0" diff --git a/tests/examples/nlohmann.json/mcpp.toml b/tests/examples/nlohmann.json/mcpp.toml index 38eda95..359cad7 100644 --- a/tests/examples/nlohmann.json/mcpp.toml +++ b/tests/examples/nlohmann.json/mcpp.toml @@ -1,11 +1,15 @@ # nlohmann/json test project: consumes the C++23 module `nlohmann.json` and # asserts round-trip + ordered_json under `mcpp test`. +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `nlohmann`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +nlohmann = { path = "../../.." } + [package] name = "nlohmann-json-tests" version = "0.1.0" -[indices] -nlohmann = { path = "../../.." } - [dependencies.nlohmann] json = "3.12.0" diff --git a/tests/examples/openblas/mcpp.toml b/tests/examples/openblas/mcpp.toml index 1ac75c6..e516d6c 100644 --- a/tests/examples/openblas/mcpp.toml +++ b/tests/examples/openblas/mcpp.toml @@ -6,9 +6,6 @@ name = "openblas-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [target.'cfg(windows)'.dependencies.compat] openblas = "0.3.33" [target.'cfg(windows)'.build] diff --git a/tests/examples/opencv-module-dnn/mcpp.toml b/tests/examples/opencv-module-dnn/mcpp.toml index b6c6d88..6318ef9 100644 --- a/tests/examples/opencv-module-dnn/mcpp.toml +++ b/tests/examples/opencv-module-dnn/mcpp.toml @@ -2,13 +2,17 @@ # ["dnn"] }` builds the underlying dnn sources and compiles opencv-m's # opencv.dnn interface, so `import opencv.dnn;` is available. All three # platforms (mlas on linux/macOS, built-in fast_gemm on windows). +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "opencv-module-dnn-tests" version = "0.1.0" -[indices] -default = { path = "../../.." } - [target.'cfg(linux)'.dependencies] opencv = { version = "0.0.10", features = ["dnn"] } diff --git a/tests/examples/opencv-module-unifont/mcpp.toml b/tests/examples/opencv-module-unifont/mcpp.toml index 593ede2..fd9d255 100644 --- a/tests/examples/opencv-module-unifont/mcpp.toml +++ b/tests/examples/opencv-module-unifont/mcpp.toml @@ -1,13 +1,17 @@ # Public `opencv` module package `unifont` FEATURE test: `opencv = { features = # ["unifont"] }` embeds the CJK font (no new module surface), so FontFace("uni") # renders CJK through `import opencv.cv;`. Exercised on linux and macOS. +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "opencv-module-unifont-tests" version = "0.1.0" -[indices] -default = { path = "../../.." } - [target.'cfg(linux)'.dependencies] opencv = { version = "0.0.9", features = ["unifont"] } diff --git a/tests/examples/opencv-module/mcpp.toml b/tests/examples/opencv-module/mcpp.toml index c04ab5f..6e54173 100644 --- a/tests/examples/opencv-module/mcpp.toml +++ b/tests/examples/opencv-module/mcpp.toml @@ -1,20 +1,25 @@ # Public `opencv` C++23-module package test (bare-name consumption from the # builtin default namespace — resolved against THIS checkout via the -# member-level `[indices] default` redirect, mcpp >= 0.0.97; kept per-member -# and single-entry because xlings breaks with >1 project index repo, mcpp#238). +# workspace-root `[indices]` redirect, which every member inherits). # Since opencv 0.0.7 the package is single-repo: the OpenCV sources are # vendored in its own release tarball, so this member exercises the whole # source build on all three platforms. +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "opencv-module-tests" version = "0.1.0" # The package's only remaining transitive dep is compat.ffmpeg (videoio -# backend), which resolves from the GLOBAL published compat index — the -# compat namespace is not redirected here, see the mcpp#238 constraint. -[indices] -default = { path = "../../.." } - +# backend), which resolves from the GLOBAL published compat index: this +# member redirects `default`, and a member-level [indices] replaces the +# root's `compat` rather than adding to it. The compat descriptor itself is +# pre-merge-validated by its own dedicated member. [target.'cfg(linux)'.dependencies] opencv = "0.0.10" diff --git a/tests/examples/openssl/mcpp.toml b/tests/examples/openssl/mcpp.toml new file mode 100644 index 0000000..ebe8c4c --- /dev/null +++ b/tests/examples/openssl/mcpp.toml @@ -0,0 +1,27 @@ +# compat.openssl test project — the dedicated member that validates the +# from-source OpenSSL build (static libssl.a + libcrypto.a + headers). +# +# It inherits the workspace-root `compat` redirect, so the descriptor in THIS +# checkout is what gets built and linked. tests/examples/asio-ssl covers the +# other direction — the same package pulled in as asio's `ssl` feature dep — +# and this member is what isolates a failure to openssl itself rather than to +# the feature wiring around it. +# +# linux + macOS only: there is no windows xpm entry yet (prebuilt MSVC archives +# unpublished), so on windows the member carries no dependency and the test +# compiles to a no-op main(). +[package] +name = "openssl-tests" +version = "0.1.0" + +[target.'cfg(linux)'.dependencies.compat] +openssl = "3.5.1" + +[target.'cfg(linux)'.build] +cxxflags = ["-DHAVE_OPENSSL=1"] + +[target.'cfg(macos)'.dependencies.compat] +openssl = "3.5.1" + +[target.'cfg(macos)'.build] +cxxflags = ["-DHAVE_OPENSSL=1"] diff --git a/tests/examples/openssl/tests/tls.cpp b/tests/examples/openssl/tests/tls.cpp new file mode 100644 index 0000000..dba1970 --- /dev/null +++ b/tests/examples/openssl/tests/tls.cpp @@ -0,0 +1,50 @@ +// compat.openssl end-to-end: headers resolve, both archives link, and the +// library initialises far enough to stand up a TLS context and run a digest. +// +// Touching BOTH archives is the point: libssl.a (SSL_CTX_new, TLS_method) and +// libcrypto.a (EVP_*). A link that silently dropped one, or picked up a host +// libssl.so instead of the package's own static build, fails here. +// +// HAVE_OPENSSL comes from this project's own cfg-gated cxxflags — the package +// is linux/macOS-only, so elsewhere this file is an empty main(). +#ifdef HAVE_OPENSSL +#include +#include +#include + +#include + +int main() { + // Built from the 3.5.1 tarball this descriptor pins. + if (OPENSSL_VERSION_MAJOR != 3 || OPENSSL_VERSION_MINOR != 5) return 1; + + // libssl: a usable client context. + SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); + if (ctx == nullptr) return 2; + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, nullptr); + SSL* ssl = SSL_new(ctx); + if (ssl == nullptr) { SSL_CTX_free(ctx); return 3; } + SSL_free(ssl); + SSL_CTX_free(ctx); + + // libcrypto: SHA-256 of "abc" — first four bytes of the known digest. + unsigned char digest[EVP_MAX_MD_SIZE] = {}; + unsigned int len = 0; + EVP_MD_CTX* md = EVP_MD_CTX_new(); + if (md == nullptr) return 4; + if (EVP_DigestInit_ex(md, EVP_sha256(), nullptr) != 1 + || EVP_DigestUpdate(md, "abc", 3) != 1 + || EVP_DigestFinal_ex(md, digest, &len) != 1) { + EVP_MD_CTX_free(md); + return 5; + } + EVP_MD_CTX_free(md); + if (len != 32) return 6; + + const unsigned char expected[4] = {0xba, 0x78, 0x16, 0xbf}; + if (std::memcmp(digest, expected, sizeof expected) != 0) return 7; + return 0; +} +#else +int main() { return 0; } // compat.openssl is linux/macOS-only; no-op elsewhere +#endif diff --git a/tests/examples/spdlog-compiled/mcpp.toml b/tests/examples/spdlog-compiled/mcpp.toml index bd84f26..42209d1 100644 --- a/tests/examples/spdlog-compiled/mcpp.toml +++ b/tests/examples/spdlog-compiled/mcpp.toml @@ -12,8 +12,5 @@ name = "spdlog-compiled-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] spdlog = { version = "1.17.0", features = ["compiled"] } diff --git a/tests/examples/spdlog/mcpp.toml b/tests/examples/spdlog/mcpp.toml index 4de1a1d..7ae9ef9 100644 --- a/tests/examples/spdlog/mcpp.toml +++ b/tests/examples/spdlog/mcpp.toml @@ -1,15 +1,13 @@ # spdlog test project: consumes compat.spdlog in its DEFAULT header-only mode # (bundled fmt, no external dependency) and asserts behavior under `mcpp test`. -# Part of the mcpp-index self-referential workspace: `[indices] compat` points -# at this repo, so the dependency resolves to the checked-in recipe +# Part of the mcpp-index self-referential workspace: the workspace-root +# `[indices]` redirect points at this repo, so the dependency resolves to the +# checked-in recipe # (pkgs/c/compat.spdlog.lua). Compiled mode is covered by the sibling member # tests/examples/spdlog-compiled/. [package] name = "spdlog-tests" version = "0.1.0" -[indices] -compat = { path = "../../.." } - [dependencies.compat] spdlog = "1.17.0" diff --git a/tests/examples/tinyhttps/mcpp.toml b/tests/examples/tinyhttps/mcpp.toml index 5b222fc..3714f66 100644 --- a/tests/examples/tinyhttps/mcpp.toml +++ b/tests/examples/tinyhttps/mcpp.toml @@ -1,9 +1,13 @@ +# Overrides the workspace-root redirect: root declares `compat`, this member +# needs `default`. A member-level [indices] REPLACES the inherited table rather +# than merging with it, which is what keeps this to ONE project index repo — +# two repos pointing at the same tree make every lookup ambiguous. +[indices] +default = { path = "../../.." } + [package] name = "tinyhttps-tests" version = "0.1.0" -[indices] -default = { path = "../../.." } - [dependencies] tinyhttps = "0.2.9" From 9ecf16fed34f64be13241bdfd7d885daa1318531 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 27 Jul 2026 00:03:49 +0800 Subject: [PATCH 2/4] fix(openssl): declare xim:make on macosx, and print the log tail on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS workspace job failed on both new members ~43s after the tarball landed — far too early for a compile, and with nothing to go on: install() writes everything to an on-disk log (xim's interface mode swallows subprocess stdout) and xlings surfaces the failure as a bare `E_INTERNAL: [openssl] failed:`. Two changes, one for the likely cause and one so the next failure says what it was. macosx now declares `deps = { "xim:make@latest" }`. The previous comment claimed it was unnecessary because "macOS ships GNU Make at /usr/bin/make" — it ships GNU Make **3.81**, the last GPLv2 release, frozen in 2006, and OpenSSL 3.x's generated Makefile does not build with it. That matches the timing exactly: ~30s of Perl Configure, then make failing on sight. compat.openblas already declares this dep on macosx. Each build step now runs through a helper that, on failure, names the step and prints the last 40 lines of the log. The log also opens with `make --version`, because 3.81-vs-4.x is precisely the distinction that is invisible after the fact. The tail is passed as one pre-formatted argument rather than a format string — build output contains `%` often enough that formatting it is its own failure mode. Verified on linux (cold): log opens with "GNU Make 4.3", openssl and asio-ssl members both pass. --- pkgs/c/compat.openssl.lua | 62 ++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index 9d38dd9..a03013d 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -41,8 +41,12 @@ package = { }, }, macosx = { - -- No xim:make dep: macOS ships GNU Make at /usr/bin/make; - -- resolve_make() falls back to PATH when the build dep is absent. + -- xim:make is declared here for the same reason as linux, and NOT + -- left to PATH: macOS does ship a /usr/bin/make, but it is GNU Make + -- 3.81 (the last GPLv2 release, frozen in 2006), and OpenSSL 3.x's + -- generated Makefile does not build with it. compat.openblas + -- declares the same dep on macosx. + deps = { "xim:make@latest" }, ["3.5.1"] = { url = { GLOBAL = "https://github.com/openssl/openssl/releases/download/openssl-3.5.1/openssl-3.5.1.tar.gz", @@ -118,6 +122,35 @@ local function have(tool) end) end +-- Last `n` lines of the build log, or nil if it cannot be read. +local function tail_lines(file, n) + local ok, content = pcall(io.readfile, file) + if not ok or not content then return nil end + local lines = {} + for line in (tostring(content) .. "\n"):gmatch("(.-)\n") do + lines[#lines + 1] = line + end + if #lines == 0 then return nil end + return table.concat(lines, "\n", math.max(1, #lines - n + 1), #lines) +end + +-- Run one build step, and on failure print the tail of the log with it. +-- +-- Everything the build says goes to an on-disk log (xim's interface mode +-- swallows subprocess stdout), and xlings surfaces a failed install() as a +-- bare `E_INTERNAL: [openssl] failed:` — so without this the only signal a CI +-- run gives is that something, somewhere, went wrong. The message is passed as +-- a single pre-formatted argument: log output contains `%` often enough that +-- handing it to a format string is its own failure mode. +local function run(step, logf, cmd) + local ok, err = pcall(os.exec, string.format("bash -c %s", sh_quote(cmd))) + if ok then return true end + local tail = tail_lines(logf, 40) or "" + log.error("%s", "compat.openssl: " .. step .. " failed (" .. tostring(err) + .. ")\n--- last 40 lines of " .. tostring(logf) .. " ---\n" .. tail) + return false +end + local function _install_impl() if not have("perl") then log.error("compat.openssl: `perl` not found on PATH. OpenSSL's " @@ -165,12 +198,23 @@ local function _install_impl() local make = resolve_make() local jobs = (os.default_njob and os.default_njob()) or 4 local flags = "no-shared no-dso no-tests no-apps no-engine" - os.exec(string.format("bash -c %s", sh_quote(string.format( + + -- Record which make is in play: "3.81 vs 4.x" is the difference between a + -- build and a wall of Makefile syntax errors, and it is invisible after + -- the fact otherwise. + run("make --version", logf, string.format( + "%s --version >> %s 2>&1 || true", make, sh_quote(logf))) + + if not run("./config", logf, string.format( "cd %s && ./config --prefix=%s --libdir=lib %s >> %s 2>&1", - sh_quote(srcroot), sh_quote(prefix), flags, sh_quote(logf))))) - os.exec(string.format("bash -c %s", sh_quote(string.format( + sh_quote(srcroot), sh_quote(prefix), flags, sh_quote(logf))) then + return false + end + if not run("make", logf, string.format( "cd %s && %s -j%d >> %s 2>&1", - sh_quote(srcroot), make, jobs, sh_quote(logf))))) + sh_quote(srcroot), make, jobs, sh_quote(logf))) then + return false + end -- macOS only: `make install_dev` runs `$(RANLIB) -c`, and the toolchain -- puts llvm-ranlib (which rejects -c) ahead of the system one on PATH. @@ -178,9 +222,11 @@ local function _install_impl() -- is confined to the platform that needs it — a Linux container without -- /usr/bin/ranlib would otherwise fail install_sw for no reason. local ranlib = (os.host() == "macosx") and "RANLIB=/usr/bin/ranlib " or "" - os.exec(string.format("bash -c %s", sh_quote(string.format( + if not run("make install_sw", logf, string.format( "cd %s && %s%s install_sw >> %s 2>&1", - sh_quote(srcroot), ranlib, make, sh_quote(logf))))) + sh_quote(srcroot), ranlib, make, sh_quote(logf))) then + return false + end -- Verify the build produced the expected archives. local libdir = path.join(prefix, "lib") From f9b0c6284b8823b3cfced3132b85b6f6e941dd11 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 27 Jul 2026 01:08:53 +0800 Subject: [PATCH 3/4] fix(openssl): drop the macosx xim:make dep, pin Apple's cc, dump build logs in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `xim:make` has no macOS build — xim-pkgindex's pkgs/m/make.lua declares only an `xpm.linux` block — so yesterday's macosx dep failed resolution outright with `E_INVALID_INPUT: package 'xim:make@latest' not found`, before install() ran. Reverted, with the finding written down. compat.openblas declares the same dep on macosx and is broken identically; nobody has noticed because that package is Windows-only in the test suite, so its macOS path is never taken. That leaves the original macOS failure — 43s after download, no message — still undiagnosed, so this stops guessing at it: * The workspace job gains an `if: failure()` step that finds every `mcpp_*_build.log` under the members and the registry and prints its tail. install()-driven packages build through their own Make/Configure system, xim's interface mode swallows that output, and xlings reports only `E_INTERNAL: [] failed:` — so a platform-specific break otherwise costs one full CI round-trip per hypothesis. * The log now opens with make/cc/perl versions, not just make's. And one likely cause addressed while here: OpenSSL is configured and built outside mcpp's compile rules, so it inherits none of the resolved toolchain's flags — it just runs `cc`. On macOS the `cc` in PATH is xim's llvm, which has no macOS SDK wired up, so every compile would fail on . macOS now builds with `CC=/usr/bin/cc`, Apple's driver, which finds the SDK itself (same spirit as the RANLIB pin, which is already macOS-only). resolve_make() also prefers a Homebrew gmake when one exists, since /usr/bin/make is GNU Make 3.81. If the SDK guess is wrong, the log tail now says what is. Verified on linux (cold): log opens with GNU Make 4.3 / gcc 16.1.0, openssl member passes. --- .github/workflows/validate.yml | 18 ++++++++++++ pkgs/c/compat.openssl.lua | 54 ++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 833e26f..4a7d33f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -393,3 +393,21 @@ jobs: exit $rc fi + # install()-driven packages (openssl, openblas) build through their own + # Make/Configure system, whose output xim's interface mode swallows; a + # failed hook surfaces only as `E_INTERNAL: [] failed:`. Each writes + # a log into its install prefix, so on failure surface those — otherwise + # diagnosing a platform-specific build break costs a full CI round-trip + # per guess. + - name: Dump install() build logs on failure + if: failure() + shell: bash + run: | + found=0 + while IFS= read -r log; do + found=1 + echo "::group::$log" + tail -80 "$log" + echo "::endgroup::" + done < <(find tests/examples "$HOME/.mcpp/registry" -name 'mcpp_*_build.log' 2>/dev/null) + [ "$found" = 1 ] || echo "no install() build logs found" diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index a03013d..3c5aaea 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -4,8 +4,14 @@ -- -- OpenSSL builds through its own Perl Configure + GNU Make system, which does -- not fit mcpp's "list the .c files" model. The xpkg install() hook runs that --- build (build-dep `xim:make@latest`) and lays the lib + headers under the --- install dir. +-- build and lays the lib + headers under the install dir. GNU Make comes from +-- the `xim:make@latest` build dep on linux; that package has no macOS build, +-- so there resolve_make() falls back to PATH. +-- +-- Because that build runs OUTSIDE mcpp's compile rules, it inherits none of +-- the resolved toolchain's flags — it just calls `cc`. Anything the host +-- toolchain needs spelled out (macOS SDK, ranlib) has to be handed to it +-- explicitly; see cc_override() and the install_sw step. -- -- HOST REQUIREMENT — perl. `./config` IS a Perl script, and there is no -- `xim:perl` to declare as a build dep, so this is the one thing the package @@ -41,12 +47,14 @@ package = { }, }, macosx = { - -- xim:make is declared here for the same reason as linux, and NOT - -- left to PATH: macOS does ship a /usr/bin/make, but it is GNU Make - -- 3.81 (the last GPLv2 release, frozen in 2006), and OpenSSL 3.x's - -- generated Makefile does not build with it. compat.openblas - -- declares the same dep on macosx. - deps = { "xim:make@latest" }, + -- NO xim:make here: that package is linux-only + -- (xim-pkgindex pkgs/m/make.lua declares only an `xpm.linux` + -- block), so declaring it fails resolution outright with + -- `E_INVALID_INPUT: package 'xim:make@latest' not found` — before + -- install() ever runs. compat.openblas declares it on macosx and + -- is broken the same way; it goes unnoticed because that package + -- is Windows-only in the test suite, so its macOS path is never + -- taken. resolve_make() therefore falls back to PATH here. ["3.5.1"] = { url = { GLOBAL = "https://github.com/openssl/openssl/releases/download/openssl-3.5.1/openssl-3.5.1.tar.gz", @@ -112,9 +120,29 @@ local function resolve_make() local cand = path.join(mk.bin, "make") if os.isfile(cand) then return cand end end + -- No build dep (macOS): prefer a Homebrew `gmake` when present. macOS's + -- own /usr/bin/make is GNU Make 3.81, frozen in 2006. + if os.host() == "macosx" and os.isfile("/opt/homebrew/bin/gmake") then + return "/opt/homebrew/bin/gmake" + end return "make" end +-- The C compiler OpenSSL's own Makefile should use. +-- +-- OpenSSL is configured and built outside mcpp's compile rules, so it does not +-- inherit the resolved toolchain's sysroot flags — it just runs `cc`. On macOS +-- the toolchain in PATH is xim's llvm, which has no macOS SDK wired up, so +-- every compile would fail on . Pin Apple's own driver, which finds +-- the SDK by itself. Left alone elsewhere: on linux the xim gcc carries its +-- own payload and is the right compiler to use. +local function cc_override() + if os.host() == "macosx" and os.isfile("/usr/bin/cc") then + return "CC=/usr/bin/cc " + end + return "" +end + local function have(tool) return pcall(function() os.exec(string.format("bash -c %s", @@ -202,12 +230,14 @@ local function _install_impl() -- Record which make is in play: "3.81 vs 4.x" is the difference between a -- build and a wall of Makefile syntax errors, and it is invisible after -- the fact otherwise. - run("make --version", logf, string.format( - "%s --version >> %s 2>&1 || true", make, sh_quote(logf))) + run("build environment", logf, string.format( + "{ %s --version; echo \"cc: $(command -v cc)\"; cc --version; " .. + "perl --version; } >> %s 2>&1 || true", make, sh_quote(logf))) + local cc = cc_override() if not run("./config", logf, string.format( - "cd %s && ./config --prefix=%s --libdir=lib %s >> %s 2>&1", - sh_quote(srcroot), sh_quote(prefix), flags, sh_quote(logf))) then + "cd %s && %s./config --prefix=%s --libdir=lib %s >> %s 2>&1", + sh_quote(srcroot), cc, sh_quote(prefix), flags, sh_quote(logf))) then return false end if not run("make", logf, string.format( From 232c1b89b73f1faff288d8646b04c0c6959c1916 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Mon, 27 Jul 2026 02:14:48 +0800 Subject: [PATCH 4/4] fix(openssl): pass RANLIB as a make command-line assignment, not an env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS build now gets all the way through configure and compile — the CC pin was the missing piece there — and dies in `make install_sw`, one line after copying libcrypto.a into place: install libcrypto.a -> .../3.5.1/lib/libcrypto.a llvm-ranlib: error: Invalid option: '-c' make: *** [install_dev] Error 1 which is precisely what the RANLIB override exists to prevent. OpenSSL's `darwin-common` sets `ranlib => "ranlib -c"` (Configurations/10-main.conf:1844) and PATH resolves `ranlib` to the toolchain's llvm-ranlib, which rejects `-c`. The override was not taking effect, and that is a regression I introduced when confining it to macOS: I moved it from `make RANLIB=… install_sw` to `RANLIB=… make install_sw`. The first is a command-line assignment and beats the Makefile's own definition; the second is only an environment variable, which a Makefile assignment overrides absent `make -e` — so it silently did nothing and the build failed exactly as if it were not there. Restored to the command-line form, with a comment saying why the position matters. Also confirmed from the same log that `--libdir=lib` does its job on macOS: the archive installs to `3.5.1/lib/`, not `lib64/`. Verified on linux (cold): openssl member passes. --- pkgs/c/compat.openssl.lua | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/pkgs/c/compat.openssl.lua b/pkgs/c/compat.openssl.lua index 3c5aaea..47ac3e4 100644 --- a/pkgs/c/compat.openssl.lua +++ b/pkgs/c/compat.openssl.lua @@ -246,15 +246,23 @@ local function _install_impl() return false end - -- macOS only: `make install_dev` runs `$(RANLIB) -c`, and the toolchain - -- puts llvm-ranlib (which rejects -c) ahead of the system one on PATH. - -- Pinning an absolute /usr/bin/ranlib is itself a host assumption, so it - -- is confined to the platform that needs it — a Linux container without - -- /usr/bin/ranlib would otherwise fail install_sw for no reason. - local ranlib = (os.host() == "macosx") and "RANLIB=/usr/bin/ranlib " or "" + -- macOS only: Configure bakes `RANLIB = ranlib -c` into the Makefile for + -- darwin targets, and the `ranlib` that PATH resolves to is the + -- toolchain's llvm-ranlib, which rejects `-c` — install_dev then dies + -- right after copying libcrypto.a. Point RANLIB at Apple's own, without + -- the flag. Confined to the platform that needs it: a Linux container + -- without /usr/bin/ranlib should not fail install_sw for no reason. + -- + -- It MUST be spelled `make RANLIB=… install_sw` and not + -- `RANLIB=… make install_sw`. The first is a command-line assignment, + -- which beats the Makefile's own; the second is merely an environment + -- variable, which a Makefile assignment overrides (absent `make -e`), so + -- the override silently does nothing and the build fails exactly as if it + -- were not there. + local ranlib = (os.host() == "macosx") and " RANLIB=/usr/bin/ranlib" or "" if not run("make install_sw", logf, string.format( "cd %s && %s%s install_sw >> %s 2>&1", - sh_quote(srcroot), ranlib, make, sh_quote(logf))) then + sh_quote(srcroot), make, ranlib, sh_quote(logf))) then return false end