Files
Alexander J. Semenuk 5d5e35fb9b fix: Windows toolchain compatibility (curl 8.21 re-vendor, endless reconfigure loop) (#4355)
## Problem

Windows builds break with a current local toolchain (Scoop LLVM 22.1.8,
CMake 4.4.0, VS 2026), in two independent ways:

1. The build stops at curl's deliberate guard: `#error "no non-blocking
method was found/used/set"` in `third-party/curl/lib/nonblock.c`.
2. From the second configure onward, `cmake --build` re-runs CMake in an
endless loop (observed 42 consecutive reconfigure cycles in a single
build). Likely the same mechanism behind the "endlessly building" VS
2026 note in `docs/setup/dev/vs.md`.

## Root cause

1. `third-party/curl/CMake/CurlTests.c` passes `int *` to
`ioctlsocket()`, whose third parameter is `u_long *`. Clang 22 promotes
`-Wincompatible-pointer-types` to a hard error in C, so the
`HAVE_IOCTLSOCKET_FIONBIO` try_compile silently fails and
`curl_config.h` never defines it. Upstream CI does not see this because
the windows-2022 runner image ships an older LLVM. GCC 14 promotes the
same warning to a hard error, which is very likely the `CurlTests.c.obj`
failure reported from MSYS2 in open-goal/jak-project#3551. Upstream curl
hit the identical problem with GCC 14 and fixed the probe in curl 8.8.0
(curl/curl#13578).
2. The root CMakeLists copies the build tree's `compile_commands.json`
into `<src>/build/` for clangd using `configure_file()`, which registers
its input as a configure dependency. CMake rewrites
`compile_commands.json` late in every generation, after
`CTestTestfile.cmake` and `cmake_install.cmake` (outputs of the same
Ninja regen rule), so once the dependency is registered the rule is
deterministically dirty and every `ninja` invocation re-runs CMake. A
pristine first configure is safe (the file does not exist yet, so the
`if(EXISTS ...)` guard skips the copy), which is why the loop looks
machine- or IDE-specific.

## Fix

1. Per review, re-vendor `third-party/curl` at the `curl-8_21_0` tag
(previously `curl-8_3_0`), which carries the upstream probe fix plus two
years of upstream development; `vendor.yaml` updated to match.
Adjustments the version jump forced:
- curl 8.15 removed the native macOS Secure Transport backend
(`CURL_USE_SECTRANSP`), so macOS now builds curl against OpenSSL like
Linux. The two macOS workflows install Homebrew `openssl@3` and export
`OPENSSL_ROOT_DIR` (keg-only), and the macOS setup docs gained the same
two lines.
- `CURL_BROTLI` / `CURL_ZSTD` switched to AUTO-detection in curl 8.10;
pinned OFF to keep the previous no-compression behavior and avoid
silently linking whatever the CI images happen to have.
- curl's new top-level `BUILD_EXAMPLES` cache option (default ON) leaked
into discord-rpc's identically named option and broke configure at a
nonexistent `examples/send-presence` directory; pinned OFF ahead of the
third-party subdirectories.

The diff is dominated by the mechanical tag-tree swap under
`third-party/curl` (linguist-vendored, collapsed in review). The
hand-written changes are `CMakeLists.txt`, the two macOS workflows,
`docs/setup/system/macos.md`, and `vendor.yaml`.
2. Swap `configure_file()` for `file(COPY ...)`: the same clangd copy
with no configure dependency registered. (`file(COPY_FILE ...
ONLY_IF_DIFFERENT)` would be cleaner still but requires CMake 3.21,
above the declared `cmake_minimum_required(VERSION 3.10)`.)

## Test plan

- [x] Fresh `cmake --preset Release-windows-clang` (LLVM 22, no cache
seeding) completes and logs `Enabled SSL backends: Schannel`; the
FIONBIO probe passes without the previous `#error`
- [x] Full Windows Release build from scratch in the branch worktree
(all 1422 targets)
- [x] goalc-test suite: 1509 passed, 0 failed
- [x] Second consecutive configure with `compile_commands.json` present:
the regen rule in `build.ninja` has no `compile_commands.json` input;
`<src>/build/compile_commands.json` is still refreshed for clangd
- [x] Repeated `ninja` invocations after a full build no longer re-run
CMake
- [x] macOS Intel and ARM CI green (first exercise of the OpenSSL
backend switch)

---

I work off a self-hosted forge, so this GitHub account is quiet; the
configure logs and ninja dirty-node traces from the investigation are
available if anyone wants the raw data.

(AI-assisted)
2026-07-27 19:19:18 -04:00

318 lines
8.7 KiB
C
Vendored
Generated

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curlx/strparse.h"
void curlx_str_init(struct Curl_str *out)
{
out->str = NULL;
out->len = 0;
}
void curlx_str_assign(struct Curl_str *out, const char *str, size_t len)
{
out->str = str;
out->len = len;
}
/* remove bytes from the end of the string, never remove more bytes than what
the string holds! */
void curlx_str_trim(struct Curl_str *out, size_t len)
{
DEBUGASSERT(out);
DEBUGASSERT(out->len >= len);
out->len -= len;
}
/* Get a word until the first DELIM or end of string. At least one byte long.
return non-zero on error. If 'max' is zero, it will always return error. */
int curlx_str_until(const char **linep, struct Curl_str *out,
const size_t max, char delim)
{
const char *s;
size_t len = 0;
DEBUGASSERT(linep);
DEBUGASSERT(*linep);
DEBUGASSERT(out);
DEBUGASSERT(delim);
s = *linep;
curlx_str_init(out);
while(*s && (*s != delim)) {
s++;
if(++len > max) {
return STRE_BIG;
}
}
if(!len)
return STRE_SHORT;
out->str = *linep;
out->len = len;
*linep = s; /* point to the first byte after the word */
return STRE_OK;
}
/* Get a word until the first space or end of string. At least one byte long.
return non-zero on error */
int curlx_str_word(const char **linep, struct Curl_str *out, const size_t max)
{
return curlx_str_until(linep, out, max, ' ');
}
/* Get a word until a newline byte or end of string. At least one byte long.
return non-zero on error */
int curlx_str_untilnl(const char **linep, struct Curl_str *out,
const size_t max)
{
const char *s = *linep;
size_t len = 0;
DEBUGASSERT(linep && *linep && out && max);
curlx_str_init(out);
while(*s && !ISNEWLINE(*s)) {
s++;
if(++len > max)
return STRE_BIG;
}
if(!len)
return STRE_SHORT;
out->str = *linep;
out->len = len;
*linep = s; /* point to the first byte after the word */
return STRE_OK;
}
/* Get a "quoted" word. Escaped quotes are supported.
return non-zero on error */
int curlx_str_quotedword(const char **linep, struct Curl_str *out,
const size_t max)
{
const char *s = *linep;
size_t len = 0;
DEBUGASSERT(linep && *linep && out && max);
curlx_str_init(out);
if(*s != '\"')
return STRE_BEGQUOTE;
s++;
while(*s && (*s != '\"')) {
if(*s == '\\' && s[1]) {
s++;
if(++len > max)
return STRE_BIG;
}
s++;
if(++len > max)
return STRE_BIG;
}
if(*s != '\"')
return STRE_ENDQUOTE;
out->str = (*linep) + 1;
out->len = len;
*linep = s + 1;
return STRE_OK;
}
/* Advance over a single character.
return non-zero on error */
int curlx_str_single(const char **linep, char byte)
{
DEBUGASSERT(linep && *linep);
if(**linep != byte)
return STRE_BYTE;
(*linep)++; /* move over it */
return STRE_OK;
}
/* Advance over a single space.
return non-zero on error */
int curlx_str_singlespace(const char **linep)
{
return curlx_str_single(linep, ' ');
}
/* given an ASCII character and max ascii, return TRUE if valid */
#define valid_digit(x, m) \
(((x) >= '0') && ((x) <= (m)) && curlx_hexasciitable[(x) - '0'])
/* We use 16 for the zero index (and the necessary bitwise AND in the loop)
to be able to have a non-zero value there to make valid_digit() able to
use the info */
const unsigned char curlx_hexasciitable[] = {
16, 1, 2, 3, 4, 5, 6, 7, 8, 9, /* 0x30: 0 - 9 */
0, 0, 0, 0, 0, 0, 0,
10, 11, 12, 13, 14, 15, /* 0x41: A - F */
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
10, 11, 12, 13, 14, 15 /* 0x61: a - f */
};
/* no support for 0x prefix nor leading spaces */
static int str_num_base(const char **linep, curl_off_t *nump, curl_off_t max,
int base) /* 8, 10 or 16, nothing else */
{
curl_off_t num = 0;
const char *p;
int m = (base == 10) ? '9' : /* the largest digit possible */
(base == 16) ? 'f' : '7';
DEBUGASSERT(linep && *linep && nump);
DEBUGASSERT((base == 8) || (base == 10) || (base == 16));
DEBUGASSERT(max >= 0); /* mostly to catch SIZE_MAX, which is too large */
*nump = 0;
p = *linep;
if(!valid_digit(*p, m))
return STRE_NO_NUM;
if(max < base) {
/* special-case low max scenario because check needs to be different */
do {
int n = curlx_hexval(*p++);
num = (num * base) + n;
if(num > max)
return STRE_OVERFLOW;
} while(valid_digit(*p, m));
}
else {
do {
int n = curlx_hexval(*p++);
if(num > ((max - n) / base))
return STRE_OVERFLOW;
num = (num * base) + n;
} while(valid_digit(*p, m));
}
*nump = num;
*linep = p;
return STRE_OK;
}
/* Get an unsigned decimal number with no leading space or minus. Leading
zeroes are accepted. return non-zero on error */
int curlx_str_number(const char **linep, curl_off_t *nump, curl_off_t max)
{
return str_num_base(linep, nump, max, 10);
}
/* Get an unsigned hexadecimal number with no leading space or minus and no
"0x" support. Leading zeroes are accepted. return non-zero on error */
int curlx_str_hex(const char **linep, curl_off_t *nump, curl_off_t max)
{
return str_num_base(linep, nump, max, 16);
}
/* Get an unsigned octal number with no leading space or minus and no "0"
prefix support. Leading zeroes are accepted. return non-zero on error */
int curlx_str_octal(const char **linep, curl_off_t *nump, curl_off_t max)
{
return str_num_base(linep, nump, max, 8);
}
/*
* Parse a positive number up to 63-bit number written in ASCII. Skip leading
* blanks. No support for prefixes.
*/
int curlx_str_numblanks(const char **str, curl_off_t *num)
{
curlx_str_passblanks(str);
return curlx_str_number(str, num, CURL_OFF_T_MAX);
}
/* CR or LF
return non-zero on error */
int curlx_str_newline(const char **linep)
{
DEBUGASSERT(linep && *linep);
if(ISNEWLINE(**linep)) {
(*linep)++;
return STRE_OK; /* yessir */
}
return STRE_NEWLINE;
}
#ifndef WITHOUT_LIBCURL
/* case insensitive compare that the parsed string matches the given string.
Returns non-zero on match. */
int curlx_str_casecompare(struct Curl_str *str, const char *check)
{
size_t clen = check ? strlen(check) : 0;
return ((str->len == clen) && curl_strnequal(str->str, check, clen));
}
#endif
/* case sensitive string compare. Returns non-zero on match. */
int curlx_str_cmp(struct Curl_str *str, const char *check)
{
if(check) {
size_t clen = strlen(check);
return ((str->len == clen) && !strncmp(str->str, check, clen));
}
return !!(str->len);
}
/* Trim off 'num' number of bytes from the beginning (left side) of the
string. If 'num' is larger than the string, return error. */
int curlx_str_nudge(struct Curl_str *str, size_t num)
{
if(num <= str->len) {
str->str += num;
str->len -= num;
return STRE_OK;
}
return STRE_OVERFLOW;
}
/* Get the following character sequence that consists only of bytes not
present in the 'reject' string. Like strcspn(). */
int curlx_str_cspn(const char **linep, struct Curl_str *out,
const char *reject)
{
const char *s = *linep;
size_t len;
DEBUGASSERT(linep && *linep);
len = strcspn(s, reject);
if(len) {
out->str = s;
out->len = len;
*linep = &s[len];
return STRE_OK;
}
curlx_str_init(out);
return STRE_SHORT;
}
/* remove ISBLANK()s from both ends of the string */
void curlx_str_trimblanks(struct Curl_str *out)
{
while(out->len && ISBLANK(*out->str))
curlx_str_nudge(out, 1);
/* trim trailing spaces and tabs */
while(out->len && ISBLANK(out->str[out->len - 1]))
out->len--;
}
/* increase the pointer until it has moved over all blanks */
void curlx_str_passblanks(const char **linep)
{
while(ISBLANK(**linep))
(*linep)++; /* move over it */
}